阅读 2684
MVVM+Kotlin协程+JetPack(ViewModel+LiveData)+Retrofit的小DEMO

MVVM+Kotlin协程+JetPack(ViewModel+LiveData)+Retrofit的小DEMO

1、MVVM架构模式概览

这是使用MVVM架构模式+Kotlin协程+JetPack(ViewModel+LiveData)+Retrofit的架构,实现WanAndroid登录接口的小DEMO,后续会慢慢完善WanAndroid客户端,地址见: https://github.com/chaozhouzhang/WanAndroid

1、ViewModel 为了从界面控制器Activity/Fragment逻辑中分离出视图View数据所有权,架构组件为界面控制器提供了 ViewModel 辅助程序类,该类负责为界面准备数据。在配置更改期间会自动保留 ViewModel 对象,以便它们存储的数据立即可供下一个 Activity 或 Fragment 实例使用。

2、LiveData LiveData 是一种可观察的数据存储器类,具有生命周期感知能力,意指它遵循其他应用组件如 Activity、Fragment 或 Service 生命周期,可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。LiveData 对象通常存储在 ViewModel 对象中,并可通过 getter 方法进行访问。

3、Kotlin协程 协程依附在线程上,可以实现顺序编写异步代码,自动进行线程切换。 并且ViewModelScope为应用中的每个 ViewModel 定义了 ViewModelScope。如果 ViewModel 已清除,则在此范围内启动的协程都会自动取消。

4、Retrofit 将服务接口中的网络请求函数声明为suspend挂起接口函数,以支持Kotlin线程,并将suspend函数结果作为 LiveData 对象传送。

2、ViewModel

//获取ViewModel
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
复制代码

ViewModel 对象存在的时间范围是获取 ViewModel 时传递给 ViewModelProvider 的 Lifecycle。ViewModel 将一直留在内存中,直到限定其存在时间范围的 Lifecycle 永久消失:对于 Activity,是在 Activity 完成时;而对于 Fragment,是在 Fragment 分离时。

相关链接:https://developer.android.google.cn/topic/libraries/architecture/viewmodel

3、LiveData

//对User数据进行观察
viewModel.user.observe(this, Observer {
    //展示登录结果
    if (it.errorCode == 0) {
        Toast.makeText(this, it.data?.nickname, Toast.LENGTH_SHORT).show()
    } else {
        Toast.makeText(this, it.errorMsg, Toast.LENGTH_SHORT).show()
    }
})
复制代码

使用 LiveData 具有以下优势: 确保界面符合数据状态

LiveData 遵循观察者模式。当生命周期状态发生变化时,LiveData 会通知 Observer 对象。您可以整合代码以在这些 Observer 对象中更新界面。观察者可以在每次发生更改时更新界面,而不是在每次应用数据发生更改时更新界面。
复制代码

不会发生内存泄漏

观察者会绑定到 Lifecycle 对象,并在其关联的生命周期遭到销毁后进行自我清理。
复制代码

不会因 Activity 停止而导致崩溃

如果观察者的生命周期处于非活跃状态(如返回栈中的 Activity),则它不会接收任何 LiveData 事件。
不再需要手动处理生命周期
界面组件只是观察相关数据,不会停止或恢复观察。LiveData 将自动管理所有这些操作,因为它在观察时可以感知相关的生命周期状态变化。
复制代码

数据始终保持最新状态

如果生命周期变为非活跃状态,它会在再次变为活跃状态时接收最新的数据。例如,曾经在后台的 Activity 会在返回前台后立即接收最新的数据。
复制代码

适当的配置更改

如果由于配置更改(如设备旋转)而重新创建了 Activity 或 Fragment,它会立即接收最新的可用数据。
复制代码

共享资源

可以使用单一实例模式扩展 LiveData 对象以封装系统服务,以便在应用中共享它们。
复制代码

相关链接:https://developer.android.google.cn/topic/libraries/architecture/livedata

4、Kotlin协程

4.1、异步的本质

什么是异步?

异步就是同时进行一个以上彼此目的不同的任务。
复制代码

但是对于有前后依赖关系的任务,异步该如何处理呢?

利用异步中的回调机制处理。
复制代码

为什么需要异步回调机制?

因为不同的任务之间存在前后的依赖关系。
复制代码

异步回调机制有什么缺点?

代码结构过分耦合,遇到多重函数回调的嵌套耦合,也就是回调地狱,代码会难以维护。
复制代码

解决回调地狱的方案有什么?

链式调用结构。
常见方式就是使用RxJava,它是反应函数式编程在Java中的实现。
但是RxJava中流的创建、转化与消费都需要使用到各种类和丰富的操作符,加大了RxJava的学习成本。
减少在无封装情况下使用RxJava,因为你无法保证团队里面的每一个成员都能看懂它,并且在修改时都能做出正确选择。
复制代码

在串行的执行中,虽然代码确实是顺序执行的,但其实是在不同的线程上顺序执行的。 那为什么在串行的执行中代码执行顺序一致,却还要使用回调呢?

因为串行的执行中,执行是阻塞式的,主线程的阻塞会导致很严重的问题,所以所有的耗时操作不能在主线程中执行,所以就需要多线程并行来执行。
复制代码

