Jetpack ViewModel 详解

1. ViewModel 概述

ViewModel 是 Android Jetpack 组件库中的一个核心组件,它的主要设计目标是:

  • 分离关注点:将 UI 相关的数据逻辑从 UI 控制器(Activity/Fragment)中分离出来
  • 生命周期感知:在配置更改(如屏幕旋转)期间保留数据
  • UI 状态管理:提供一个存储和管理 UI 状态的中心点
  • 组件间通信:简化 Fragment 之间的通信

2. ViewModel 实例的构建

2.1 基本实现

创建一个 ViewModel 类非常简单,只需继承 ViewModel 基类:

class MyViewModel : ViewModel() {
    // 数据存储和业务逻辑
    private val _counter = MutableLiveData(0)
    val counter: LiveData<Int> get() = _counter
    
    fun incrementCounter() {
        _counter.value = (_counter.value ?: 0) + 1
    }
}

2.2 使用 ViewModelProvider

在 Activity 或 Fragment 中获取 ViewModel 实例:

class MyActivity : AppCompatActivity() {
    private lateinit var viewModel: MyViewModel
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_my)
        
        // 方法 1:使用 ViewModelProvider
        viewModel = ViewModelProvider(this)[MyViewModel::class.java]
        
        // 观察数据变化
        viewModel.counter.observe(this) { count ->
            textView.text = count.toString()
        }
    }
}

2.3 使用 ViewModelFactory

当 ViewModel 需要构造函数参数时,需要使用 ViewModelProvider.Factory

class MyViewModel(private val repository: UserRepository) : ViewModel() {
    // ViewModel 逻辑
}

class MyViewModelFactory(private val repository: UserRepository) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
            return MyViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

// 在 Activity/Fragment 中使用
val viewModel = ViewModelProvider(this, MyViewModelFactory(repository))[MyViewModel::class.java]

2.4 使用 KTX 扩展函数

通过 Android KTX,可以使用更简洁的语法:

// 在 Activity 中
val viewModel by viewModels<MyViewModel>()

// 带 Factory 的版本
val viewModel by viewModels<MyViewModel> {
    MyViewModelFactory(repository)
}

// 在 Fragment 中
val viewModel by activityViewModels<MyViewModel>() // 共享 Activity 的 ViewModel
val viewModel by viewModels<MyViewModel>() // Fragment 自己的 ViewModel

3. ViewModel 的使用场景

3.1 数据保存与配置更改

ViewModel 最基本的用途是在配置更改期间保留数据:

class ProductListViewModel : ViewModel() {
    private val _products = MutableLiveData<List<Product>>()
    val products: LiveData<List<Product>> get() = _products
    
    fun loadProducts() {
        // 模拟网络请求
        viewModelScope.launch {
            val result = repository.getProducts()
            _products.value = result
        }
    }
}

// 即使屏幕旋转,也不会重新加载数据
class ProductListActivity : AppCompatActivity() {
    private val viewModel by viewModels<ProductListViewModel>()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 只在首次创建时加载数据
        if (savedInstanceState == null) {
            viewModel.loadProducts()
        }
        
        viewModel.products.observe(this) { products ->
            // 更新 UI
        }
    }
}

3.2 Fragment 间通信

ViewModel 是在 Fragment 之间共享数据的理想方式:

// 共享的 ViewModel
class SharedViewModel : ViewModel() {
    private val _selectedItemId = MutableLiveData<Long>()
    val selectedItemId: LiveData<Long> get() = _selectedItemId
    
    fun selectItem(itemId: Long) {
        _selectedItemId.value = itemId
    }
}

// 第一个 Fragment
class ListFragment : Fragment() {
    private val sharedViewModel by activityViewModels<SharedViewModel>()
    
    fun onItemClick(itemId: Long) {
        sharedViewModel.selectItem(itemId)
        // 导航到详情 Fragment
    }
}

// 第二个 Fragment
class DetailFragment : Fragment() {
    private val sharedViewModel by activityViewModels<SharedViewModel>()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        sharedViewModel.selectedItemId.observe(viewLifecycleOwner) { itemId ->
            loadItemDetails(itemId)
        }
    }
}

