标题皮一下,个人项目引进Kotlin Flow一段时刻了,这篇文章想写写个人了解到的一点皮裘,有错欢迎在评论区指出。
Flow基础常识
Flow可了解为数据流,运用起来比较简略,看几个demo就能够直接上手了,除了提几个点之外也不再赘述。
- Flow为冷流。在Flow常识体系中,生产(获取)数据的可称为生产者(producer),消费(运用)数据的可称为顾客(consumer),冷流即有顾客消费数据,生产者才会生产数据。
- Flow中生产者与顾客为一对一的联系,即顾客不share(同享)同一个Flow,新加一个顾客,就会新创立一个Flow。
上面两个点能够通过个简略的demo进行验证。
val timerFlow = flow {
val start = 0
var current = start
while (true) {
emit(current)
current++
delay(1000)
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var firstTimer by mutableStateOf(0)
var secondTimer by mutableStateOf(0)
var thirdTimer by mutableStateOf(0)
val fontSize: TextUnit = 30.sp
lifecycleScope.launch {
while (true) {
delay(1000)
firstTimer++
}
}
setContent {
var secondTimerIsVisible by remember {
mutableStateOf(false)
}
var thirdTimerIsVisible by remember {
mutableStateOf(false)
}
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "屏幕发动时刻为${firstTimer}秒",
textAlign = TextAlign.Center, fontSize = fontSize
)
if (secondTimerIsVisible) {
Text(
"第一个自定义计时器的时刻为${secondTimer}秒。",
textAlign = TextAlign.Center,
fontSize = fontSize
)
} else {
Button(
onClick = {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
timerFlow.collect {
secondTimer = it
}
}
}
secondTimerIsVisible = true
},
) {
Text(
text = "发动第一个自定义计时器",
textAlign = TextAlign.Center,
fontSize = fontSize
)
}
}
if (thirdTimerIsVisible) {
Text(
"第二个自定义计时器的时刻为${thirdTimer}秒。",
textAlign = TextAlign.Center,
fontSize = fontSize
)
} else {
Button(
modifier = Modifier.padding(10.dp),
onClick = {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
timerFlow.collect {
thirdTimer = it
}
}
}
thirdTimerIsVisible = true
},
) {
Text(
text = "发动第二个自定义计时器",
textAlign = TextAlign.Center,
fontSize = fontSize
)
}
}
}
}
}
}
运转一下。
在上面的demo中,创立了三个计时器,第一个计时器用协程来实现,来计时屏幕的发动时刻,第二,第三个计时器用flow来实现,为自定义计时器,需求手动发动。
- 在屏幕发动几秒后,才发动第二个计时器,该计时器是从0秒开端发动的,这阐明flow并不是屏幕一发动就发生数据,而是有顾客消费数据,才会发生数据。
- 第二个计时器和第三个计时器的时刻不一样,阐明它们虽然用了同一个timerFlow变量,却不是同享同一个flow,新加一个顾客,就会新创立一个Flow。
SharedFlow
稍微了解规划形式的读者应该知道,Flow其实是用了观察者形式,生产者对应subject(被观察者),顾客对应observer(观察者),仅仅flow中每个subject只允许有一个observer,但在实际项目中,一个subject有多个observer的状况再正常不过,于是乎就有了SharedFlow。
SharedFlow是同享流,它的特性与flow刚好反着来。
- SharedFlow是暖流,即便没有顾客也会一向发生数据,该发生数据的战略是可变的,后面会具体讲。
- 多个顾客会同享同一个Flow。
对上面代码进行修正,将Flow转换为SharedFlow,并将其移动到新建的MainViewModel中。
class MainViewModel : ViewModel() {
val timerFlow = flow {
val start = 0
var current = start
while (true) {
emit(current)
current++
delay(1000)
}
}.shareIn(viewModelScope, SharingStarted.Eagerly,0)
}
修正MainActivity的代码,增加viewModel的实例化代码private val viewModel: MainViewModel = MainViewModel()
,并timerFlow.collect
改成viewModel.timerFlow.collect
,改动较少,就不放出全部源码了,需求注意的是,将MainViewModel直接实例化的做法是过错的,理由是当Activity由于某种原因,如屏幕旋转而毁掉时,MainViewModel会重新实例化,这样就达不到ViewModel数据持久化的意图了,本文是为了便利演示SharedFlow是暖流的特性才直接实例化。
运转一下。
效果图有两个点是比较关键的。
- 自定义计时器的时刻与屏幕发动时刻是一样的,阐明SharedFlow不管有没有顾客,都会发生数据。
- 两个自定义计时器的时刻是一样的,阐明两个计时器同享了同一个SharedFlow。
先看看shareIn()办法的源码。
public fun <T> Flow<T>.shareIn(
scope: CoroutineScope,
started: SharingStarted,
replay: Int = 0
): SharedFlow<T>
-
scope参数为指定SharedFlow在哪个协程域发动。
-
replay参数指定当有新的顾客呈现时,发送多少个之前的数据给该顾客。
-
started为发动战略。
有三个发动战略可选。
-
SharingStarted.Eagerly 。SharedFlow会当即发生数据,即便连第一个顾客还没呈现,demo中运用的便是该发动战略。
-
SharingStarted.Lazily。SharedFlow只要在第一个顾客消费数据后才发生数据。
-
WhileSubscribed。WhileSubscribed的源码如下所示。
public fun SharingStarted.Companion.WhileSubscribed( stopTimeout: Duration = Duration.ZERO, replayExpiration: Duration = Duration.INFINITE )
- stopTimeOut。当SharedFlow一个顾客也没有的时分,等候多久才中止流。
- replayExpiration。用来指定replay个数量的缓存在等候多少时刻后无效,当你不想用户看到较旧的数据时,可运用这个参数。
-
此外,SharedFlow也能够直接创立。
class MainViewModel : ViewModel() {
val timerFlow = MutableSharedFlow<Int>()
init {
viewModelScope.launch {
val start = 0
var current = start
while (true) {
timerFlow.emit(current)
current++
delay(1000)
}
}
}
}
StateFlow
StateFlow是SharedFlow的一个特殊变种,其特性有:
- 一直有值且值仅有。
- 能够有多个顾客。
- 永远只把最新的值给到顾客。
第二,第三特性比较好了解,便是replay参数为1的SharedFlow,那第一个特性需求结合demo才更好了解。
先将flow转化为StateFlow。
class MainViewModel : ViewModel() {
val timerFlow = flow {
val start = 0
var current = start
while (true) {
emit(current)
current++
delay(1000)
}
}.stateIn(viewModelScope, SharingStarted.Eagerly,0)
}
sharedIn()的源码如下所示。
public fun <T> Flow<T>.stateIn(
scope: CoroutineScope,
started: SharingStarted,
initialValue: T//初始值
): StateFlow<T>{
}
运转一下。
与SharedFlow比较,最大的不同便是SharedFlow demo中的自定义计时器是从0开端的,之后才和屏幕发动时刻共同,而这个StateFlow demo中的自定义计时器是一发动就和屏幕发动时刻共同,呈现这种状况的原因是:
- SharedFlow并不存储值,MainActivity只要在 SharedFlow emit()出最新值的时分,才干collect()到值。
- 根据StateFlow的第一点特性,其一直有值且值仅有,在MainActivity一订阅StateFlow的时分,就立马就将最新的值给到了MainActivity,所以StateFlow demo中的计时器没有阅历0的阶段。
能够看到,StateFlow与之前的LiveData比较类似的。
StateFlow还有另一种在实际项目中更常用的运用方式,修正MainViewModel的代码。
class MainViewModel : ViewModel() {
private val _timerFlow: MutableStateFlow<Int> = MutableStateFlow(0)
val timerFlow: StateFlow<Int> = _timerFlow.asStateFlow()
init {
viewModelScope.launch {
val start = 0
var current = start
while (true) {
_timerFlow.value = current
current++
delay(1000)
}
}
}
}
代码中先创立私有MutableStateFlow实例_timerFlow,再将其转化为公共StateFlow实例timerFlow,由于timerFlow只可读,不能修正,露出给Main Activity运用更符合标准。
collect Flow的标准做法
官方引荐咱们用lifeCycle.repeatOnLifecycle()去collect flow。
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.timerFlow.collect {
...
}
}
}
Activity会在onStart()开端搜集数据,在onStop()结束数据的搜集。
如下图所示,假如直接运用lifecycleScope.launch去collect flow,那么在应用进入后台后,也会继续进行数据的搜集,这样将形成资源的糟蹋。
要是嫌上述代码繁琐,也能够增加以下依靠。
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha01"
然后将collect代码改成下述代码也能达到相同的效果,不过该办法只适用于StateFlow。
viewModel.timerFlow.collectAsStateWithLifecycle()
该办法的源码如下所示。
fun <T> StateFlow<T>.collectAsStateWithLifecycle(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
context: CoroutineContext = EmptyCoroutineContext
): State<T>
从第二个参数能够知道默认是从onStart()开端搜集数据。
项目真的需求引进Flow吗?
谷歌对Flow的推崇力度很大,Android官网中除了Flow相关的文章之外,许多代码示例也多多少少用了Flow,大有一种Flow放之四海而皆准的态势,但运用一段时刻后,我发现Flow的应用场景其实也是有一定局限的。
以我个人项目中的之前Repository类中某段代码为例。
override suspend fun getCategory(): Flow<List<Category>?> {
return flow {
when (val response = freeApi.getCategoryList()) {
is ApiSuccess -> {
val categories = response.data
withContext(Dispatchers.IO) {
Timber.v("cache categories in db")
categoryDao.insertCategoryList(categories)
}
emit(categories)//1
}
else -> {
Timber.d(response.toString())
val cacheCategories = withContext(Dispatchers.IO) {
categoryDao.getCategoryList()
}
if (cacheCategories.isNotEmpty()) {
Timber.d("load categories from db")
emit(cacheCategories)//2
} else {
Timber.d("fail to load category from db")
emit(null)//3
}
}
}
}
}
其实上面代码并不适合用Flow,由于虽然代码1,2,3处都有emit,但终究getCategory()只会emit一次值,Flow是数据流,但一个数据并不能流(Flow)起来,这样无法体现出Flow的优点,徒增资源的耗费。
除此之外,在一个屏幕需求获取从多个api获取数据的时分,假如强行用Flow就会呈现繁琐重复的代码,像下面的代码会有好几处。
getXXX().catch{
//进行反常处理
}.collect{
//得到数据
}
我也去查阅了相关的材料,发现确实如此,具体可见参阅材料1和2。
参阅材料
本文首要参阅了材料4,与材料4在排版,内容有较多类似当地。
- Kotlin Flow: Best Practices
- one-shot operation with Flow in Android
- Complete guide to LiveData and Flow: Answering — Why, Where, When, and Which.
- 官方引荐 Flow 取代 LiveData,有必要吗?
- Recommended Ways To Create ViewModel or AndroidViewModel