Kotlin 学习笔记(五)—— Flow 数据流学习实践指北(一)(下)

简介: Kotlin 学习笔记(五)—— Flow 数据流学习实践指北(一)(下)

3.3 zip 中间操作符


zip 顾名思义,就是可以将两个 Flow 汇合成一个 Flow,举个栗子就知道了:

//code 11
lateinit var testFlow1: Flow<String>
lateinit var testFlow2: Flow<String>
private fun setupTwoFlow() {
    testFlow1 = flowOf("Red", "Blue", "Green")
    testFlow2 = flowOf("fish", "sky", "tree", "ball")
    CoroutineScope(Dispatchers.IO).launch {
        testFlow1.zip(testFlow2) { firstWord, secondWord ->
            "$firstWord $secondWord"
        }.collect {
            println("+++ $it +++")
        }
    }
}
// 输出结果:
//com.example.myapplication I/System.out: +++ Red fish +++
//com.example.myapplication I/System.out: +++ Blue sky +++
//com.example.myapplication I/System.out: +++ Green tree +++
//zip 方法声明:
public fun <T1, T2, R> Flow<T1>.zip(other: Flow<T2>, transform: suspend (T1, T2) -> R): Flow<R> = zipImpl(this, other, transform)

zip 方法的声明中可知,zip 方法的第二个参数就是针对两个 Flow 进行各种处理的挂起函数,也可如例子中写成尾调函数的样子,返回值是处理之后的 Flow。而且当两个 Flow 长度不一样时,最后的结果会默认剔除掉先前较长的 Flow 中的元素。所以 testFlow2 中的 “ball” 就被自动剔除掉了。


4. Flow 异常处理


正如 RxJava 框架中的 subscribe 方法可以通过传入 Observer 对象在其 onNextonCompleteonError 返回之前处理的结果,Flow 也有诸如 catchonCompletion 等操作符去处理执行的结果。例如下面的代码:

//code 12
private fun handleExceptionDemo() {
    val testFlow = (1..5).asFlow()
    CoroutineScope(Dispatchers.Default).launch {
        testFlow.map {
            check(it != 3) {
                //it == 3 时,会走到这里
                println("+++ catch value = $it")
            }
            println("+++ not catch value = $it")
            it * it
        }.onCompletion {
            println("+++ onCompletion value = $it")
        }.catch { exception ->
            println("+++ catch exception = $exception")
        }.collect{
            println("+++ collect value = $it")
        }
    }
}
//输出结果:
//com.example.myapplication I/System.out: +++ not catch value = 1
//com.example.myapplication I/System.out: +++ collect value = 1
//com.example.myapplication I/System.out: +++ not catch value = 2
//com.example.myapplication I/System.out: +++ collect value = 4
//com.example.myapplication I/System.out: +++ catch value = 3
//com.example.myapplication I/System.out: +++ onCompletion value = java.lang.IllegalStateException: kotlin.Unit
//com.example.myapplication I/System.out: +++ catch exception = java.lang.IllegalStateException: kotlin.Unit

顺着代码咱先来看看一些常用的 Flow 中间操作符。

1)map :用来将 Flow 中的数据一个个拿出来做各自的处理,然后交给下一个操作符;本例中就是将 Flow 中的数据进行平方处理;

2)check() :类似于一个检查站,满足括号内条件的数据可以通过,不满足则交给它的尾调函数处理,并且抛出异常;

3)onCompletion :Flow 最后的兜底器。无论 Flow 最后是执行完成、被取消、抛出异常,都会走到 onCompletion 操作符中,类似于在 Flow 的 collect 函数外加了个 tryfinally。官方给了个小栗子,还是很清楚的:

//code 13
try {
    myFlow.collect { value ->
        println(value)
    }
} finally {
    println("Done")
}
//上述代码可以替换为下面的代码:
myFlow
    .onEach { println(it) }
    .onCompletion { println("Done") }
    .collect()