3.3 长时间运行的操作

ViewModel 可以安全地处理长时间运行的操作,如网络请求或数据库查询:

class TaskViewModel(private val repository: TaskRepository) : ViewModel() {
    private val _taskResult = MutableLiveData<Result<Task>>()
    val taskResult: LiveData<Result<Task>> get() = _taskResult
    
    fun executeLongRunningTask(taskId: String) {
        viewModelScope.launch {
            _taskResult.value = Result.Loading
            try {
                val result = repository.executeTask(taskId)
                _taskResult.value = Result.Success(result)
            } catch (e: Exception) {
                _taskResult.value = Result.Error(e)
            }
        }
    }
}

4. ViewModel 的生命周期控制

4.1 ViewModel 的生命周期图解

ViewModel 的生命周期比创建它的 Activity/Fragment 更长。具体来说:

  • Activity 场景:从首次调用 ViewModelProvider 开始,直到 Activity 被销毁(非配置更改导致的销毁)
  • Fragment 场景:从首次调用 ViewModelProvider 开始,直到 Fragment 被移除
  • 配置更改:ViewModel 实例在屏幕旋转、语言切换等配置更改期间保持不变

4.2 onCleared() 方法

当 ViewModel 即将被销毁时,会调用 onCleared() 方法,可以在这里执行清理操作:

class MyViewModel : ViewModel() {
    private val disposable = CompositeDisposable()
    
    fun startObserving() {
        // 添加订阅
        disposable.add(/* 某些 RxJava 订阅 */)
    }
    
    override fun onCleared() {
        super.onCleared()
        // 清理资源
        disposable.clear()
    }
}

4.3 使用 viewModelScope

ViewModel 提供了内置的协程作用域 viewModelScope,它会在 ViewModel 被销毁时自动取消所有协程:

class MyViewModel : ViewModel() {
    fun fetchData() {
        viewModelScope.launch {
            // 执行协程操作
            // 当 ViewModel 销毁时,此协程会自动取消
        }
    }
}

5. 在 Compose 中使用 ViewModel

5.1 基本用法

在 Jetpack Compose 中使用 ViewModel 非常简单:

@Composable
fun MyComposableScreen() {
    // 使用 viewModel() 函数获取 ViewModel
    val viewModel: MyViewModel = viewModel()
    
    // 观察状态
    val uiState by viewModel.uiState.collectAsState()
    
    // UI 渲染
    when (uiState) {
        is UiState.Loading -> LoadingScreen()
        is UiState.Success -> SuccessScreen(uiState.data)
        is UiState.Error -> ErrorScreen(uiState.exception)
    }
}

// ViewModel 定义
class MyViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()
    
    init {
        loadData()
    }
    
    private fun loadData() {
        viewModelScope.launch {
            try {
                val data = repository.getData()
                _uiState.update { UiState.Success(data) }
            } catch (e: Exception) {
                _uiState.update { UiState.Error(e) }
            }
        }
    }
}

5.2 在 Compose 中共享 ViewModel

在多个 Composable 之间共享同一个 ViewModel 实例:

// 父级 Composable
@Composable
fun ParentScreen() {
    // 在这里获取 ViewModel
    val sharedViewModel: SharedViewModel = viewModel()
    
    Column {
        // 子 Composable 可以接收同一个 ViewModel 实例
        ChildOneScreen(viewModel = sharedViewModel)
        ChildTwoScreen(viewModel = sharedViewModel)
    }
}

@Composable
fun ChildOneScreen(viewModel: SharedViewModel) {
    // 使用传入的 ViewModel
}

@Composable
fun ChildTwoScreen(viewModel: SharedViewModel) {
    // 使用传入的 ViewModel
}

5.3 Compose 中的状态管理

推荐在 ViewModel 中使用 StateFlowFlow,并在 Composable 中使用 collectAsState() 进行观察:

class ComposeViewModel : ViewModel() {
    // 使用 StateFlow 而不是 LiveData
    private val _counter = MutableStateFlow(0)
    val counter: StateFlow<Int> = _counter
    
