1949啦网--小小 痛苦,是因为能力和欲望不匹配造成的

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

微信扫码关注

更新实时通知

作者:xialibing 分类:编程小记 浏览: