禅与计算机程序设计艺术技术二三Kotlin编程

Plaid 开源库学习

2019-11-17  本文已影响0人  chendroid

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 要求

参考链接:
https://juejin.im/post/5d5f80836fb9a06b2548ee47
https://www.kotlincn.net/docs/reference/coroutines/coroutines-guide.html
viewModelScope 的销毁

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

上一篇下一篇

猜你喜欢

热点阅读