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~