所以,在 code 12 中的 onCompletion 操作符可以接住从 check 那儿抛出的异常;

4)catch :不用多说,专门用于捕捉异常的,避免程序崩溃。这里如果把 catch 去掉,程序就会崩溃。如果把 catchonCompletion 操作符位置调换,则 onCompletion 里面就接收不到异常信息了,如图所示。

image.png


5. Flow 数据请求实例


说了这么多,举个在实际中经常用到的数据请求的例子吧。先来看一个最简单的例子:


5.1 单接口请求


现在一般都是在 ViewModel 里持有 LiveData 数据,并且进行数据的请求,所以先来看下 ViewModel 中的代码实现:

//code 14
class SingleNetworkCallViewModel: ViewModel() {
    private val users = MutableLiveData<Resource<List<ApiUser>>>()
    private val apiHelperImpl = ApiHelperImpl(RetrofitBuilder.apiService)
    fun fetchUsers() {
        viewModelScope.launch {
            users.postValue(Resource.loading(null))
            apiHelperImpl.getUsers()
                .catch { e ->
                    users.postValue(Resource.error(e.toString(), null))
                }
                .collect {
                    users.postValue(Resource.success(it))
                }
        }
    }
    fun getUsersData(): LiveData<Resource<List<ApiUser>>> {
        return users
    }
}

从代码可看出,fetchUsers 方法就是数据请求方法,里面的核心方法是 ApiHelperImpl 类对象的 getUsers 方法,在之前初始化 apiHelperImpl 对象时传入了一个 RetrofitBuilder.apiService 值,所以底层还是用到了 Retrofit 框架进行的网络请求。Retrofit 相关的代码如下:

