Jetpack系列(三) -- LiveData

前言

时间: 23/09/17

AndroidStudio版本: Giraffe 2022.3.1 JDK:17 开发语言: Kotlin

Gradle版本: 8.0 Gradle plugin Version: 8.1.1

基本使用

  • 添加依赖

    groovy 复制代码
    implementation("androidx.lifecycle:lifecycle-livedata:2.6.2")
    implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.2")
  • 修改MainViewModel

    • 给ViewModel添加 MutableLiveData<> 类型变量。

      kotlin 复制代码
      private var result = MutableLiveData<Long>()
    • 设置LiveData类型常量并赋值为result

      kotlin 复制代码
      val _result: LiveData<Long> = result
    • 因为result是私有变量,所以需要写一个修改值的方法

      kotlin 复制代码
      fun setResult(showLong: Long) {
          result.value = showLong
          time = showLong.times(1000)
      }
    • 整体

      kotlin 复制代码
      class MainViewModel : ViewModel() {
          var time: Long = 5000
      
          private var result = MutableLiveData<Long>()
      
          val _result: LiveData<Long> = result
      
          fun setResult(showLong: Long) {
              result.value = showLong
              time = showLong.times(1000)
          }
      
      }
  • 添加一个MainManage类,用来控制ViewModel中的数据变化

    kotlin 复制代码
    class MainManager(mainViewModel: MainViewModel) : LifecycleEventObserver {
        private val TAG = "MainManager"
    
        private var countDownTimer: CountDownTimer? = object : CountDownTimer(mainViewModel.time, 1000) {//添加一个倒计时,时间为time=5000ms
            override fun onTick(timeAfter: Long) {
                Log.d(TAG, "倒计时 --- ${timeAfter / 1000}s")
                mainViewModel.setResult(timeAfter / 1000)
            }
    
            override fun onFinish() {
                Log.d(TAG, "倒计时 结束")
            }
    
        }
    
    
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
            Log.d(TAG, "onStateChanged: $event")
            when (event) {
                Lifecycle.Event.ON_START -> countDownTimer?.start()
                Lifecycle.Event.ON_DESTROY -> {
                    countDownTimer?.cancel()
                    countDownTimer = null
                }
                else -> {}
            }
        }
    }
  • MainActivity中添加LiveData的observe

    kotlin 复制代码
    class MainActivity : AppCompatActivity() {
        private val TAG = "MainActivity"
    
        private lateinit var mainViewModel : MainViewModel
        private lateinit var binding: ActivityMainBinding
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            binding = ActivityMainBinding.inflate(layoutInflater)
            setContentView(binding.root)
            mainViewModel = ViewModelProvider(this)[MainViewModel::class.java]
            val mainManager = MainManager(mainViewModel)
            lifecycle.addObserver(mainManager)
            mainViewModel._result.observe(this) {//观察_result,_result的值会随着result变化而变化
                binding.showTime.text = "倒计时还剩下 ${it}s"//it是LiveData<>规定的类型Long
                if (it == 0L) {
                    Log.d(TAG, "  OVER!! ")
                    binding.showTime.text = "倒计时结束"//这里使用了ViewBinding,这个后续讲。
                    //不会使用可以先用findViewById绑定好视图再操作
                    //其实前面两篇文章我已经用到了这个,之前写代码的习惯,这个也是jetpack的组件技术,所以就后续再细说
                }
            }
        }
    }

Log显示

这里用的是私有变量,如果使用的kotlin默认的public的话,可以直接使用变量去完成这个observe的操作。但是共用变量可能会导致数据操作的完整性出现问题,因此我这边直接用的私有来声明

LiveData<>类型转化方法 - map & switchMap

从前面所讲的和代码来看,我们观察所接收到的是LiveData所指定的类型,如果这个类型是我们自己写的一个实体(entity)类,那么我们的需求有很大的可能是要拿到这个类中的某个属性,于是我们在拿到这个实体类后还需要进行加工,这样我们每次都要使用getter方法增加 Activity 中的代码量。

这个时候就需要使用到类型转化方法 ------ map 或者 switchMap 。

map

编写一个Data类Team,设置三个属性,id、name、point

kotlin 复制代码
data class Team(var name: String, var id: Long, var point: Int)

然后再编写一个TeamViewModel,对应Team。

kotlin 复制代码
class TeamViewModel : ViewModel() {
    private var teamData = MutableLiveData<Team>()
    val _teamData: LiveData<Team> = teamData

    fun setTeamData(team: Team) {
        teamData.value = team
    }
}

然后在Activity中observe,这个时候观察的对象就是Team。如果我们只是想要显示point属性,那么就需要使用getter。

假设我们的需求就只是获取每一个team的分数,那我们可以对ViewModel中的LiveData进行修改。

由于书本中Transformations这个类被Google "优化" 了。现在可以直接用LiveData使用map方法,即teamData.map

kotlin 复制代码
    val _teamData: LiveData<Int> = teamData.map {
        it.point
   }

这样就可以把LiveData指定的类型改为point的类型。在观察时直接使用即可。

布局和Activity的代码往下看

