Plaid 开源库学习

Plaid 库是 google 之前的一个 demo 库,近期利用 kotlin 进行了重写.

某种程度上,是 Kotlin 和 Jetpack 的一个实践。

github 地址 :https://github.com/android/plaid
https://github.com/android/plaid
https://mp.weixin.qq.com/s/9FyPM-VjgMwZErmEtbj2uQ

以下内容从三个方面来说:

  1. Plaid 项目划分
  2. Plaid 的代码结构
  3. Plaid 的代码实现 - coroutines 协程实现

1. Plaid 项目划分

Plaid 模块化结构图:

plaid 代码结构模块化图

属于多模块化的设计, core 是继承模块,其他模块是业务模块。

2. Plaid 每个模块代码设计结构:

官方的设计类图如下:

plaid 设计类图

图片来自:https://mp.weixin.qq.com/s/9FyPM-VjgMwZErmEtbj2uQ

分为三层:

  1. UI 层
  2. Domain 层
  3. Data 层

可简单看作 MVP 的一种延伸。

2.1 Data 层 -> model 层

根据数据来源分为两部分,本地数据LocalDataSource 和 网络接口数据 RemoteDataSource.

其他层次不关系 data 数据内部数据是来自哪,所以,在 data 层里面有个 Repository 类,外部只需要去 Repository 获取数据和存储数据,而不关心数据来自哪。

比如代码中的:UserRepository 和 UserRemoteDataSource.

Repository 中可以实现一部分的数据缓存,避免不必要的流量浪费和用户体验。

2. 2Domain 层

presenter 层

在这里使用了 UseCase 这个概念。
实际上是把一些小型的轻量级并且可以复用的逻辑单独放入一个类「UseCase」里面,
这些类将基于实际的业务逻辑开处理数据。
比如说回复评论,获取回答等单独的任务。
例如:获取回答列表,有太多地方在使用这个接口去获取, 查找问题时也不是很方便,如果统一,确实会有些帮助
例如:PostReplyUseCase

个人理解:弱化了 ViewModel 的作用,把一些在 ViewModel 里面处理的逻辑划分给了 UseCase。
现在 ViewModel 只负责拿到数据后的 UI 逻辑处理.
这也是为什么在上面官方给出的图中,把 ViewModel 划分在 UI 层的一个原因。

2.3 UI 层

在这个设计中,包含了 View 层「Activity, fragment, xml」和 Presenter 逻辑层「ViewModel 被弱化了」。

在这一层中,ViewModel主要是为了 UI 提供数据并根据「用户操作触发不同的逻辑执行」, 依赖着 UseCase 去获取数据,然后把数据通过 LiveData 的形式输出给 Activity 「View 层」。

LiveData 是 ViewModel 对外部输出的唯一数据。

2.4 总结以下代码结构上的逻辑

由上面的可得到, 代码执行的逻辑是:


Activity->ViewModel: 执行某个逻辑
ViewModel->XXXUseCase: 执行某个复杂逻辑
XXXUseCase->XXXRepository: 去 data 中拿取数据
XXXRepository->XXXDataSource: 真正拿数据的地方
XXXRepository-->XXXUseCase: 在 UseCase 中处理一下
XXXUseCase-->ViewModel: 返回数据给 viewModel
ViewModel-->Activity: liveData 反馈给 Activity
代码流程图

3. 从代码层面看一看

想要分享这个库的原因之一,它使用了 kotlin 和 Jetpack 实现。

kotlin ,当然这里使用 coroutine 实现。
Jetpack ,使用了 LiveData, Room , Data Binding

使用前提:引入协程库。

代码

  1. 首先在 View 层的 Activity或者 Fragment 中获取到 ViewModel;
  2. 手动调用 ViewModel.getXXX() 去获取数据
  3. 对一些需要的数据利用 LiveData 观察变化,而获取数据和做 UI 改变

下面看一些具体的代码实现:

3.1 获取到 ViewModel

在 Plaid 中使用的是 Dragger 实现注入的。