//code 15
object RetrofitBuilder {
    private const val BASE_URL = "https://xxxxxxx/"
    private fun getRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
//ApiService 中的代码也是一般常见的代码:
interface ApiService {
    @GET("users")
    suspend fun getUsers(): List<ApiUser>
}

再回过来看看 ViewModel 的代码,从 apiHelperImpl.getUsers 方法后面的 catchcollect 操作符也可看出,getUsers 方法返回的就是一个 Flow 对象,其使用的构造方法就是前文中说到的 flow{} 方法:

//code 16
class ApiHelperImpl(private val apiService: ApiService) : ApiHelper {
    override fun getUsers(): Flow<List<ApiUser>> {
        return flow { emit(apiService.getUsers()) }
    }
}

ApiHelper 其实就是一个接口,规定了 ApiHelperImpl 中数据请求的方法名及返回值,返回值是一个 Flow,里面是我们最终需要的数据列表:

//code 17
interface ApiHelper {
    fun getUsers(): Flow<List<ApiUser>>
}

Flow 调用 emit 发出去的就是 Retrofit 进行数据请求后返回的 List<ApiUser> 数据。

如何在 Activity 中使用就是之前使用 LiveData 的常规操作了:

//code 18
private fun setupObserver() {
    viewModel.getUsersData().observe(this, Observer {
        when (it.status) {
            Status.SUCCESS -> {
                progressBar.visibility = View.GONE
                it.data?.let { users -> renderList(users) }
                recyclerView.visibility = View.VISIBLE
            }
            Status.LOADING -> {
                progressBar.visibility = View.VISIBLE
                recyclerView.visibility = View.GONE
            }
            Status.ERROR -> {
                //Handle Error
                progressBar.visibility = View.GONE
                Toast.makeText(this, it.message, Toast.LENGTH_SHORT).show()
            }
        }
    })
}

5.2 双接口并行请求


上述例子是最简单的单个数据接口请求的场景,如果是两个或是多个数据接口需要并行请求,该如何处理呢?这就需要用到之前说的 Flow 中的 zip 操作符了。接着上面的例子,再添加一个数据请求方法 getMoreUsers ,那么两个接口并行的例子为:

//code 18
fun fetchUsers() {
    viewModelScope.launch {
        users.postValue(Resource.loading(null))
        apiHelper.getUsers()
            .zip(apiHelper.getMoreUsers()) { usersFromApi, moreUsersFromApi ->
                val allUsersFromApi = mutableListOf<ApiUser>()
                allUsersFromApi.addAll(usersFromApi)
                allUsersFromApi.addAll(moreUsersFromApi)
                return@zip allUsersFromApi
            }
            .flowOn(Dispatchers.Default)
            .catch { e ->
                users.postValue(Resource.error(e.toString(), null))
            }
            .collect {
                users.postValue(Resource.success(it))
            }
    }
}

两个数据接口请求的快慢肯定不一样,但不用担心,zip 操作符会等待两个接口的数据都返回之后才进行拼接并交给后面的操作符处理,所以这里还需要调用 flowOn 操作符将线程切换到后台线程中去挂起等待。但后面的 collect 操作符执行的代码是在主线程中,感兴趣的同学可以打印线程信息看看,这就需要了解一下 flowOn 操作符的用法了。

flowOn 方法可以切换 Flow 处理数据的所在线程,类似于 RxJava 中的 subscribeOn 方法。例如 flowOn(Dispatchers.Default) 就是将 Flow 的操作都放到后台线程中执行。

flowOn 操作符之前没有设置任何的协程上下文,那么 flowOn 操作符可以为它之前的操作符设置执行所在的线程,并不会影响它之后下游的执行所在线程。下面是一个简单例子:

//code 19
private fun flowOnDemo() {
    val testFlow = (1..2).asFlow()
    MainScope().launch {
        testFlow
            .filter {
                println("1+++ $it  ${Thread.currentThread().name}")
                it != 3
            }.flowOn(Dispatchers.IO)
            .map {
                println("2+++ $it  ${Thread.currentThread().name}")
                it*it
            }.flowOn(Dispatchers.Main)
            .filter {
                println("3+++ $it  ${Thread.currentThread().name}")
                it!=25
            }.flowOn(Dispatchers.IO)
            .collect{
                println("4+++ $it  ${Thread.currentThread().name}")
            }
    }
}
//输出结果:
//com.example.myapplication I/System.out: 1+++ 1  DefaultDispatcher-worker-1
//com.example.myapplication I/System.out: 1+++ 2  DefaultDispatcher-worker-1
//com.example.myapplication I/System.out: 2+++ 1  main
//com.example.myapplication I/System.out: 2+++ 2  main
//com.example.myapplication I/System.out: 3+++ 1  DefaultDispatcher-worker-1
//com.example.myapplication I/System.out: 3+++ 4  DefaultDispatcher-worker-1
//com.example.myapplication I/System.out: 4+++ 1  main
//com.example.myapplication I/System.out: 4+++ 4  main

发现了么?flowOn 操作符只对最近的上游操作符线程负责,它下游的线程会自动切换到之前所在的线程。如果连续有两个或多个 flowOn 操作符切换线程,则会切换到首个 flowOn 操作符切换的线程中去:

//code 20
testFlow
    .filter {
        println("1+++ $it  ${Thread.currentThread().name}")
        it != 3    //最终会在 Main 主线程中执行
    }.flowOn(Dispatchers.Main).flowOn(Dispatchers.IO).flowOn(Dispatchers.Default)
    .collect{
        println("4+++ $it  ${Thread.currentThread().name}")
}

filter 后面连续有两个 flowOn 操作符,但最终会在 Main 线程中执行 filter 操作符中的逻辑。

整体上看,Flow 在数据请求时所扮演的角色是数据接收与处理后发送给 UI 层的作用,这跟 RxJava 的职责是相同的,而且两者都有丰富的操作符来处理各种不同的情况。不同的是 Flow 是将接收到的数据放到 Flow 载体中,而 RxJava 一般将数据放到 Observable 对象中;Flow 处理数据更加方便和自然,去除了 RxJava 中繁多且功能臃肿的操作符。


总结


最后总结一下 Flow 第一小节的内容吧:

1)Flow 数据流可异步按顺序返回多个数据;

