MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kotlin Android ViewModel详解

2023-10-026.2k 阅读

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 会被重建,我们通过 onSaveInstanceStateonCreate 方法中的逻辑来恢复数据。然而,这种方式存在一些问题,比如代码耦合度高,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:FragmentAFragmentBFragmentA 中有一个输入框,用户输入文本后,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,FragmentAFragmentB 实现了数据的共享和通信。这种方式使得代码结构清晰,各个 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 的最佳实践

  1. 单一职责原则:确保 ViewModel 只负责管理与 UI 相关的数据和业务逻辑,避免在 ViewModel 中处理过多与 UI 无关的复杂业务。例如,网络请求的具体实现可以放在 Repository 层,ViewModel 只负责调用 Repository 获取数据并提供给视图。
  2. 数据验证:在 ViewModel 中对数据进行验证,确保提供给视图的数据是合法的。比如在处理用户输入时,检查输入格式是否正确,长度是否符合要求等。
  3. 避免内存泄漏:由于 ViewModel 的生命周期较长,要注意避免在 ViewModel 中持有对视图组件的强引用,否则可能会导致内存泄漏。例如,不要在 ViewModel 中直接持有 Activity 或 Fragment 的实例。
  4. 测试:对 ViewModel 进行单元测试非常重要。由于 ViewModel 不依赖于视图组件的生命周期,很容易进行单元测试。可以使用 Mockito 等框架来模拟依赖,测试 ViewModel 的各种业务逻辑。

总结

ViewModel 是 Kotlin Android 开发中管理 UI 数据的强大工具。它通过分离视图和数据逻辑,提高了代码的可维护性和可测试性。结合 LiveData,能实现数据的响应式更新,使 UI 与数据的交互更加流畅。在多个 Fragment 通信以及依赖注入方面,ViewModel 也有着出色的表现。遵循最佳实践,能让我们在项目开发中充分发挥 ViewModel 的优势,构建出高质量的 Android 应用。