switchMap

switchMap转化是将从外部,例如其它app进程或网络中获取到的LiveData转化为可观察的方式。举个栗子。

新建一个TeamRespository类,添加getTeamPoint方法。

kotlin 复制代码
class TeamRespository {
    /*
    * 根据id获取得分,'模拟'网络请求
    */
    fun getTeamPoint(id: Int): LiveData<Int> {
        val teamScoreLiveData = MutableLiveData<Int>()
        if (id == 7) {//图个方便
            teamScoreLiveData.value = 55
        } else {
            teamScoreLiveData.value = 0
        }
        return teamScoreLiveData
    }
}

这个getTeamPoint 方法后续会在 ViewModel 中调用 。先看下layout布局。

新建一个activity_team.xml到layout中。

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".activities.MainActivity">

        <TextView
            android:id="@+id/team_info"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"
            android:textSize="32sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/textInputLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginEnd="16dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/team_info">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/input_text"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center"
                android:inputType="number"
                android:hint="Please text Team ID" />
        </com.google.android.material.textfield.TextInputLayout>

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/submit"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="submit"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textInputLayout" />

        <TextView
            android:id="@+id/score_show"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/submit" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

主要是添加了一个输入框和按钮,来通过输入的ID获取得分。

看到按钮,这个时候第一时间想到的实现方法,就是在点击监听中,拿到ID,调取获取得分的方法,显示出结果。

在ViewModel中添加一个返回类型为LiveData的getPoint方法,这样才可以在 Activity 中添加 observe 方法。

kotlin 复制代码
//viewmodel中
fun getPoint(id: Int): LiveData<Int> {
	return TeamRespository().getTeamPoint(id)
}
//activity中
teamViewModel.setTeamData(team1)
binding.submit.setOnClickListener {
	teamViewModel.getPoint(binding.inputText.text.toString().toInt()).observe(this, Observer { 
		binding.scoreShow.text = "score: $it"
	})
}

看着会觉得有点奇怪,因为每次获取得分的时候都会返回一个新的LiveData,所以这个observe其实压根没啥作用,甚至不如直接写成

kotlin 复制代码
binding.submit.setOnClickListener {
    binding.scoreShow.text
    =  "score: ${teamViewModel.getPoint(binding.inputText.text.toString().toInt())}"
}

嘿嘿,甚至看着还更简单点。但是这中方式完全就和LiveData不搭边,甚至之前写的全都是冗余代码。

怎么解决这种情况呢?可以看到,我们是通过ID获取对应的分数的,那我们就可以定义一个可观察的id变量

kotlin 复制代码
//viewmodel中
private var teamIdLiveData = MutableLiveData<Int>()

fun setTeamId(teamId: Int) {
    teamIdLiveData.value = teamId
}

然后通过switchMap将 teamIdLiveData 映射为返回的分数结果类型

kotlin 复制代码
//viewmodel中
var newPoint: LiveData<Int> = teamIdLiveData.switchMap {id: Int -> 
	return@switchMap TeamRespository().getTeamPoint(id)
}

这样我们就可以在activity中添加对newPoint的observe,点击事件则是调用setTeamId方法,因为调用了setTeamId方法 teamIdLiveData 的值才会改变,newPoint 的observe方法才会监听到变化执行相关操作。

kotlin 复制代码
teamViewModel.newPoint.observe(this) {
    binding.scoreShow.text = "score: $it"
}
binding.submit.setOnClickListener {
    teamViewModel.setTeamId(binding.inputText.text.toString().toInt())
}

result显示

总结

LiveData 一般都定义在 ViewModel 中,作为可观察数据,它具有生命周期感知的能力,可以确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。

Lifecycle、ViewModel 和 LiveData之间的关系比较密切,一般都是结合三者一起使用。利用好这三者可以大幅度简化 Activity 中的代码,为后续 mvvm 框架的解耦作好铺垫。

本文没有对于 LiveData 的原理进行剖析,因为原理代码相对比较简单。感兴趣的可以自行阅读,主要方向是 observe 方法。

相关推荐
ULTRA??5 小时前
归并排序算法实现,kotlin,c++,python
c++·python·kotlin
Kapaseker13 小时前
四大组件齐上阵,轻松拿捏实习生
android·kotlin
ljt27249606611 天前
Compose笔记(六十一)--SelectionContainer
android·笔记·android jetpack
Kapaseker2 天前
三分钟搞懂 Kotlin Flow 中的背压
android·kotlin
QING6182 天前
Jetpack Compose 中的 ViewModel 作用域管理 —— 新手指南
android·kotlin·android jetpack
惟恋惜2 天前
Jetpack Compose 的状态使用之“界面状态”
android·android jetpack
KotlinKUG贵州2 天前
Kotlin/Ktor 实践:利用 MCP 从零打造 AI Agent 服务端指南
kotlin·agent·mcp
喜熊的Btm2 天前
探索 Kotlin 的不可变集合库
kotlin·android jetpack
惟恋惜2 天前
Jetpack Compose 界面元素状态(UI Element State)详解
android·ui·android jetpack