    fun increment() {
        _counter.update { it + 1 }
    }
}

@Composable
fun CounterScreen() {
    val viewModel: ComposeViewModel = viewModel()
    
    // 使用 collectAsState() 观察 StateFlow
    val count by viewModel.counter.collectAsState()
    
    Button(onClick = { viewModel.increment() }) {
        Text(text = "Count: $count")
    }
}

6. 在单 Activity 应用中使用 ViewModel

6.1 多 Fragment 共享 ViewModel

在单 Activity 多 Fragment 架构中,ViewModel 是实现组件间通信的理想选择:

// MainActivity 作为容器
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // 初始化导航组件
        val navController = findNavController(R.id.nav_host_fragment)
        setupActionBarWithNavController(navController)
    }
}

// 共享的应用级 ViewModel
class AppViewModel : ViewModel() {
    private val _user = MutableLiveData<User?>(null)
    val user: LiveData<User?> = _user
    
    fun login(username: String, password: String) {
        viewModelScope.launch {
            try {
                val userData = repository.login(username, password)
                _user.value = userData
            } catch (e: Exception) {
                // 处理错误
            }
        }
    }
}

// 各 Fragment 可以访问共享的 ViewModel
class ProfileFragment : Fragment() {
    private val appViewModel by activityViewModels<AppViewModel>()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        appViewModel.user.observe(viewLifecycleOwner) { user ->
            user?.let { updateUI(it) }
        }
    }
}

6.2 使用 Navigation 组件集成

结合 Navigation 组件,ViewModel 可以在导航图中更有效地共享:

// 在 build.gradle 中添加依赖
implementation "androidx.navigation:navigation-fragment-ktx:2.5.3"
implementation "androidx.navigation:navigation-ui-ktx:2.5.3"

// 在 Fragment 中使用
class DetailFragment : Fragment() {
    // 方式1:获取自己的 ViewModel
    private val detailViewModel by viewModels<DetailViewModel>()
    
    // 方式2:获取共享的 Activity ViewModel
    private val sharedViewModel by activityViewModels<SharedViewModel>()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 从导航参数获取数据
        val args: DetailFragmentArgs by navArgs()
        val itemId = args.itemId
        
        detailViewModel.loadItemDetails(itemId)
    }
}

6.3 分层 ViewModel 架构

在复杂应用中,可以采用分层 ViewModel 架构:

// 应用级 ViewModel(保存用户会话等全局状态)
class AppViewModel(application: Application) : AndroidViewModel(application) {
    // 全局状态管理
}

// 功能模块级 ViewModel
class UserModuleViewModel : ViewModel() {
    // 用户相关功能的状态管理
}

// 页面级 ViewModel
class ProfileViewModel(private val userRepository: UserRepository) : ViewModel() {
    // 个人资料页面的状态管理
}

7. ViewModel 与其他组件的集成

7.1 与 LiveData 集成

ViewModel 通常与 LiveData 结合使用:

class UserViewModel(private val repository: UserRepository) : ViewModel() {
    private val _user = MutableLiveData<User?>(null)
    val user: LiveData<User?> get() = _user
    
    private val _isLoading = MutableLiveData(false)
    val isLoading: LiveData<Boolean> get() = _isLoading
    
    fun fetchUserData(userId: String) {
        _isLoading.value = true
        viewModelScope.launch {
            try {
                val data = repository.getUserById(userId)
                _user.value = data
            } finally {
                _isLoading.value = false
            }
        }
    }
}

7.2 与 Room 数据库集成

ViewModel 与 Room 数据库结合使用:

@Dao
interface UserDao {
    @Query("SELECT * FROM users")
    fun getAllUsers(): LiveData<List<User>>
}

class UserRepository(private val userDao: UserDao) {
    fun getAllUsers() = userDao.getAllUsers()
}

class UserListViewModel(private val repository: UserRepository) : ViewModel() {
    // Room 返回的 LiveData 会在后台线程执行查询
    val users = repository.getAllUsers()
}

7.3 与 DataBinding 集成

ViewModel 可以与 DataBinding 无缝集成:

