标题皮一下,个人项目引进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
             )
           }
         }
       }
     }
   }
}

运转一下。

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

在上面的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是暖流的特性才直接实例化。

运转一下。

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

效果图有两个点是比较关键的。

  • 自定义计时器的时刻与屏幕发动时刻是一样的,阐明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,那么在应用进入后台后,也会继续进行数据的搜集,这样将形成资源的糟蹋。

学之前“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