在并行的执行中,异步回调其实就是代码的多线程顺序执行。 那能不能既按照顺序的方式编写代码,又可以让代码在不同的线程顺序执行,自动完成线程的切换工作呢?

那就是Kotlin协程。
Kotlin 的协程是一种无栈协程的实现,它的控制流转依靠对协程体本身编译生成的状态机的状态流转来实现,变量保存也是通过闭包语法来实现的。
复制代码

结论:

异步回调就是代码的多线程顺序执行,而Kotlin协程可以实现顺序编写异步代码,自动进行线程切换。
复制代码

那么协程自动进行线程切换的原理是什么?

Yield:让出CPU,放弃调度控制权,回到上一次Resume的地方
Resume:获取调度控制权,继续执行程序,到上一次Yield的地方
复制代码

例子:

1. GlobalScope.launch发起了一个协程,并在IO线程上执行,
2. 在协程里,去调用接口获取结果。
3. 拿到结果,使用withContext(Dispatchers.Main)切换到主线程并更新界面
复制代码

4.2、协程的类型

CoroutineScope

是协程范围,指的是协程内的代码运行的时间周期范围,如果超出了指定的协程范围,协程会被取消执行。
复制代码

GlobalScope

指的是与应用进程相同的协程范围,也就是在进程没有结束之前协程内的代码都可以运行。
复制代码

JetPack中提供的生命周期感知型协程范围:

ViewModelScope,为应用中的每个 ViewModel 定义了 ViewModelScope。如果 ViewModel 已清除,则在此范围内启动的协程都会自动取消。

LifecycleScope,为每个 Lifecycle 对象定义了 LifecycleScope。在此范围内启动的协程会在 Lifecycle 被销毁时取消。

使用 LiveData 时,可能需要异步计算值。可以使用 liveData 构建器函数调用 suspend 函数,并将结果作为 LiveData 对象传送。
复制代码

相关链接:https://developer.android.google.cn/topic/libraries/architecture/coroutines

4.3、协程的启动

launch方法:

/**
 * 重要知识:ViewModel+协程
 */
fun ViewModel.launch(
    block: suspend CoroutineScope.() -> Unit,
    onError: (e: Throwable) -> Unit = {},
    onComplete: () -> Unit = {}
) {
    viewModelScope.launch(CoroutineExceptionHandler { _, e -> onError(e) }) {
        try {
            block.invoke(this)
        } finally {
            onComplete()
        }
    }
}
复制代码

源码:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}
复制代码

4.3.1、launch方法解释

context

协程上下文,可以指定协程运行的线程。默认与指定的CoroutineScope中的coroutineContext保持一致,比如GlobalScope默认运行在一个后台工作线程内。也可以通过显示指定参数来更改协程运行的线程,Dispatchers提供了几个值可以指定:Dispatchers.Default、Dispatchers.Main、Dispatchers.IO、Dispatchers.Unconfined。
复制代码

start

协程的启动模式。默认的CoroutineStart.DEFAULT是指协程立即执行,除此之外还有CoroutineStart.LAZY、CoroutineStart.ATOMIC、CoroutineStart.UNDISPATCHED。
复制代码

block

协程主体。也就是要在协程内部运行的代码,可以通过lamda表达式的方式方便的编写协程内运行的代码。
复制代码

CoroutineExceptionHandler

指定CoroutineExceptionHandler来处理协程内部的异常。
复制代码

Job

返回值,对当前创建的协程的引用。可以通过Job的start、cancel、join等方法来控制协程的启动和取消。
复制代码

4.4、suspend挂起函数

suspend关键字只起到了标志这个函数是一个耗时操作,必须放在协程中执行的作用,而withContext方法则进行了线程的切换工作。

协程中的代码自动地切换到其他线程之后又自动地切换回了主线程! 顺序编写保证了逻辑上的直观性,协程的自动线程切换又保证了代码的非阻塞性。 挂起函数必须在协程或者其他挂起函数中被调用,也就是挂起函数必须直接或者间接地在协程中执行。

那为什么协程中的代码没有在主线程中执行呢?而且执行完毕为什么还会自动地切回主线程呢?

协程的挂起可以理解为协程中的代码离开协程所在线程的过程,协程的恢复可以理解为协程中的代码重新进入协程所在线程的过程。协程就是通过的这个挂起恢复机制进行线程的切换。

4.5、async await方法

用async方法包裹了suspend方法来执行并发请求,并发结果都返回之后,切换到主线程,接着再用await方法来获取并发请求结果。

5、Retrofit

HTTP接口suspend挂起函数:

interface ApiService {
    @FormUrlEncoded
    @POST("user/login")
    suspend fun loginForm(@Field("username")  username: String,@Field("password")  password: String): BaseResponse<User>
}
复制代码

kotlin泛型:

data class BaseResponse<T>(
    val errorCode: Int=0,
    val errorMsg:String? = null,
    var data: T? = null
)
复制代码

这是使用MVVM架构模式+Kotlin协程+JetPack(ViewModel+LiveData)+Retrofit的架构,实现WanAndroid登录接口的小DEMO,后续会慢慢完善WanAndroid客户端,地址见: https://github.com/chaozhouzhang/WanAndroid

欢迎关注微信公众号,Android技术堆栈:

文章分类
阅读
文章标签