// 在 ViewModel 中提供命令
class LoginViewModel : ViewModel() {
    val username = MutableLiveData("")
    val password = MutableLiveData("")
    val loginCommand = object : ViewModelCommand {
        override fun execute() {
            // 执行登录逻辑
        }
    }
}

// 在布局文件中绑定
<layout>
    <data>
        <variable
            name="viewModel"
            type="com.example.LoginViewModel" />
    </data>
    
    <EditText
        android:text="@{viewModel.username}"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    
    <Button
        android:onClick="@{() -> viewModel.loginCommand.execute()}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</layout>

// 在 Activity 中设置绑定
class LoginActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: ActivityLoginBinding = DataBindingUtil.setContentView(this, R.layout.activity_login)
        val viewModel = ViewModelProvider(this)[LoginViewModel::class.java]
        binding.viewModel = viewModel
        binding.lifecycleOwner = this
    }
}

8. ViewModel 最佳实践

8.1 关注点分离

保持 ViewModel 专注于 UI 状态管理:

// 好的做法:ViewModel 只负责 UI 状态管理
class ProductViewModel(private val productUseCase: GetProductsUseCase) : ViewModel() {
    private val _state = MutableStateFlow<ProductState>(ProductState.Loading)
    val state: StateFlow<ProductState> = _state
    
    fun loadProducts() {
        viewModelScope.launch {
            try {
                val products = productUseCase.execute()
                _state.update { ProductState.Success(products) }
            } catch (e: Exception) {
                _state.update { ProductState.Error(e.message) }
            }
        }
    }
}

// 业务逻辑被封装在 UseCase 中
class GetProductsUseCase(private val repository: ProductRepository) {
    suspend fun execute(): List<Product> {
        // 业务逻辑
        return repository.getProducts()
    }
}

8.2 使用 UiState 模式

定义清晰的 UI 状态类:

sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<out T>(val data: T) : UiState<T>()
    data class Error(val message: String?) : UiState<Nothing>()
}

class MyViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState<MyData>>(UiState.Loading)
    val uiState: StateFlow<UiState<MyData>> = _uiState
}

8.3 避免内存泄漏

  • 不要在 ViewModel 中持有 Activity/Fragment 的引用
  • 不使用 Context(除非使用 AndroidViewModel)
  • 使用 viewModelScope 管理协程
  • 在 onCleared() 中释放资源
// 错误示例
class BadViewModel(private val context: Context) : ViewModel() {
    // 持有 Context 引用可能导致内存泄漏
}

// 正确示例
class GoodViewModel(application: Application) : AndroidViewModel(application) {
    // 可以安全地使用 application Context
    private val appContext: Context = application
}

8.4 测试 ViewModel

ViewModel 设计为易于测试:

@RunWith(JUnit4::class)
class MyViewModelTest {
    private lateinit var viewModel: MyViewModel
    private lateinit var repository: MockRepository
    
    @Before
    fun setup() {
        repository = MockRepository()
        viewModel = MyViewModel(repository)
    }
    
    @Test
    fun `loading products updates state correctly`() = runBlockingTest {
        // 准备数据
        val testProducts = listOf(Product(1, "Test"))
        repository.setProducts(testProducts)
        
        // 执行操作
        viewModel.loadProducts()
        
        // 验证结果
        assertThat(viewModel.uiState.value).isInstanceOf(UiState.Success::class.java)
        val successState = viewModel.uiState.value as UiState.Success
        assertThat(successState.data).isEqualTo(testProducts)
    }
}

9. 总结

ViewModel 是 Android Jetpack 中一个强大的组件,它通过以下方式简化了 Android 应用开发:

  • 配置更改生存:在屏幕旋转等配置更改期间保存数据
  • 清晰的架构:促进关注点分离,提高代码可维护性
  • 生命周期感知:自动处理组件生命周期,避免内存泄漏
  • 组件通信:简化 Fragment 之间以及不同 UI 组件之间的通信

通过遵循本文介绍的最佳实践和模式,您可以充分利用 ViewModel 构建稳健、可维护的 Android 应用。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容