Android MVVM 入门与实践教程
在经历了 android 项目 MVC 架构的万能 Activity 维护的困扰和 MVP 架构的令人头大的复杂接口之后,我打算尝试 MVVM,一开始是通过阅读 android 官方的 应用架构指南 入门,看完之后认为 MVVM 或许是个不错的解决方案。(如果没有阅读过官方的应用架构指南的话,强烈建议阅读一遍,官方文档写得很好也很透彻,看完之后会对 MVVM 架构会有个大致的认识。)
入门
大多数谈到架构的博客都会用登录页面举例,但是,实际的开发过程中怎么可能是这么简单的项目,这未免不现实,如果你真能通过一个登录页面的实例就能清晰地理解这个架构,那我觉得你可能不看那些博客也能理解。我是通过 android 官方的 architecture-samples 来学习的。接下来我会结合着这个项目简单谈谈我对 MVVM 的认知。
一般来说,应用的开发从数据开始,数据的来源有很多,有本地数据库的缓存,也有云端的真实数据,或者开发环境的测试数据。这些都是我们的数据源 (Data Source),为了方便我们测试和变更数据源,我们用数据仓库来管理数据源 (Data Repository),有了数据仓库,我们还需要一个桥梁来让界面 (Activity/Fragment) 获取仓库数据,这个桥梁就是 ViewModel。在 MVVM 中,数据是中心,界面围绕数据去变动,落到实现层面,也就是 LiveData,官方称其为「可观察的数据存储器」,应用中的其他组件通过它来监控对象的变更。这样,ViewModel 中持有 LiveData,界面监听这些 LiveData 的变化来动态响应,这样就形成了 MVVM 的核心思想,就像官方文档中给出的这幅图:
例如,在官方给出的 TODO 应用的实例中,数据部分的代码:
// Data Source interface TasksDataSource { suspend fun getTasks(): Result<List<Task>> suspend fun getTask(taskId: String): Result<Task> suspend fun saveTask(task: Task) suspend fun completeTask(task: Task) suspend fun completeTask(taskId: String) suspend fun activateTask(task: Task) suspend fun activateTask(taskId: String) suspend fun clearCompletedTasks() suspend fun deleteAllTasks() suspend fun deleteTask(taskId: String) } // Data Repository interface TasksRepository { suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task> suspend fun saveTask(task: Task) suspend fun completeTask(task: Task) suspend fun completeTask(taskId: String) suspend fun activateTask(task: Task) suspend fun activateTask(taskId: String) suspend fun clearCompletedTasks() suspend fun deleteAllTasks() suspend fun deleteTask(taskId: String) } // Data Repository 实现 class DefaultTasksRepository @Inject constructor( @TasksRemoteDataSource private val tasksRemoteDataSource: TasksDataSource, @TasksLocalDataSource private val tasksLocalDataSource: TasksDataSource, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO ) : TasksRepository { private var cachedTasks: ConcurrentMap<String, Task>? = null override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> { wrapEspressoIdlingResource { return withContext(ioDispatcher) { // Respond immediately with cache if available and not dirty if (!forceUpdate) { cachedTasks?.let { cachedTasks -> return@withContext Success(cachedTasks.values.sortedBy { it.id }) } } val newTasks = fetchTasksFromRemoteOrLocal(forceUpdate) // Refresh the cache with the new tasks (newTasks as? Success)?.let { refreshCache(it.data) } cachedTasks?.values?.let { tasks -> return@withContext Success(tasks.sortedBy { it.id }) } (newTasks as? Success)?.let { if (it.data.isEmpty()) { return@withContext Success(it.data) } } return@withContext Error(Exception("Illegal state")) } } } // 篇幅原因,省略以下代码 }
在这里,数据仓库对数据做了缓存,用以解决数据的临时保存的问题,接下来看看 ViewModel 和界面部分:
// View Model class TasksViewModel @Inject constructor( private val tasksRepository: TasksRepository ) : ViewModel() { private val _items = MutableLiveData<List<Task>>().apply { value = emptyList() } val items: LiveData<List<Task>> = _items private val _currentFilteringLabel = MutableLiveData<Int>() val currentFilteringLabel: LiveData<Int> = _currentFilteringLabel // 省略部分数据定义 private var _currentFiltering = TasksFilterType.ALL_TASKS private val _openTaskEvent = MutableLiveData<Event<String>>() val openTaskEvent: LiveData<Event<String>> = _openTaskEvent private val _newTaskEvent = MutableLiveData<Event<Unit>>() val newTaskEvent: LiveData<Event<Unit>> = _newTaskEvent // This LiveData depends on another so we can use a transformation. val empty: LiveData<Boolean> = Transformations.map(_items) { it.isEmpty() } init { // Set initial state setFiltering(TasksFilterType.ALL_TASKS) loadTasks(true) } fun loadTasks(forceUpdate: Boolean) { // ... } // 限于篇幅原因,省略以下代码 } // 鉴于使用了 databinding,layout 更具参考价值,这里受限于篇幅只粘贴关键部分 <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <import type="android.view.View" /> <import type="androidx.core.content.ContextCompat" /> <variable name="viewmodel" type="com.example.android.architecture.blueprints.todoapp.tasks.TasksViewModel" /> </data> <TextView android:id="@+id/filteringLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@{context.getString(viewmodel.currentFilteringLabel)}"/> <androidx.recyclerview.widget.RecyclerView android:id="@+id/tasks_list" android:layout_width="match_parent" android:layout_height="wrap_content" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:items="@{viewmodel.items}" /> </layout>
这里通过 LiveData 将数据与界面绑定在一起,filter 在不同类型下的说明都是通过 currentFilteringLabel 的变动来实现改变,列表也同 items 数据绑定。可以看到,在使用 MVVM 架构后,代码十分简洁,少了很多和逻辑无关的 View 设置代码,阅读起来轻松明了,代码只专注于逻辑。
实践
在看完了官方的实例之后,我决定也按照 MVVM 架构来开发一个应用,这个项目目前已经开发完成,并且在 GitHub 上开源了,项目地址:Watt,开源的安卓组件禁用工具 (欢迎 Star :P)。接下来我聊聊在实际开发过程中遇到的问题。
DataSouce 的 Context 问题:
这个问题其实没什么好说的,依赖注入就可以解决,推荐使用 Dagger2。
RecyclerView 更新某特定项问题:
这是个很棘手的问题,因为 MVVM 一般是通过 LiveData<List<Bean>>
来设置数据,当数据变更时直接调用 ListAdapter.submitList(List)
,这意味着即使是一个小变动也需要提交一整个列表,但是其实很好解决,将 Bean 中的变动项换用 ObservableField
,例如将 Boolean
替换为 ObservableField<Boolean>
,这样,当数据变动时,列表会自动更新。
String 资源使用问题:
例如使用 SnackBar 显示一个需要格式化的 string,官方实例中使用 LiveData<Event<Int>>
来在 ViewModel 中使用 SnackBar 展示相关提示,这在提示只是一个简单的说明时 (例如:操作完成) 可行,但是在例如「添加了 5 个订单」这样的提示就没法操作了,我目前的解决办法是 ViewModel 返回值给 Fragment,然后在 Fragment 中展示提示,这个做法并不优雅。
关于 RecyclerView 多选:
官方的 recyclerview-selection 真的是很难用,感觉侵入性很强,还不如直接封装 ActionMode 好用。
总结
目前就我的使用感受来看,MVVM 确实算是当下最优雅的架构,设计合理,各部分职责明确,边界清晰,代码的可维护性也很高。十分推荐
版权声明:本文为期权记的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://www.qiquanji.com/post/4631.html
微信扫码关注
更新实时通知