Coroutines in Android - One Shot and Multiple Values
coroutines in android - one shot and multiple values
在android中, 我们用到的数据有可能是一次性的, 也有可能是需要多个值的.
本文介绍android中结合协程(coroutines)的mvvm模式如何处理这两种情况. 重点介绍协程flow
在android中的应用.
one-shot vs multiple values
实际应用中要用到的数据可能是一次性获取的(one-shot), 也可能是多个值(multiple values), 或者称为流(stream).
举例, 一个微博应用中:
- 微博信息: 请求的时候获取, 结果返回即完成. -> one-shot.
- 阅读和点赞数: 需要观察持续变化的数据源, 第一次结果返回并不代表完成. -> multiple values, stream.
mvvm构架中的数据类型
一次性操作和观察多个值(流)的数据, 在架构上看起来会有什么不同呢?
- one-shot operation: viewmodel中是
livedata
, repository和data source中是suspend fun
.
class myviewmodel { val result = livedata { emit(repository.fetchdata()) } }
多个值的实现有两种选择:
- multiple values with livedata: viewmodel, repository, data source都返回
livedata
. 但是livedata
其实并不是为流式而设计的, 所以用起来会有点奇怪. - streams with flow: viewmodel中是
livedata
, repository和data source返回flow
.
可以看出两种方式的主要不同点就是viewmodel消费的数据形式, 是livedata
还是flow
.
后面会从viewmodel, repository和data source三个层面来说明.
flow是什么
既然提到了flow
, 我们先来简单讲一下它是什么, 这样大家能在same page.
kotlin中的多个值, 可以存储在集合中, 比如list, 也可以靠计算生成sequence, 但如果值是异步生成的, 需要将方法标记为suspend
来避免阻塞主线程.
flow和sequence类似, 但flow是非阻塞的.
看这个例子:
fun foo(): flow<int> = flow { // flow builder for (i in 1..3) { delay(1000) // pretend we are doing something useful here emit(i) // emit next value } } fun main() = runblocking<unit> { // launch a concurrent coroutine to check if the main thread is blocked launch { for (k in 1..3) { println("i'm not blocked $k") delay(1000) } } // collect the flow foo().collect { value -> println(value) } }
这段代码执行后输出:
i'm not blocked 1 1 i'm not blocked 2 2 i'm not blocked 3 3
- 这里用来构建flow的
flow
方法是一个builder function, 在builder block里的代码可以被suspend
. -
emit
方法负责发送值. - cold stream: 只有调用了terminal operation才会被激活. 最常用的是
collect()
.
如果熟悉reactive streams, 或用过rxjava就可以感觉到, flow的设计看起来很类似.
viewmodel层
发送单个值的情况比较简单和典型, 这里不再多说, 主要说发送多个值的情况. 每次又分viewmodel消费的类型是livedata
还是flow
两种情况来讨论.
发射n个值
livedata -> livedata
val currentweather: livedata<string> = datasource.fetchweather()
flow -> livedata
val currentweatherflow: livedata<string> = livedata { datasource.fetchweatherflow().collect { emit(it) } }
为了减少boilerplate代码, 简化写法:
val currentweatherflow: livedata<string> = datasource.fetchweatherflow().aslivedata()
后面都直接用这种简化的形式了.
发射1+n个值
livedata -> livedata
val currentweather: livedata<string> = livedata { emit(loading_string) emitsource(datasource.fetchweather()) }
emitsource()
发送的是一个livedata
.
flow -> livedata
用flow
的时候可以用上面同样的形式:
val currentweatherflow: livedata<string> = livedata { emit(loading_string) emitsource( datasource.fetchweatherflow().aslivedata() ) }
这样写看起来有点奇怪, 可读性不好, 所以可以利用flow
的api, 写成这样:
val currentweatherflow: livedata<string> = datasource.fetchweatherflow() .onstart{emit(loading_string)} .aslivedata()
suspend transformation
如果想在viewmodel中做一些转换.
livedata -> livedata
val currentweatherlivedata: livedata<string> = datasource.fetchweather().switchmap { livedata { emit(heavytransformation(it)) } }
这里不太适合用map
来做转换, 因为是在主线程.
flow -> livedata
用flow
来做转换就很方便:
val currentweatherflow: livedata<string> = datasource.fetchweatherflow() .map{ heavytransformation(it) } .aslivedata()
repository层
repository层通常用来组装和转换数据.livedata
被设计的初衷并不是做这些转换的.flow
则提供了很多有用的操作符, 所以显然是一种更好的选择:
val currentweatherflow: flow<string> = datasource.fetchweatherflow() .map { ... } .filter { ... } .dropwhile { ... } .combine { ... } .flowon(dispatchers.io) .oncompletion { ... }
data source层
data source层是网络和数据库, 通常会用到一些第三方的库.
如果用了支持协程的库, 如retrofit和room, 那么只需要把方法标记为suspend的, 就行了.
- retrofit supports coroutines from 2.6.0
- room supports coroutines from 2.1.0
one-shot operations
对于一次性操作比较简单, 数据层的只要suspend
方法返回值就可以了.
suspend fun dooneshot(param: string) : string = retrofitclient.dosomething(param)
如果所用的网络或者数据库不支持协程, 有办法吗? 答案是肯定的.
用suspendcoroutine
来解决.
比如你用的第三方库是基于callback的, 可以用suspendcancellablecoroutine
来改造one-shot operation:
suspend fun dooneshot(param: string): result<string> = suspendcancellablecoroutine { continuation -> api.addoncompletelistener { result -> continuation.resume(result) }.addonfailurelistener { error -> continuation.resumewithexception(error) }.fetchsomething(param) }
如果协程被取消了, 那么resume会被忽略.
验证代码如期工作后, 可以做进一步的重构, 把这部分抽象出来.
data source with flow
数据层返回flow
, 可以用flow
builder:
fun fetchweatherflow(): flow<string> = flow { var counter = 0 while(true) { counter++ delay(2000) emit(weatherconditions[counter % weatherconditions.size]) } }
如果你所用的库不支持flow, 而是用回调, callbackflow
builder可以用来改造流.
fun flowfrom(api: callbackbasedapi): flow<t> = callbackflow { val callback = object: callback { override fun onnextvalue(value: t) { offer(value) } override fun onapierror(cause: throwable) { close(cause) } override fun oncompleted() = close() } api.register(callback) awaitclose { api.unregister(callback) } }
可能并不需要livedata
在上面的例子中, viewmodel仍然保持了自己向ui暴露的数据是livedata
类型. 那么有没有可能不用livedata
呢?
lifecyclescope.launchwhenstarted { viewmodel.flowtoflow.collect { binding.currentweather.text = it } }
这样其实和用livedata
是一样的效果.
参考
视频:
文档:
博客:
- coroutines on android (part iii): real work
- lessons learnt using coroutines flow in the android dev summit 2019 app
最后, 欢迎关注微信公众号: 圣骑士wind