Kotlin Android ViewModel详解
Kotlin Android ViewModel 基础概念
在 Android 开发中,ViewModel 是 Jetpack 组件库中的重要一员,它主要用于管理 UI 相关的数据,并在配置更改(如屏幕旋转)时保留数据。在 Kotlin 语言环境下使用 ViewModel,能充分利用 Kotlin 的简洁语法和特性,提升开发效率。
ViewModel 旨在分离视图(View)和数据逻辑。传统开发中,Activity 或 Fragment 不仅要处理 UI 展示,还要负责数据获取、处理和保存,这使得代码变得臃肿且难以维护。ViewModel 的出现,将数据相关的操作从视图组件中剥离出来,让视图专注于 UI 呈现,ViewModel 专注于数据管理。
以一个简单的计数器应用为例,假设我们有一个 Activity 来显示计数器的值,并通过按钮点击来增加计数。如果不使用 ViewModel,可能会这样写:
class CounterActivity : AppCompatActivity() {
private var count = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_counter)
val textView = findViewById<TextView>(R.id.counter_text_view)
val button = findViewById<Button>(R.id.increment_button)
if (savedInstanceState != null) {
count = savedInstanceState.getInt("count")
}
textView.text = count.toString()
button.setOnClickListener {
count++
textView.text = count.toString()
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("count", count)
}
}
上述代码中,Activity 既要处理 UI 元素(如按钮点击和文本显示),又要负责数据(计数器的值)的保存和恢复。当屏幕旋转时,Activity 会被重建,我们通过 onSaveInstanceState
和 onCreate
方法中的逻辑来恢复数据。然而,这种方式存在一些问题,比如代码耦合度高,Activity 职责不单一。
引入 ViewModel 后,代码结构会更加清晰。首先,创建一个继承自 ViewModel
的类来管理计数器数据:
class CounterViewModel : ViewModel() {
var count = 0
}
然后在 Activity 中使用这个 ViewModel:
class CounterActivity : AppCompatActivity() {
private lateinit var viewModel: CounterViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_counter)
viewModel = ViewModelProvider(this).get(CounterViewModel::class.java)
val textView = findViewById<TextView>(R.id.counter_text_view)
val button = findViewById<Button>(R.id.increment_button)
textView.text = viewModel.count.toString()
button.setOnClickListener {
viewModel.count++
textView.text = viewModel.count.toString()
}
}
}
在这个例子中,Activity 只负责 UI 相关的操作,而数据的管理完全交给了 CounterViewModel
。这样,当屏幕旋转时,ViewModel 中的数据不会丢失,因为 ViewModel 的生命周期与 Activity 的配置更改无关。
ViewModel 的生命周期
ViewModel 的生命周期与视图组件(Activity 或 Fragment)的生命周期紧密相关,但又有所不同。
ViewModel 的创建由 ViewModelProvider
负责。当一个视图组件(如 Activity)首次请求获取 ViewModel 时,ViewModelProvider
会创建一个新的 ViewModel 实例。这个实例会一直存活,直到关联的视图组件完成销毁(如 Activity 调用 onDestroy
且不是因为配置更改而销毁)。
以 Activity 为例,当 Activity 因为配置更改(如屏幕旋转)而重建时,新创建的 Activity 会复用之前创建的 ViewModel 实例。这是因为 ViewModel 的作用域是基于视图组件的 ViewModelStore
,而不是视图组件本身。ViewModelStore
在配置更改时会被保留,所以 ViewModel 也能保留其状态。
在 Fragment 中使用 ViewModel 时,ViewModel 的生命周期同样与 Fragment 的生命周期相关。如果一个 Fragment 被添加到 Activity 中,Fragment 可以通过 ViewModelProvider
获取与 Activity 共享的 ViewModel,或者获取属于自己的独立 ViewModel。当 Fragment 被销毁时,与之关联的 ViewModel 也会被销毁(如果没有其他视图组件依赖该 ViewModel)。
使用 LiveData 与 ViewModel 结合
LiveData 是一种可观察的数据持有者类,它与 ViewModel 结合使用能实现数据的响应式更新。LiveData 具有生命周期感知能力,它只会将数据变化通知给处于活跃状态(如 STARTED 或 RESUMED)的观察者。
继续以计数器应用为例,修改 CounterViewModel
以使用 LiveData:
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class CounterViewModel : ViewModel() {
private val _count = MutableLiveData<Int>()
val count: LiveData<Int>
get() = _count
init {
_count.value = 0
}
fun increment() {
_count.value = _count.value?.plus(1)
}
}
在上述代码中,_count
是一个 MutableLiveData
,用于存储计数器的值并能通知数据变化。count
是一个只读的 LiveData
,供外部观察数据。init
块中初始化 _count
的初始值为 0,increment
方法用于增加计数器的值。
在 Activity 中观察 LiveData
:
class CounterActivity : AppCompatActivity() {
private lateinit var viewModel: CounterViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_counter)
viewModel = ViewModelProvider(this).get(CounterViewModel::class.java)
val textView = findViewById<TextView>(R.id.counter_text_view)
val button = findViewById<Button>(R.id.increment_button)
viewModel.count.observe(this, Observer { newCount ->
textView.text = newCount.toString()
})
button.setOnClickListener {
viewModel.increment()
}
}
}
这里通过 viewModel.count.observe
方法注册了一个观察者,当 count
的值发生变化时,观察者的 onChanged
方法会被调用,从而更新 UI 上的文本。这种方式实现了数据与 UI 的解耦,当数据变化时,UI 能自动更新,而不需要手动处理。
ViewModel 与 Fragment 通信
在实际应用中,经常会遇到多个 Fragment 之间需要共享数据的情况。ViewModel 可以很好地解决这个问题。
假设我们有一个主 Activity,包含两个 Fragment:FragmentA
和 FragmentB
。FragmentA
中有一个输入框,用户输入文本后,FragmentB
要显示这个文本。
首先,创建一个共享的 ViewModel:
class SharedViewModel : ViewModel() {
private val _sharedText = MutableLiveData<String>()
val sharedText: LiveData<String>
get() = _sharedText
fun setSharedText(text: String) {
_sharedText.value = text
}
}
在 FragmentA
中,获取共享的 ViewModel 并设置文本:
class FragmentA : Fragment() {
private lateinit var viewModel: SharedViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_a, container, false)
viewModel = activity?.run {
ViewModelProvider(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
val editText = view.findViewById<EditText>(R.id.input_text)
val button = view.findViewById<Button>(R.id.send_button)
button.setOnClickListener {
val text = editText.text.toString()
viewModel.setSharedText(text)
}
return view
}
}
在 FragmentB
中,获取共享的 ViewModel 并观察文本变化:
class FragmentB : Fragment() {
private lateinit var viewModel: SharedViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_b, container, false)
viewModel = activity?.run {
ViewModelProvider(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
val textView = view.findViewById<TextView>(R.id.display_text)
viewModel.sharedText.observe(viewLifecycleOwner, Observer { newText ->
textView.text = newText
})
return view
}
}
通过共享的 ViewModel,FragmentA
和 FragmentB
实现了数据的共享和通信。这种方式使得代码结构清晰,各个 Fragment 之间的耦合度降低。
依赖注入与 ViewModel
在大型项目中,依赖注入(Dependency Injection,DI)是一种重要的设计模式,它可以提高代码的可测试性和可维护性。在使用 ViewModel 时,也可以结合依赖注入框架,如 Dagger。
假设我们的 CounterViewModel
需要依赖一个网络服务来获取初始计数器值,而不是简单地初始化为 0。首先定义一个网络服务接口:
interface CounterNetworkService {
suspend fun getInitialCount(): Int
}
然后创建一个实现该接口的类:
class CounterNetworkServiceImpl : CounterNetworkService {
override suspend fun getInitialCount(): Int {
// 实际实现中这里会进行网络请求
return 10
}
}
使用 Dagger 进行依赖注入,首先创建一个模块类:
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class CounterModule {
@Provides
@Singleton
fun provideCounterNetworkService(): CounterNetworkService {
return CounterNetworkServiceImpl()
}
}
接着创建一个组件接口:
import dagger.Component
import javax.inject.Singleton
@Singleton
@Component(modules = [CounterModule::class])
interface CounterComponent {
fun inject(counterViewModel: CounterViewModel)
}
在 CounterViewModel
中,通过构造函数注入依赖:
class CounterViewModel : ViewModel() {
private val _count = MutableLiveData<Int>()
val count: LiveData<Int>
get() = _count
private val counterNetworkService: CounterNetworkService
init {
viewModelScope.launch {
_count.value = counterNetworkService.getInitialCount()
}
}
fun increment() {
_count.value = _count.value?.plus(1)
}
@Inject
constructor(counterNetworkService: CounterNetworkService) {
this.counterNetworkService = counterNetworkService
}
}
在 Activity 中,初始化 Dagger 组件并注入 ViewModel:
class CounterActivity : AppCompatActivity() {
private lateinit var viewModel: CounterViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_counter)
val component = DaggerCounterComponent.builder().build()
viewModel = ViewModelProvider(this, object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(CounterViewModel::class.java)) {
component.inject(viewModel as CounterViewModel)
return viewModel as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}).get(CounterViewModel::class.java)
val textView = findViewById<TextView>(R.id.counter_text_view)
val button = findViewById<Button>(R.id.increment_button)
viewModel.count.observe(this, Observer { newCount ->
textView.text = newCount.toString()
})
button.setOnClickListener {
viewModel.increment()
}
}
}
通过依赖注入,CounterViewModel
的依赖(CounterNetworkService
)可以在外部进行配置和管理,提高了代码的灵活性和可测试性。
ViewModel 的最佳实践
- 单一职责原则:确保 ViewModel 只负责管理与 UI 相关的数据和业务逻辑,避免在 ViewModel 中处理过多与 UI 无关的复杂业务。例如,网络请求的具体实现可以放在 Repository 层,ViewModel 只负责调用 Repository 获取数据并提供给视图。
- 数据验证:在 ViewModel 中对数据进行验证,确保提供给视图的数据是合法的。比如在处理用户输入时,检查输入格式是否正确,长度是否符合要求等。
- 避免内存泄漏:由于 ViewModel 的生命周期较长,要注意避免在 ViewModel 中持有对视图组件的强引用,否则可能会导致内存泄漏。例如,不要在 ViewModel 中直接持有 Activity 或 Fragment 的实例。
- 测试:对 ViewModel 进行单元测试非常重要。由于 ViewModel 不依赖于视图组件的生命周期,很容易进行单元测试。可以使用 Mockito 等框架来模拟依赖,测试 ViewModel 的各种业务逻辑。
总结
ViewModel 是 Kotlin Android 开发中管理 UI 数据的强大工具。它通过分离视图和数据逻辑,提高了代码的可维护性和可测试性。结合 LiveData,能实现数据的响应式更新,使 UI 与数据的交互更加流畅。在多个 Fragment 通信以及依赖注入方面,ViewModel 也有着出色的表现。遵循最佳实践,能让我们在项目开发中充分发挥 ViewModel 的优势,构建出高质量的 Android 应用。