技術共有

kotlinフロー学習ガイド(3) 最終章

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

序文

最初の 2 つの記事では、Flow とは何か、その使用方法、および関連するオペレーターの進歩について紹介しました。次の記事では、実際のプロジェクトでの Flow の使用方法を主に紹介します。

フローのライフサイクル

Flow の実際のアプリケーション シナリオを紹介する前に、Flow の最初の記事で紹介したタイマーの例を確認してみましょう。ViewModel で timeFlow データ フローを定義しました。

class MainViewModel : ViewModel() {

val timeFlow = flow {
    var time = 0
    while (true) {
        emit(time)
        delay(1000)
        time++
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

次に、アクティビティで、前に定義したデータ ストリームを受信します。

lifecycleOwner.lifecycleScope.launch {
    viewModel.timeFlow.collect { time ->
        times = time
        Log.d("ddup", "update UI $times")
    }
   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

実際の効果を確認するために実行してみましょう。

フロー1.gif

アプリがバックグラウンドに切り替わっても、ログが出力されていることに気づきましたか。これはリソースの無駄ではありません。受信側のコードを変更しましょう。

lifecycleOwner.lifecycleScope.launchWhenStarted {
     viewModel.timeFlow.collect { time ->
         times = time
         Log.d("ddup", "update UI $times")
     }
   }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

コルーチンの開始方法を launch から launchWhenStarted に変更し、再度実行して効果を確認してみましょう。

フロー2.gif

HOME ボタンをクリックしてバックグラウンドに戻ると、ログが出力されなくなっていることがわかります。変更が有効になっていることがわかりますが、フロント デスクに戻って取得しましょう。見た目:

フロー3.gif

フォアグラウンドに切り替えると、カウンターが 0 から開始しないことがわかります。そのため、実際には受信がキャンセルされず、バックグラウンドでのデータの受信が一時停止されるだけです。実際、launchWhenStarted は以前のデータを保持します。 API は廃止されました。代わりに GoogleRepeatOnLifecycle の方が推奨されており、パイプラインに古いデータが保持されるという問題がありません。
対応するコードを変換してみましょう。

lifecycleOwner.lifecycleScope.launch {
    lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.timeFlow.collect { time ->
            times = time
            Log.d("ddup", "update UI $times")
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

再実行して効果を確認します。

フロー4.gif

バックグラウンドからフォアグラウンドに切り替えると、データが再び 0 から始まることがわかります。これは、バックグラウンドに切り替えると、フローが作業をキャンセルし、元のデータがすべてクリアされることを意味します。

私たちは Flow を使用しており、repeatOnLifecycle を通じてプログラムのセキュリティをより確実に確保できます。

StateFlow が LiveData を置き換える

これまでの紹介はすべてフロー コールド フローの例です。次に、ホット フローの一般的なアプリケーション シナリオをいくつか紹介します。
先ほどのタイマーの例を引き続き使用しますが、画面が水平画面と垂直画面の間で切り替わるとどうなるでしょうか?

フロー5.gif

横画面と縦画面を切り替えた後、アクティビティが再作成され、再作成後に timeFlow が再収集され、コールド フローが再収集されて再実行され、タイマーが開始されることがわかります。多くの場合、水平画面と垂直画面を切り替えたいと考えますが、このとき、少なくとも一定時間内はページのステータスが変わらないことを望みます。ここでは、コールド フローを に変更します。ホットフローして試してみてください:

val hotFlow =
    timeFlow.stateIn(
        viewModelScope,
        SharingStarted.WhileSubscribed(5000),
        0
    )
    
    ```
lifecycleOwner.lifecycleScope.launch {
    lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.hotFlow.collect { time ->
            times = time
            Log.d("ddup", "update UI $times")
        }
    }
}
```
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

ここでは、stateIn の 3 つのパラメータに焦点を当てます。最初のパラメータはコルーチンのスコープであり、2 番目のパラメータはフローが動作状態を維持するための最大有効時間です。パラメータは初期値です。

再実行して効果を確認します。

フロー6.gif

ここでは、水平画面と垂直画面を切り替えた後に印刷されたログが表示されます。タイマーは 0 から開始されません。
上記では、コールド フローをホット フローに変更する方法を紹介しましたが、stateFlow が LiveData を置き換える方法についてはまだ紹介していません。

private val _stateFlow = MutableStateFlow(0)
val stateFlow = _stateFlow.asStateFlow()

fun startTimer() {
    val timer = Timer()
    timer.scheduleAtFixedRate(object :TimerTask() {
        override fun run() {
            _stateFlow.value += 1
        }

    },0,1000)
}

```

viewModel.startTimer()

lifecycleOwner.lifecycleScope.launch {
    lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.stateFlow.collect { time ->
            times = time
            Log.d("ddup", "update UI $times")
        }
    }
}
```
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

StateFlow ホット フローを定義し、LiveData setData と同様に startTimer() メソッドを通じて stateFlow 値を変更します。ボタンがクリックされると、StateFlow 値の変更が開始され、対応するフローの値が収集されます。データの変更を監視する LiveData Observe メソッド。
実際のランニング効果を見てみましょう。

フロー7.gif

ここまではStateFlowの基本的な使い方を紹介しましたが、今回はSharedFlowを紹介します。

シェアードフロー

SharedFlow を理解するには、まずスティッキー イベントの概念を理解します。文字通り、オブザーバーがデータ ソースにサブスクライブすると、データ ソースに既に最新のデータがある場合、データはすぐにオブザーバーにプッシュされます。上記の説明から判断すると、LiveData はこのスティッキー特性に準拠していますが、StateFlow はどうでしょうか。検証するために簡単なデモを作成してみましょう。


class MainViewModel : ViewModel() {

private val _clickCountFlow = MutableStateFlow(0)

val clickCountFlow = _clickCountFlow.asStateFlow()

fun increaseClickCount() {
    _clickCountFlow.value += 1
}
}
//MainActivity
```
val tv = findViewById<TextView>(R.id.tv_content)
val btn = findViewById<Button>(R.id.btn)
btn.setOnClickListener {
    viewModel.increaseClickCount()
}

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.clickCountFlow.collect { time ->
            tv.text = time.toString()
            Log.d("ddup", "update UI $time")
        }
    }
}
```

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

まずMainViewModelでclickCountFlowを定義し、次にActivityでButtonをクリックしてclickCountFlowのデータを変更し、clickCountFlowを受け取ってテキスト上にデータを表示します。
ランニング効果を見てみましょう。

フロー8.gif

水平画面と垂直画面を切り替えると、Activity が再作成され、clickCountFlow が再収集されることがわかります。これは、StateFlow がスティッキーであることを示しています。別の例を見てみましょう。クリックしてログインするシナリオをシミュレートします。ログイン ボタンをクリックしてログインします。

//MainViewModel
    private val _loginFlow = MutableStateFlow("")
    val loginFlow = _loginFlow.asStateFlow()
    fun startLogin() {
        // Handle login logic here.
        _loginFlow.value = "Login Success"
    }
//MainActivity

```
val tv = findViewById<TextView>(R.id.tv_content)
val btn = findViewById<Button>(R.id.btn)
btn.setOnClickListener {
    viewModel.startLogin()
}

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.loginFlow.collect {
            if (it.isNotBlank()) {
                Toast.makeText(this@MainActivity2, it, Toast.LENGTH_LONG).show()
            }
        }
    }
}
```
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

上記のコードは実際にクリック ログインをシミュレートし、ログインが成功したことを示すプロンプトを表示します。実際の操作の効果を見てみましょう。

フロー9.gif

水平画面と垂直画面を切り替えた後、再度ログイン成功のプロンプトが表示されるのがわかりましたか? これは、スティッキー イベントによって発生する繰り返しデータ受信の問題です。 SharedFlow を試してみましょう:

    private val _loginFlow = MutableSharedFlow<String>()

    val loginFlow = _loginFlow.asSharedFlow()
    fun startLogin() {
        // Handle login logic here.
        viewModelScope.launch {
            _loginFlow.emit("Login Success")
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

StateFlow を SharedFlow に変更しました。SharedFlow には初期値が必要なく、データを受信する場所は変更されていないことがわかります。

フロー10.gif

ここでは、SharedFlow を使用するとこのスティッキーの問題が発生しないことがわかります。実際、SharedFlow には構成できるパラメータが多数あります。

    public fun <T> MutableSharedFlow(
        // 每个新的订阅者订阅时收到的回放的数目,默认0
        replay: Int = 0,

       // 除了replay数目之外,缓存的容量,默认0
        extraBufferCapacity: Int = 0,

      // 缓存区溢出时的策略,默认为挂起。只有当至少有一个订阅者时,onBufferOverflow才会生效。当无订阅者时,只有最近replay数目的值会保存,并且onBufferOverflow无效。
        onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
    )
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

SharedFlow の使用法は他にもあり、皆さんが発見されるのを待っていますが、ここでは詳しく説明しません。

その他の一般的なアプリケーション シナリオ

前回は、基本的なコールド フローからホット フローまで、また StateFlow と SharedFlow の一般的な使用法と適用可能なシナリオを紹介しました。次に、フローの他の一般的なアプリケーション シナリオをいくつかの実際的な例に焦点を当てて説明します。

複雑で時間のかかるロジックを処理する

通常、時間のかかる複雑なロジックを実行し、それをサブスレッドで処理してから、メイン スレッドに切り替えて UI を表示します。Flow はスレッド切り替えもサポートしており、flowOn は以前の操作を対応するサブスレッドに入れて処理することができます。 。
ローカル読み取りを実装しますAssetsディレクトリの下にあるperson.jsonファイルを作成して解析し、jsonファイルの内容:

{
  "name": "ddup",
  "age": 101,
  "interest": "earn money..."
}
  • 1
  • 2
  • 3
  • 4
  • 5

次に、ファイルを解析します。

fun getAssetJsonInfo(context: Context, fileName: String): String {
    val strBuilder = StringBuilder()
    var input: InputStream? = null
    var inputReader: InputStreamReader? = null
    var reader: BufferedReader? = null
    try {
        input = context.assets.open(fileName, AssetManager.ACCESS_BUFFER)
        inputReader = InputStreamReader(input, StandardCharsets.UTF_8)
        reader = BufferedReader(inputReader)
        var line: String?
        while ((reader.readLine().also { line = it }) != null) {
            strBuilder.append(line)
        }
    } catch (ex: Exception) {
        ex.printStackTrace()
    } finally {
        try {
            input?.close()
            inputReader?.close()
            reader?.close()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }
    return strBuilder.toString()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

フローは次のファイルを読み取ります。

/**
 * 通过Flow方式,获取本地文件
 */
private fun getFileInfo() {
    lifecycleScope.launch {
        flow {
            //解析本地json文件,并生成对应字符串
            val configStr = getAssetJsonInfo(this@MainActivity2, "person.json")
            //最后将得到的实体类发送到下游
            emit(configStr)
        }
            .map { json ->
                Gson().fromJson(json, PersonModel::class.java) //通过Gson将字符串转为实体类
            }
            .flowOn(Dispatchers.IO) //在flowOn之上的所有操作都是在IO线程中进行的
            .onStart { Log.d("ddup", "onStart") }
            .filterNotNull()
            .onCompletion { Log.d("ddup", "onCompletion") }
            .catch { ex -> Log.d("ddup", "catch:${ex.message}") }
            .collect {
                Log.d("ddup", "collect parse result:$it")
            }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

最終的な印刷ログ:

2024-07-09 22:00:34.006 12251-12251 ddup com.ddup.flowtest D onStart 2024-07-09 22:00:34.018 12251-12251 ddup com.ddup.flowtest D collect parse result:PersonModel(name=ddup, age=101, interest=earn money...) 2024-07-09 22:00:34.019 12251-12251 ddup com.ddup.flowtest D onCompletion

依存関係のあるインターフェースリクエスト

別のリクエストの結果に依存するインターフェイス リクエスト (いわゆるネストされたリクエスト) が頻繁に発生します。ネストされたリクエストが多すぎると、同様の要件を実装するために FLow が発生します。

lifecycleScope.launch {
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        //将两个flow串联起来 先搜索目的地,然后到达目的地
        viewModel.getTokenFlows()
            .flatMapConcat {
                //第二个flow依赖第一个的结果
                viewModel.getUserFlows(it)
            }.collect {
                tv.text = it ?: "error"
            }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

複数のインターフェースからのデータを結合する

複数のインターフェイスからのデータを組み合わせるシナリオは何ですか? たとえば、複数のインターフェイスをリクエストし、それらの結果を組み合わせてそれらを均一に表示したり、別のインターフェイスのリクエスト パラメーターとして使用したりする方法を教えてください。
1 つ目は、1 つずつリクエストしてからそれらをマージすることです。
2 番目のタイプは、同時にリクエストを行ってから、すべてのリクエストをマージするものです。
明らかに、2 番目の効果の方が効率的です。コードを見てみましょう。

//分别请求电费、水费、网费,Flow之间是并行关系
suspend fun requestElectricCost(): Flow<SpendModel> =
    flow {
        delay(500)
        emit(SpendModel("电费", 10f, 500))
    }.flowOn(Dispatchers.IO)

suspend fun requestWaterCost(): Flow<SpendModel> =
    flow {
        delay(1000)
        emit(SpendModel("水费", 20f, 1000))
    }.flowOn(Dispatchers.IO)

suspend fun requestInternetCost(): Flow<SpendModel> =
    flow {
        delay(2000)
        emit(SpendModel("网费", 30f, 2000))
    }.flowOn(Dispatchers.IO)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

まず、ViewModel でいくつかのネットワーク リクエストをシミュレートして定義し、それらのリクエストをマージしました。

lifecycleScope.launch {
    val electricFlow = viewModel.requestElectricCost()
    val waterFlow = viewModel.requestWaterCost()
    val internetFlow = viewModel.requestInternetCost()

    val builder = StringBuilder()
    var totalCost = 0f
    val startTime = System.currentTimeMillis()
    //NOTE:注意这里可以多个zip操作符来合并Flow,且多个Flow之间是并行关系
    electricFlow.zip(waterFlow) { electric, water ->
        totalCost = electric.cost + water.cost
        builder.append("${electric.info()},n").append("${water.info()},n")
    }.zip(internetFlow) { two, internet ->
        totalCost += internet.cost
        two.append(internet.info()).append(",nn总花费:$totalCost")
    }.collect {
        tv.text = it.append(",总耗时:${System.currentTimeMillis() - startTime} ms")
        Log.d(
            "ddup",
            "${it.append(",总耗时:${System.currentTimeMillis() - startTime} ms")}"
        )
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

操作結果:
フロー11.png
費やした合計時間は、基本的に最長リクエスト時間と同じであることがわかります。

フロー利用時の注意点

複数Flow一つに入れることはできないlifecycleScope.launch中に入るcollect{}、入っているのでcollect{}無限ループと同じで、次のコード行は実行されません。lifecycleScope.launch{}中に入って、中から再びオンにできますlaunch{}サブコルーチンが実行されます。
エラーの例:

lifecycleScope.launch {
    flow1
        .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
        .collect {}

   flow2
        .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
        .collect {}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

正しい書き方:

lifecycleScope.launch {
    launch {
       flow1
            .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
            .collect {}
    }

    launch {
      flow2
            .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
            .collect {}
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

要約する

フローのライフサイクルから、リソースの無駄を避けるためのフローの正しい利用姿勢、通常のコールドフローからホットフローへの変換、LiveDataに代わるStateFlowとそのスティッキー問題を紹介し、スティッキー問題を解決しました。 SharedFlow、次に一般的なアプリケーション シナリオ、最後にフローを使用する際の注意事項について、基本的にフローのほとんどの機能とアプリケーション シナリオをカバーします。これはフロー学習の最終章でもあります。
作るのは簡単じゃないけど、いいねするのはめんどくさいいいね、集めてコメントして励みにしましょう
参考記事
Kotlin Flow リアクティブ プログラミング、StateFlow、SharedFlow

Kotlin | Flow データフローのいくつかの使用シナリオ