2)Flow 整体是由 构建器中间操作符末端操作符 组成;

3)冷流只有在调用末端操作符时,流的构造器和中间操作符才会开始执行;冷流的使用方和提供方是一对一的;

4)简单介绍了 collectreduce 末端操作符以及 zipmap 等中间操作符的使用;

5)Flow 异常处理所用到的 catchcheckonCompletion 等操作符的用法;

6)Flow 在数据请求上的实例

所用实例来源:github.com/MindorksOpe…

更多内容,欢迎关注公众号:修之竹

赞人玫瑰,手留余香!欢迎点赞、转发~ 转发请注明出处~


参考文献


  1. Android 上的 Kotlin 数据流;官方文档   https://developer.android.com/kotlin/flow
  2. Flow Kotlin 官方文档; https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/
  3. 【Kotlin Flow】 一眼看全——Flow操作符大全; 搬砖小子出现了   https://juejin.cn/post/6989536876096913439
  4. What is Flow in Kotlin and how to use it in Android Project?; Himanshu Singh; https://blog.mindorks.com/what-is-flow-in-kotlin-and-how-to-use-it-in-android-project
  5. Understanding Terminal Operators in Kotlin Flow; Amit Shekhar; https://blog.mindorks.com/terminal-operators-in-kotlin-flow
  6. Creating Flow Using Flow Builder in Kotlin; Amit Shekhar; https://blog.mindorks.com/creating-flow-using-flow-builder-in-kotlin
  7. Exception Handling in Kotlin Flow; Amit Shekhar; https://blog.mindorks.com/exception-handling-in-kotlin-flow
