Plaid
库是 google
之前的一个 demo
库,近期利用 kotlin
进行了重写.
某种程度上,是 Kotlin
和 Jetpack
的一个实践。
github
地址 :https://github.com/android/plaid
官方公众号地址:https://mp.weixin.qq.com/s/9FyPM-VjgMwZErmEtbj2uQ
封面图片来自:https://mp.weixin.qq.com/s/9FyPM-VjgMwZErmEtbj2uQ
通过公众号地址可大致了解到这个项目是做什么的,虽然现在还存在 crash 的错误,不影响阅读代码。
以下内容从三个方面来说:
Plaid
项目划分
Plaid
的代码结构
Plaid
的代码实现 - coroutines
协程实现
总结
Plaid
模块化结构图:
属于多模块化的设计, core
是基础核心模块,其他模块是业务模块。
官方的设计类图如下:
图片来自:https://mp.weixin.qq.com/s/9FyPM-VjgMwZErmEtbj2uQ
分为三层:
UI
层
Domain
层
Data
层
可简单看作 MVP
的一种延伸。
根据数据来源分为两部分,本地数据LocalDataSource
和 网络接口数据 RemoteDataSource
.
其他层次不关系 data
数据内部数据是来自哪,所以,在 data
层里面有个 Repository
类,外部只需要去 Repository
获取数据和存储数据,而不关心数据来自哪。
比如代码中的:UserRepository
和 UserRemoteDataSource
.
Repository
中可以实现一部分的数据缓存,避免不必要的流量浪费和用户体验。
presenter
层
在这里使用了 UseCase
这个概念。
实际上是把一些小型的轻量级并且可以复用的逻辑单独放入一个类「UseCase
」里面,
这些类将基于实际的业务逻辑开处理数据。
比如说回复评论,获取回答等单独的任务。
例如:获取回答列表,有太多地方在使用这个接口去获取, 查找问题时也不是很方便,如果统一,确实会有些帮助
例如:PostReplyUseCase
个人理解:弱化了
ViewModel
的作用,把一些在ViewModel
里面处理的逻辑划分给了UseCase
。
现在ViewModel
只负责拿到数据后的UI
逻辑处理.
这也是为什么在上面官方给出的图中,把ViewModel
划分在UI
层的一个原因。
在这个设计中,包含了 View
层「Activity
, fragment
, xml
」和 Presenter
逻辑层「ViewModel
被弱化了」。
在这一层中,ViewModel
主要是为了 UI
提供数据并根据「用户操作触发不同的逻辑执行」, 依赖着 UseCase
去获取数据,然后把数据通过 LiveData
的形式输出给 Activity
「View
层」。
LiveData
是ViewModel
对外部输出的唯一数据。
由上面的可得到, 代码执行的逻辑是:
1Activity->ViewModel: 执行某个逻辑
2ViewModel->XXXUseCase: 获取某个数据/执行某个复杂逻辑
3XXXUseCase->XXXRepository: 去 data 中拿取数据
4XXXRepository->XXXDataSource: 真正拿数据的地方
5XXXRepository-->XXXUseCase: 拿到数据后,在 UseCase 中处理一下
6XXXUseCase-->ViewModel: 返回数据给 viewModel
7ViewModel-->Activity: 通过 liveData 等操作反馈给 Activity
想要分享这个库的原因之一,它使用了 kotlin
和 Jetpack
实现。
kotlin
,当然这里使用 coroutine
实现。Jetpack
,使用了 LiveData
, Room
, Data Binding
使用前提:引入协程库。
代码
首先在 View
层的 Activity
或者 Fragment
中获取到 ViewModel
;
手动调用 ViewModel.getXXX()
去获取数据
对一些需要的数据利用 LiveData
观察变化,而获取数据和做 UI
改变
下面看一些具体的代码实现:
在 Plaid
中使用的是 Dragger
实现注入的。
代码大致如下:
1Provides
2fun provideLoginViewModel(
3 factory: DesignerNewsViewModelFactory
4): LoginViewModel =
5 ViewModelProviders.of(activity, factory).get(LoginViewModel::class.java)
上述代码省去了 Inject
的注入过程。
嗯……因为个人原因,不太喜欢使用 Dragger
.
在Activity
中观察 liveData
代码:
1// 在 activity 中的 observer
2viewModel.uiState.observe(this, Observer {
3 val uiModel = it ?: return@Observer
4 // balabala 的 UI 上的操作
5 ....
6})
代码示例如下:
1// 在 ViewModel 代码中
2private fun getComments() = viewModelScope.launch(dispatcherProvider.computation) {
3 val result = getCommentsWithRepliesAndUsers(story.links.comments)
4 if (result is Result.Success) {
5 // 切换到主线程
6 withContext(dispatcherProvider.main) {
7 //通过 liveData 抛给 Activity 的 observer
8 emitUiModel(result.data)
9 }
10 }
11}
代码中 viewModelScope
来自 liftcycle-viewmodel-ktx-2.2.0
,是 ViewModel
的一个扩展属性,源码如下:
1/**
2 * [CoroutineScope] tied to this [ViewModel].
3 * This scope will be canceled when ViewModel will be cleared, it.e [ViewModel.onCleared] is called
4 *
5 * This scope is bound to [Dispatchers.Main]
6 */
7val ViewModel.viewModelScope: CoroutineScope
8 get() {
9 val scope: CoroutineScope? = this.getTag(JOB_KEY)
10 if (scope != null) {
11 return scope
12 }
13 return setTagIfAbsent(JOB_KEY,
14 CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main))
15 }
返回的是一个 CloseableCoroutineScope
.
同时,这里会有一个
setTagIfAbsent(xxx)
在mBagOfTags
这里存储了CloseableCoroutineScope
的实例 ,会在ViewModel
被销毁时回收掉。
参考viewModelScope
的销毁
上述代码中的 getCommentsWithRepliesAndUsers
其实是 GetCommentsWithRepliesAndUsersUseCase
的一个实例, 最终,在这里调用的方法为:
1// get the users
2val usersResult = userRepository.getUsers(userIds)
调用路径为:
3.4 Repository 的实现
其实这一层的需要不需要,完全看开发。
在这个例子中 UserRepository
的实现,里面有一个成员变量 cachedUsers
, 用做缓存,减少不必要的网络访问。一些需求是不需要这样的逻辑的,可完全抛弃掉 Repository
。
1class UserRepository(private val dataSource: UserRemoteDataSource) {
2 private val cachedUsers = mutableMapOf<Long, User>()
3
4 suspend fun getUsers(ids: Set<Long>): Result<Set<User>> {
5 ...
6 }
7
8}
Repository
的作用:
做一些缓存,减少不必要的接口再次访问;
处理一下数据,精简逻辑和数据,dataSource
返回的数据,需要经过它的处理再返回给 ViewModel
数据来源为两方面 local
和 remote
,需要经过 Repository
的合并或者筛选再返回给 ViewModel
往往我们会认为 DataSource
是来自网络的,而忽视了本地的数据,所以应该把 DataSource
分为两类,一种是 local
数据,一种是 remote
数据。
代码实现:
1// safeApiCall() 是一个高阶函数,本质上是做了 try catch 操作「最小程度代码块的 try catch」
2suspend fun getUsers(userIds: List<Long>) = safeApiCall(
3 call = { requestGetUsers(userIds) },
4 errorMessage = "Error getting user"
5)
6//请求数据
7private suspend fun requestGetUsers(userIds: List<Long>): Result<List<User>>{
8 ....
9 service.getUser(userIds)
10 ...
11}
一定要让 DataSource
尽可能纯粹,它只负责请求数据,返回数据,而不对数据进行处理。
对于
safeApiCall()
和Result
的实现,感兴趣的可以私下看一看。
其实在这部分代码中,很多 kotlin
的小细节都值得学习,因为太过详细,这里不再介绍,真心推荐一下,源码还是不错的,虽然使用了 Dragger
,在阅读体验上并不是很好,但还是特别值得学习的一个代码。
当然上面是个人的一些浅显理解,有错误的地方还请指出。
lifecycle-viewmodel
版本号:2.2.0
lifecycle-viewmodel-ktx
版本号:2.2.0
使用 coroutines
要求
引入 org.jetbrains.kotlinx:kotlinx-coroutines-core
和 org.jetbrains.kotlinx:kotlinx-coroutines-android
引入 retrofit
- 2.6.0 以下版本,需要使用 https://github.com/JakeWharton/retrofit2-kotlin-coroutines-adapter
兼容;
- 2.6.0 以上版本,不需要兼容, 支持 suspend
参考链接:
https://juejin.im/post/5d5f80836fb9a06b2548ee47
https://www.kotlincn.net/docs/reference/coroutines/coroutines-guide.htmlviewModelScope
的销毁:https://medium.com/androiddevelopers/easy-coroutines-in-android-viewmodelscope-25bffb605471
2019.11.17 by chendroid