Plaid 开源库学习

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

某种程度上,是 KotlinJetpack 的一个实践。

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 获取数据和存储数据,而不关心数据来自哪。

比如代码中的:UserRepositoryUserRemoteDataSource.

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 的形式输出给 ActivityView 层」。

LiveDataViewModel 对外部输出的唯一数据。

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

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


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

3. 从代码层面看一看

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

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. 数据来源为两方面 localremote ,需要经过 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-coreorg.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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容