目录
相关文章
|
8天前
|
数据处理 开发者 Kotlin
利用Kotlin Flow简化数据流管理
随着移动端应用的复杂化,数据流管理成为一大挑战。Kotlin Flow作为一种基于协程的响应式编程框架,可简化数据流处理并支持背压机制,有效避免应用崩溃。本文通过解答四个常见问题,详细介绍Kotlin Flow的基本概念、创建方法及复杂数据流处理技巧,帮助开发者轻松上手,提升应用性能。
36 16
|
2天前
|
存储 API 数据库
Kotlin协程与Flow的魅力——打造高效数据管道的不二法门!
在现代Android开发中,Kotlin协程与Flow框架助力高效管理异步操作和数据流。协程采用轻量级线程管理,使异步代码保持同步风格,适合I/O密集型任务。Flow则用于处理数据流,支持按需生成数据和自动处理背压。结合两者,可构建复杂数据管道,简化操作流程,提高代码可读性和可维护性。本文通过示例代码详细介绍其应用方法。
11 2
|
9天前
|
数据处理 Kotlin
掌握这项Kotlin技能,让你的数据流管理不再头疼!Flow的秘密你解锁了吗?
【9月更文挑战第12天】随着移动应用发展,数据流管理日益复杂。Kotlin Flow作为一种基于协程的异步数据流处理框架应运而生,它可解耦数据的生产和消费过程,简化数据流管理,并支持背压机制以防应用崩溃。本文通过四个问题解析Kotlin Flow的基础概念、创建方式、复杂数据流处理及背压实现方法,助您轻松掌握这一高效工具,在实际开发中更从容地应对各种数据流挑战,提升应用性能。
28 8
|
10天前
|
数据处理 API 数据库
揭秘Kotlin Flow:迈向响应式编程的黄金钥匙
【9月更文挑战第11天】在现代软件开发中,异步编程与数据处理对于构建高性能应用至关重要。Kotlin Flow作为协程库的一部分,提供了简洁高效的API来处理数据流。本文将通过实例引导你从零开始学习Kotlin Flow,掌握构建响应式应用的方法。Flow是一种冷流,仅在订阅时才开始执行,支持map、filter等操作符,简化数据处理。
25 7
|
8天前
|
存储 数据处理 Kotlin
Kotlin Flow背后的神秘力量:背压、缓冲与合并策略的终极揭秘!
【9月更文挑战第13天】Kotlin Flow 是 Kotlin 协程库中处理异步数据流的强大工具,本文通过对比传统方法,深入探讨 Flow 的背压、缓冲及合并策略。背压通过 `buffer` 函数控制生产者和消费者的速率,避免过载;缓冲则允许数据暂存,使消费者按需消费;合并策略如 `merge`、`combine` 和 `zip` 则帮助处理多数据源的整合。通过这些功能,Flow 能更高效地应对复杂数据处理场景。
22 2
|
8天前
|
移动开发 定位技术 Android开发
「揭秘高效App的秘密武器」:Kotlin Flow携手ViewModel,打造极致响应式UI体验,你不可不知的技术革新!
【9月更文挑战第12天】随着移动开发领域对响应式编程的需求增加,管理应用程序状态变得至关重要。Jetpack Compose 和 Kotlin Flow 的组合提供了一种优雅的方式处理 UI 状态变化,简化了状态管理。本文探讨如何利用 Kotlin Flow 增强 ViewModel 功能,构建简洁强大的响应式 UI。
21 3
|
9天前
|
数据库 Kotlin
Kotlin中的冷流和热流以及如何让Flow停下来
本文介绍了Kotlin中`Flow`的概念及其类型,包括冷流(Cold Flow)、热流`SharedFlow`及具有最新值的`StateFlow`。文中详细描述了每种类型的特性与使用场景,并提供了停止`Flow`的方法,如取消协程、使用操作符过滤及异常处理。通过示例代码展示了如何运用这些概念。
17 2
|
2天前
|
API 数据处理 数据库
掌握 Kotlin Flow 的艺术:让无限数据流处理变得优雅且高效 —— 实战教程揭秘如何在数据洪流中保持代码的健壮与灵活
Kotlin Flow 是一个强大的协程 API,专为处理异步数据流设计。它适合处理网络请求数据、监听数据库变化等场景。本文通过示例代码展示如何使用 Kotlin Flow 管理无限流,如实时数据流。首先定义了一个生成无限整数的流 `infiniteNumbers()`,然后结合多种操作符(如 `buffer`、`onEach`、`scan`、`filter`、`takeWhile` 和 `collectLatest`),实现对无限流的优雅处理,例如计算随机数的平均值并在超过阈值时停止接收新数据。这展示了 Flow 在资源管理和逻辑清晰性方面的优势。
10 0
|
1月前
|
前端开发 编译器 测试技术
Kotlin Multiplatform 跨平台开发的优化策略与实践
本文深入讲解Kotlin Multiplatform(KMP)的优化策略与实践。KMP是由JetBrains推出的开源技术,允许跨平台共享代码同时保持原生优势。文章覆盖KMP核心概念、性能优化技巧(如代码结构优化、利用`expect`/`actual`关键字、Kotlin/Native性能特性等),以及在移动、桌面和Web应用的实际案例分析。此外,还介绍了如何利用KMP生态系统工具进行快速开发,并展望了KMP的未来发展。
42 0
|
8天前
|
Android开发 开发者 Kotlin
告别AsyncTask:一招教你用Kotlin协程重构Android应用,流畅度飙升的秘密武器
【9月更文挑战第13天】随着Android应用复杂度的增加,有效管理异步任务成为关键。Kotlin协程提供了一种优雅的并发操作处理方式,使异步编程更简单直观。本文通过具体示例介绍如何使用Kotlin协程优化Android应用性能,包括网络数据加载和UI更新。首先需在`build.gradle`中添加coroutines依赖。接着,通过定义挂起函数执行网络请求,并在`ViewModel`中使用`viewModelScope`启动协程,结合`Dispatchers.Main`更新UI,避免内存泄漏。使用协程不仅简化代码,还提升了程序健壮性。
21 1