美文网首页
学之前“flow?狗都不学”学之后“狗不学正好我学”

学之前“flow?狗都不学”学之后“狗不学正好我学”

作者: _Jun | 来源:发表于2023-01-27 15:17 被阅读0次

标题皮一下,个人项目引入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在排版,内容有较多相似地方。

  1. Kotlin Flow: Best Practices
  2. one-shot operation with Flow in Android
  3. Complete guide to LiveData and Flow: Answering — Why, Where, When, and Which.
  4. 官方推荐 Flow 取代 LiveData,有必要吗?
  5. Recommended Ways To Create ViewModel or AndroidViewModel

作者:DoubleYellowIce
链接:https://juejin.cn/post/7190005859034857532

相关文章

  • 学之前“flow?狗都不学”学之后“狗不学正好我学”

    标题皮一下,个人项目引入Kotlin Flow一段时间了,这篇文章想写写个人理解到的一点皮毛,有错欢迎在评论区指出...

  • Kotlin 学?不学?学?不学?......

    没想到Kotlin让Android开发社区沸腾了,一夜之间各种入门到精通的教程都有了,好多兄弟跑来问Kotlin的...

  • 《财富自由之路》读书笔记7

    1.作者用“汉族狗”和“朝族狗”的例子,绝对地说服了我学英语跟智商无关。学不学得好英语要看英语是不是“刚需”。我想...

  • 学or不学?

    还记得小时候对爸爸妈妈说:“我永远不学自行车,宁愿走。”后来在初中时学会了,感觉比走路快多了。 看电视被爸爸妈妈碎...

  • 学不学

    在学校大家总爱把我和一位老师对比,认为我们两个人之间很像,但是我却落后她很多,要我多像她学习。学什么呢?大概就是她...

  • 挣扎的痛苦

    学还是不学,脑海中的撕杀,学还不好,不学又无正事,挣扎,喘息……学!加油!

  • 时间紧张的情况下学好英语的秘籍

    是否,您和很多朋友一样,觉得我每天工作忙成狗,如何还有心思学英语?即使学,效果也不会好,还不如不学!今天,Kevi...

  • 如何利用碎片时间自学英语

    是否,您和很多朋友一样,觉得我每天工作忙成狗,如何还有心思学英语?即使学,效果也不会好,还不如不学!今天,Kevi...

  • 英语和拼音能否同学?

    能。 第一、少儿学英语不学ABC就像学音乐不学123,学算术不学123。没有人因为怕孩子弄混了音乐123和算术12...

  • 人生苦短

    人生苦短,汝若学,则智。 汝若不学,则殆。 汝学矣,觉长,汝不适学, 汝不学,觉长,汝不适不学。 何为者也?吾也!...

网友评论

      本文标题:学之前“flow?狗都不学”学之后“狗不学正好我学”

      本文链接:https://www.haomeiwen.com/subject/zxeehdtx.html