代码大致如下:

Provides
fun provideLoginViewModel(
    factory: DesignerNewsViewModelFactory
): LoginViewModel =
    ViewModelProviders.of(activity, factory).get(LoginViewModel::class.java)

上述代码省去了 Inject 的注入过程。

嗯……因为个人原因,不太喜欢使用 Dragger.

在Activity 中观察 liveData 代码:

// 在 activity 中的 observer
viewModel.uiState.observe(this, Observer {
    val uiModel = it ?: return@Observer
    // balabala 的 UI 上的操作
    ....
})
3.2 ViewModel 发起数据请求

代码示例如下:

// 在 ViewModel 代码中
private fun getComments() = viewModelScope.launch(dispatcherProvider.computation) {
    val result = getCommentsWithRepliesAndUsers(story.links.comments)
    if (result is Result.Success) {
        // 切换到主线程
         withContext(dispatcherProvider.main) { 
             //通过 liveData 抛给 Activity 的 observer
             emitUiModel(result.data) 
         }
    }
}

代码中 viewModelScope 来自 liftcycle-viewmodel-ktx-2.2.0 ,是 ViewModel 的一个扩展属性,源码如下:

/**
 * [CoroutineScope] tied to this [ViewModel].
 * This scope will be canceled when ViewModel will be cleared, it.e [ViewModel.onCleared] is called
 *
 * This scope is bound to [Dispatchers.Main]
 */
val ViewModel.viewModelScope: CoroutineScope
        get() {
            val scope: CoroutineScope? = this.getTag(JOB_KEY)
            if (scope != null) {
                return scope
            }
            return setTagIfAbsent(JOB_KEY,
                CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main))
        }

返回的是一个 CloseableCoroutineScope.

同时,这里会有一个 setTagIfAbsent(xxx) 在 mBagOfTags 这里存储了 CloseableCoroutineScope 的实例 ,会在 ViewModel 被销毁时回收掉。
参考 viewModelScope 的销毁

3.3 UseCase 调度接口

上述代码中的 getCommentsWithRepliesAndUsers 其实是 GetCommentsWithRepliesAndUsersUseCase 的一个实例, 最终,在这里调用的方法为:

// get the users
val usersResult = userRepository.getUsers(userIds)

调用路径为:


代码2
3.4 Repository 的实现

其实这一层的需要不需要,完全看开发。

在这个例子中 UserRepository 的实现,里面有一个成员变量 cachedUsers, 用做缓存,减少不必要的网络访问。一些需求是不需要这样的逻辑的,可完全抛弃掉 Repository。

class UserRepository(private val dataSource: UserRemoteDataSource) {
    private val cachedUsers = mutableMapOf<Long, User>()
    
    suspend fun getUsers(ids: Set<Long>): Result<Set<User>> {
        ...
    }

}

Repository 的作用:

  1. 做一些缓存,减少不必要的接口再次访问;
  2. 处理一下数据,精简逻辑和数据,dataSource 返回的数据,需要经过它的处理再返回给 ViewModel
  3. 数据来源为两方面 local 和 remote ,需要经过 Repository 的合并或者筛选再返回给 ViewModel
3.5 DataSource 的实现

往往我们会认为 DataSource 是来自网络的,而忽视了本地的数据,所以应该把 DataSource 分为两类,一种是 local 数据,一种是 remote 数据。

代码实现:

// safeApiCall() 是一个高阶函数,本质上是做了 try catch 操作「最小程度代码块的 try catch」
suspend fun getUsers(userIds: List<Long>) = safeApiCall(
    call = { requestGetUsers(userIds) },
    errorMessage = "Error getting user"
)
//请求数据
private suspend fun requestGetUsers(userIds: List<Long>): Result<List<User>>{
    ....
    service.getUser(userIds)
    ...
}

一定要让 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.html
viewModelScope 的销毁

https://mp.weixin.qq.com/s/d9lx8iSGRabeuuYYVz-z1Q

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容