Move stones, not mountains.
Google 官方的 Android Architecture Blueprints 推出了 v2 版本,相比于之前的 v1 版本,v2 采取了更先进的设计思想与组件:
采用 Kotlin Coroutines 处理后台操作
单一 Activity 结构,用 Navigation component 处理Fragment 之间跳转
由 Fragment(View) 和 ViewModel 组成的 Presentation 层,即 MVVM 模式
基于 LiveData 和 DataBinding 的响应式(Reactive)UI
data 层使用一个 Repository 和两个 Datasource(本地数据、远端数据),采用直观的调用方式(非回调、非 data stream)
两个 product flavor,分别是mock和prod,对应着测试与开发环境
一系列单元测试、集成测试以及端到端测试
接下来从源码角度解析 master 分支工程,看看 v2 究竟可以为我们带来什么便利。
程序入口 入口在TasksActivity.java,注意到这是一个“SPA”,即 Single Page Application,在AndroidManifest.xml中只声明了这一个 Activity。TasksActivity实际上只是一个壳页面,只处理了 Navigation、ActionBar、NavigationDrawer 等基础功能。它在onCreate里进行这些初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private lateinit var drawerLayout: DrawerLayoutprivate lateinit var appBarConfiguration: AppBarConfigurationoverride fun onCreate (savedInstanceState: Bundle ?) { super .onCreate(savedInstanceState) setContentView(R.layout.tasks_act) setupNavigationDrawer() setSupportActionBar(findViewById(R.id.toolbar)) val navController: NavController = findNavController(R.id.nav_host_fragment) appBarConfiguration = AppBarConfiguration.Builder(R.id.tasks_fragment_dest, R.id.statistics_fragment_dest) .setDrawerLayout(drawerLayout) .build() setupActionBarWithNavController(navController, appBarConfiguration) findViewById<NavigationView>(R.id.nav_view) .setupWithNavController(navController) }
该应用采用 Navigation component 管理页面跳转,跳转关系在nav_graph.xml文件中以可视化的方式呈现。我们这里不需要关心跳转组件的具体用法, 只要知道它可以启动我们要分析的TasksFragment就可以了。
数据 我习惯从数据开始分析代码走向,看一下data目录的结构:
1 2 3 4 5 6 7 8 9 10 11 12 λ tree data data |-- Result.kt |-- Task.kt `-- source |-- DefaultTasksRepository.kt |-- TasksDataSource.kt |-- TasksRepository.kt `-- local |-- TasksDao.kt |-- TasksLocalDataSource.kt `-- ToDoDatabase.kt
Result.kt 与 Task.kt Result.kt是一个数据请求的结果封装类,业务层对数据的请求均是通过 Result 对象进行封装。这个类使用到了多个 Kotlin 特性,容我在注释里一一说明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 sealed class Result <out R > { data class Success <out T > (val data : T) : Result<T>() data class Error (val exception: Exception) : Result<Nothing >() object Loading : Result<Nothing >() override fun toString () : String { return when (this ) { is Success<*> -> "Success[data=$data ]" is Error -> "Error[exception=$exception ]" Loading -> "Loading" } } } val Result<*>.succeeded get () = this is Success && data != null
Task.kt描述了任务对象,由title、description、completed和id四个字段构成,同时借助 Room 组件自动关联到名为tasks 的数据表。
数据接口:TasksDataSource.kt 与 TasksRepository.kt TasksDataSource.kt和TasksRepository.kt是两个接口类,内容十分相似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 ) } 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 ) }
可以看到两者的方法几乎是一样的:名字和数量相同,区别仅仅在于TasksRepository中的个别方法多了forceUpdate参数。不过,这两个接口在语义上是不同的。
TasksDatasource是底层数据封装,数据可能来自网络,也可能来自于文件、数据库。
TasksRepository是数据层对外的接口,业务代码通过该接口对数据进行增删改查。forceUpdate参数作用于接口实现类内部的缓存。
suspend关键字说明它们均为 Coroutines 接口。
然后我们来看下对外的接口是如何给到使用者的。TodoApplication类继承自Application,其中有一个成员变量taskRepository。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 val taskRepository: TasksRepository get () = ServiceLocator.provideTasksRepository(this ) fun provideTasksRepository (context: Context ) : TasksRepository { synchronized(this ) { return tasksRepository ?: tasksRepository ?: createTasksRepository(context) } } private fun createTasksRepository (context: Context ) : TasksRepository { return DefaultTasksRepository(FakeTasksRemoteDataSource, createTaskLocalDataSource(context)) } fun Fragment.getViewModelFactory () : ViewModelFactory { val repository = (requireContext().applicationContext as TodoApplication).taskRepository return ViewModelFactory(repository) }
local 目录:本地数据实现 local 目录下是TasksDatasource的本地实现,与此相对的,若数据来源于网络,则还应当有一个 remote 目录。
ToDoDatabase 是数据库声明
TasksDao 声明 tasks 表的 CRUD 操作
TasksLocalDataSource 是TasksDataSource的本地实现,使用Dispatchers.IO作为协程上下文,调用TasksDao完成数据操作
数据层总结 相比于曾经分析过的todo-mvp 和todo-mvp-clean ,最直观的感受是,v2在保证数据接口语义不变的前提下,借助 Coroutines 简化了原有的回调写法,用同步的方式写异步的代码。此外,像使用 Room 做 ORM、local/remote 两套数据实现等,与之前的项目并无不同。
ViewModel 背景知识:ViewModel 与 LiveData ViewModel 与 LiveData 都是 Android Jetpack 中的架构组件,它们通常组合使用,达到将数据和视图解耦的目的。
jetpack
ViewModel
避免屏幕旋转等事件发生时,保存在 Activity 中的数据被销毁并重建
异步回调时防止内存泄漏、Context 为 Null
将数据和视图解耦,防止出现 God Activities 和 God Fragments
viewmodel
LiveData
DataBinding 思想的一种实现,数据/视图双向绑定
与生命周期关联,页面销毁后自动将其从订阅者列表去除
ViewModelFactory ViewModel 的创建采用工厂模式进行统一管理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Suppress("UNCHECKED_CAST" ) class ViewModelFactory constructor ( private val tasksRepository: TasksRepository ) : ViewModelProvider.NewInstanceFactory() { override fun <T : ViewModel> create (modelClass: Class <T >) = with(modelClass) { when { isAssignableFrom(StatisticsViewModel::class .java ) -> StatisticsViewModel(tasksRepository) isAssignableFrom(TaskDetailViewModel::class .java ) -> TaskDetailViewModel(tasksRepository) isAssignableFrom(AddEditTaskViewModel::class .java ) -> AddEditTaskViewModel(tasksRepository) isAssignableFrom(TasksViewModel::class .java ) -> TasksViewModel(tasksRepository) else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name} " ) } } as T }
还记得上文提到过为 Fragment 增加的扩展函数吗?在每一个页面(Fragment)里通过这个扩展函数获取到工厂类,进而获得对应 ViewModel 类的实例。
1 2 3 4 5 6 7 8 9 10 11 fun Fragment.getViewModelFactory () : ViewModelFactory { val repository = (requireContext().applicationContext as TodoApplication).taskRepository return ViewModelFactory(repository) } class TasksFragment : Fragment () { private val viewModel by viewModels<TasksViewModel> { getViewModelFactory() } }
接下来是 ViewModel 类,它承担了与 Presenter 类似的职责,是处理业务逻辑的地方。如果需要的话,可以增加一个 Domain 层,负责提取出来的业务逻辑(Use cases),提供复用,这样就变成了 MVVM-Clean 模式。在 master 分支上还没有 domain 层。
TasksViewModel ViewModel 接受一个 TasksRepository 参数,用作数据层接口。(在clean架构里,这里传入的不是Repository,而是UseCases)。
1 2 3 4 5 class TasksViewModel ( private val tasksRepository: TasksRepository ) : ViewModel() { }
随后声明了一系列变量作为页面数据&状态,这里采用“一个对象,两个变量”的成对写法,略显繁琐,不知道有没有更优美的处理方法。这样做的目的是把对变量的修改关闭,对外(即LiveData)仅提供读取变量的接口,只可以在 ViewModel 内部修改变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 private val _items = MutableLiveData<List<Task>>().apply { value = emptyList() } val items: LiveData<List<Task>> = _itemsprivate val _dataLoading = MutableLiveData<Boolean >()val dataLoading: LiveData<Boolean > = _dataLoadingprivate val _currentFilteringLabel = MutableLiveData<Int >()val currentFilteringLabel: LiveData<Int > = _currentFilteringLabelprivate val _noTasksLabel = MutableLiveData<Int >()val noTasksLabel: LiveData<Int > = _noTasksLabelprivate val _noTaskIconRes = MutableLiveData<Int >()val noTaskIconRes: LiveData<Int > = _noTaskIconResprivate val _tasksAddViewVisible = MutableLiveData<Boolean >()val tasksAddViewVisible: LiveData<Boolean > = _tasksAddViewVisibleprivate val _snackbarText = MutableLiveData<Event<Int >>()val snackbarText: LiveData<Event<Int >> = _snackbarTextprivate var _currentFiltering = TasksFilterType.ALL_TASKS private val _openTaskEvent = MutableLiveData<Event<String>>()val openTaskEvent: LiveData<Event<String>> = _openTaskEventprivate val _newTaskEvent = MutableLiveData<Event<Unit >>()val newTaskEvent: LiveData<Event<Unit >> = _newTaskEventval empty: LiveData<Boolean > = Transformations.map(_items) { it.isEmpty() }
可以说管理上面这些数据是 ViewModel 最主要的职责了,从功能上区分,这些数据可以分成3种 。
业务实体如 items(任务对象列表),ViewModel 通过修改这类对象,借助于 DataBinding 更新 UI
数据状态对象如 dataLoading(是否正在加载数据)、currentFilteringLabel(当前的过滤器文字)、noTasksLabel(没有任务的文字)、snackbarText(提示栏文字),这一类对象不进行持久化存储,但是也会影响到 UI 显示
事件包装对象如 openTaskEvent(打开某个任务,在点击列表中的任务时触发)、newTaskEvent(创建一个任务,在点击+时触发),它们负责通知页面进行跳转——这部分设计得不佳,为了 DataBinding 而强行 DataBinding
总之贯彻的思想是:一切变动都是数据变动,数据变动通过 DataBinding 自动投射到 UI。
视图 视图也就是TasksFragment.kt类,负责初始化布局,触发 ViewModel 进行首次加载。这里的 Fragment 继承自 androidx 中的 Fragment。
在onCreateView里初始化 DataBinding(数据)。
1 2 3 4 5 6 7 8 9 10 override fun onCreateView ( inflater: LayoutInflater , container: ViewGroup ?, savedInstanceState: Bundle ? ) : View? { viewDataBinding = TasksFragBinding.inflate(inflater, container, false ).apply { viewmodel = viewModel } setHasOptionsMenu(true ) return viewDataBinding.root }
在onActivityCreated里初始化 UI(视图),初始化完成后启动加载(代码最后一行)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 override fun onActivityCreated (savedInstanceState: Bundle ?) { super .onActivityCreated(savedInstanceState) viewDataBinding.lifecycleOwner = this .viewLifecycleOwner setupSnackbar() setupListAdapter() setupRefreshLayout(viewDataBinding.refreshLayout, viewDataBinding.tasksList) setupNavigation() setupFab() viewModel.loadTasks(true ) }
此外,Fragment 中还会进行设置 OnClickListener、Adapter 等操作,比较简单,不赘述。
总结 v2 的项目设计时采取了 MVVM 思想,旨在解决 MVP 模式下Presenter 层过于庞大的问题。其实 MVP-Clean 模式已经对此有一些缓解。而 MVVM 做的更彻底,干脆把数据对 UI 的控制完全交给框架自动进行。这样做的好处显而易见,但也并不是没有缺点,比如一旦出了问题,如果没有掌握个中原理,调试时必定摸不清头绪。总而言之,这是一个值得学习与尝试的架构设计。
Bonus:usecases usecases 是 v2 中的一个 Stable 分支(另一个是dagger-android )。usecases 以解耦、抽象、单向依赖为核心设计理念,这也是 Clean 架构的核心思想。
表现层只能访问到领域层/用例层,不知道数据层的存在
领域层/用例层只能访问到数据层,无法访问表现层
数据层无法访问表现层和领域层
领域层(或者叫用例层),即 Domain Layer,是由多个 UseCase 组成的,每一个 UseCase 对应一个业务逻辑。以“加载单个Task”为例,可以看到 UseCase 里直接将请求转发给 TasksRepository 来处理,逻辑十分简单。
在分析具体差别之前,可以先看一遍 usecase 分支与 master 分支的 diff:https://github.com/googlesamples/android-architecture/compare/usecases#files_bucket
GetTaskUseCase.kt
1 2 3 4 5 6 7 8 9 class GetTaskUseCase private val tasksRepository: TasksRepository ) { suspend operator fun invoke (taskId: String , forceUpdate: Boolean = false ) : Result<Task> { wrapEspressoIdlingResource { return tasksRepository.getTask(taskId, forceUpdate) } } }
而获取 Tasks 列表的 Usecase 相对复杂一些,包括了原本在 TasksViewModel 中处理的过滤逻辑,从另一个角度看,这相当于减轻了 ViewModel 的负担。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class GetTasksUseCase ( private val tasksRepository: TasksRepository ) { suspend operator fun invoke ( forceUpdate: Boolean = false , currentFiltering: TasksFilterType = ALL_TASKS ) : Result<List<Task>> { wrapEspressoIdlingResource { val tasksResult = tasksRepository.getTasks(forceUpdate) if (tasksResult is Success && currentFiltering != ALL_TASKS) { val tasks = tasksResult.data val tasksToShow = mutableListOf<Task>() for (task in tasks) { when (currentFiltering) { ACTIVE_TASKS -> if (task.isActive) { tasksToShow.add(task) } COMPLETED_TASKS -> if (task.isCompleted) { tasksToShow.add(task) } else -> NotImplementedError() } } return Success(tasksToShow) } return tasksResult } } }
至于 ViewModel 里,将原来的 Repository 参数改为当前 ViewModel 用到的 UseCase 参数即可,所有处理数据的请求都有 UseCase 来接管。
TasksViewModel.kt
1 2 3 4 5 6 7 8 class TasksViewModel ( private val getTasksUseCase: GetTasksUseCase, private val clearCompletedTasksUseCase: ClearCompletedTasksUseCase, private val completeTaskUseCase: CompleteTaskUseCase, private val activateTaskUseCase: ActivateTaskUseCase ) : ViewModel() { }
Over~