Technologieaustausch

Kotlin Flow Study Guide (3) Letztes Kapitel

2024-07-12

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

Vorwort

In den ersten beiden Artikeln wurde erläutert, was Flow ist, wie man es verwendet und welche Fortschritte die Bediener machen. Im nächsten Artikel wird hauptsächlich die Verwendung von Flow in tatsächlichen Projekten vorgestellt.

Flow-Lebenszyklus

Bevor wir die tatsächlichen Anwendungsszenarien von Flow vorstellen, schauen wir uns zunächst das im ersten Artikel von Flow vorgestellte Timer-Beispiel an. Wir haben einen timeFlow-Datenfluss in ViewModel definiert:

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

Empfangen Sie dann in der Aktivität den zuvor definierten Datenstrom.

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

Lassen Sie es mich ausführen, um den tatsächlichen Effekt zu sehen:

flow1.gif

Ist Ihnen aufgefallen, dass das Protokoll weiterhin gedruckt wird, wenn die App in den Hintergrund wechselt? Ändern wir den Empfangscode:

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

Lassen Sie uns die Methode zum Starten der Coroutine von „launch“ in „launchWhenStarted“ ändern und sie erneut ausführen, um den Effekt zu sehen:

flow2.gif

Wir können sehen, dass das Protokoll nicht mehr gedruckt wird, wenn auf die Schaltfläche „HOME“ geklickt wird. Es ist ersichtlich, dass die Änderung wirksam wurde. Wurde der Stream jedoch abgebrochen? ein Blick:

flow3.gif

Wenn wir in den Vordergrund wechseln, können wir sehen, dass der Zähler nicht bei 0 beginnt, also den Empfang nicht abbricht, sondern nur den Datenempfang im Hintergrund unterbricht. Tatsächlich behält die Flow-Pipeline die vorherigen Daten bei Die API wurde stattdessen aufgegeben und Google RepeatOnLifecycle wird eher empfohlen, und es besteht nicht das Problem, dass alte Daten in der Pipeline verbleiben.
Versuchen wir, den entsprechenden Code umzuwandeln:

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

Führen Sie den Vorgang erneut aus, um den Effekt zu sehen:

flow4.gif

Wir können sehen, dass beim Wechsel vom Hintergrund in den Vordergrund die Daten wieder bei 0 beginnen, was bedeutet, dass Flow beim Wechsel in den Hintergrund die Arbeit abbricht und alle Originaldaten gelöscht werden.

Wir verwenden Flow und können durch repeatOnLifecycle die Sicherheit unseres Programms besser gewährleisten.

StateFlow ersetzt LiveData

Die vorherigen Einführungen sind alle Beispiele für den Kaltfluss. Als Nächstes stellen wir einige gängige Anwendungsszenarien des Heißflusses vor.
Nehmen wir weiterhin das vorherige Timer-Beispiel. Was passiert, wenn der Bildschirm zwischen horizontalen und vertikalen Bildschirmen umgeschaltet wird?

flow5.gif

Wir können sehen, dass nach dem Wechsel zwischen horizontalen und vertikalen Bildschirmen die Aktivität neu erstellt wird. Nach der Neuerstellung wird timeFlow erneut erfasst, der Kaltfluss wird erneut erfasst und erneut ausgeführt, und dann wird der Timer gestartet Zählen von 0. Wir möchten oft zwischen horizontalen und vertikalen Bildschirmen wechseln. Zu diesem Zeitpunkt hoffen wir, dass der Status der Seite zumindest innerhalb einer bestimmten Zeitspanne unverändert bleibt Flow und versuchen:

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

Hier konzentrieren wir uns auf die drei Parameter in stateIn. Der erste ist der Umfang der Coroutine, der zweite ist die maximale effektive Zeit, die der Fluss benötigt, um seinen Arbeitszustand aufrechtzuerhalten Parameter ist der Anfangswert.

Führen Sie den Vorgang erneut aus, um den Effekt zu sehen:

flow6.gif

Hier sehen wir das gedruckte Protokoll nach dem Wechsel zwischen horizontalem und vertikalem Bildschirm. Der Timer startet nicht bei 0.
Wir haben oben erklärt, wie man einen kalten Fluss in einen heißen Fluss umwandelt. Wir haben noch nicht vorgestellt, wie stateFlow LiveData ersetzen kann.

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

Wir definieren einen StateFlow-Hot-Flow und ändern dann den StateFlow-Wert über eine startTimer()-Methode, ähnlich wie bei LiveData setData. Wenn Sie auf die Schaltfläche klicken, beginnen Sie mit der Änderung des StateFlow-Werts und sammeln Sie die Werte des entsprechenden Flows, ähnlich wie bei LiveData Observe-Methode zur Überwachung von Datenänderungen.
Werfen wir einen Blick auf den tatsächlichen Laufeffekt:

flow7.gif

An dieser Stelle haben wir die grundlegende Verwendung von StateFlow eingeführt und stellen nun SharedFlow vor.

Gemeinsam genutzter Fluss

Um SharedFlow zu verstehen, kennen wir zunächst das Konzept der Sticky-Ereignisse. Wenn ein Beobachter eine Datenquelle abonniert und die Datenquelle bereits über die neuesten Daten verfügt, werden die Daten sofort an den Beobachter übertragen. Der obigen Erklärung nach zu urteilen, entspricht LiveData dieser klebrigen Eigenschaft. Was ist mit StateFlow? Schreiben wir zur Verifizierung eine einfache Demo:


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

Wir definieren zunächst einen ClickCountFlow im MainViewModel, ändern dann in der Aktivität die ClickCountFlow-Daten, indem wir auf die Schaltfläche klicken, empfangen dann den ClickCountFlow und zeigen die Daten im Text an.
Werfen wir einen Blick auf den Laufeffekt:

flow8.gif

Wir können sehen, dass beim Wechseln zwischen horizontalen und vertikalen Bildschirmen die Aktivität neu erstellt und clickCountFlow erneut erfasst wird. Die Daten beginnen immer noch bei den vorherigen 4, was darauf hinweist, dass StateFlow hier hängen bleibt, aber lassen Sie uns Schauen Sie sich ein anderes Beispiel an. Wir simulieren ein Click-to-Login-Szenario. Klicken Sie auf die Login-Schaltfläche, um sich anzumelden und anzumelden:

//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

Der obige Code simuliert tatsächlich eine Klick-Anmeldung und zeigt dann an, dass die Anmeldung erfolgreich ist. Schauen wir uns den tatsächlichen Vorgangseffekt an:

flow9.gif

Haben Sie gesehen, dass nach dem Wechsel zwischen horizontalen und vertikalen Bildschirmen die Aufforderung zur erfolgreichen Anmeldung erneut angezeigt wird? Dies ist das Problem des wiederholten Datenempfangs, der durch Sticky-Ereignisse verursacht wird SharedFlow und probieren Sie es aus:

    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

Wir haben StateFlow in SharedFlow geändert. Wir können sehen, dass für SharedFlow kein Anfangswert erforderlich ist. Die Emit-Methode wird zum Anmeldeort hinzugefügt, um Daten zu senden. Führen Sie sie erneut aus, um den Effekt zu sehen.

flow10.gif

Hier können wir sehen, dass dieses Klebrigkeitsproblem bei Verwendung von SharedFlow nicht auftritt. Tatsächlich verfügt SharedFlow über viele Parameter, die konfiguriert werden können:

    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

Es gibt noch weitere Einsatzmöglichkeiten von SharedFlow, die jeder entdecken kann, aber ich werde hier nicht auf Details eingehen.

Weitere gängige Anwendungsszenarien

Zuvor haben wir den grundlegenden Kaltfluss in den Heißfluss sowie die allgemeine Verwendung und die anwendbaren Szenarien von StateFlow und SharedFlow eingeführt. Als nächstes werden wir uns auf mehrere praktische Beispiele konzentrieren, um andere gängige Anwendungsszenarien des Flusses zu betrachten.

Behandeln Sie komplexe, zeitaufwändige Logik

Normalerweise führen wir eine komplexe zeitaufwändige Logik aus, verarbeiten sie in einem Unterthread und wechseln dann zum Hauptthread, um die Benutzeroberfläche anzuzeigen. Flow unterstützt auch den Thread-Wechsel, und flowOn kann vorherige Vorgänge zur Verarbeitung in den entsprechenden Unterthread stellen .
Wir implementieren einen Read LocalAssetsunter dem Verzeichnisperson.jsonDatei ablegen und analysieren,jsonInhalt der Datei:

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

Analysieren Sie dann die Datei:

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 liest Dateien:

/**
 * 通过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

Endgültiges Druckprotokoll:

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

Schnittstellenanfragen mit Abhängigkeiten

Wir stoßen oft auf Schnittstellenanfragen, die von den Ergebnissen einer anderen Anfrage abhängen, also sogenannte verschachtelte Anfragen. Wenn es zu viele verschachtelte Anfragen gibt, kommt es zur Rückrufhölle. Wir verwenden FLow, um eine ähnliche Anforderung zu implementieren:

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

Kombinieren Sie Daten von mehreren Schnittstellen

Was ist das Szenario der Kombination von Daten von mehreren Schnittstellen? Wir fordern beispielsweise mehrere Schnittstellen an und kombinieren dann deren Ergebnisse, um sie einheitlich anzuzeigen oder sie als Anforderungsparameter für eine andere Schnittstelle zu verwenden.
Die erste Möglichkeit besteht darin, sie einzeln anzufordern und dann zusammenzuführen.
Der zweite Typ besteht darin, gleichzeitige Anfragen zu stellen und dann alle Anfragen zusammenzuführen.
Offensichtlich ist der zweite Effekt effizienter. Schauen wir uns den Code an:

//分别请求电费、水费、网费,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

Zuerst haben wir mehrere Netzwerkanfragen in ViewModel simuliert und definiert und dann die Anfragen zusammengeführt:

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

Operationsergebnis:
flow11.png
Wir sehen, dass die insgesamt aufgewendete Zeit im Grunde mit der längsten Anfragezeit übereinstimmt.

Vorsichtsmaßnahmen für die Verwendung von Flow

MehrereFlowkann nicht in einem platziert werdenlifecycleScope.launchGehen hineincollect{}, weil betretencollect{}Äquivalent zu einer Endlosschleife wird die nächste Codezeile nie ausgeführt, wenn Sie nur eine schreiben möchtenlifecycleScope.launch{}Gehen Sie hinein, Sie können es von innen wieder einschaltenlaunch{}Die Sub-Coroutine wird ausgeführt.
Fehlerbeispiel:

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

Richtige Schreibweise:

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

Zusammenfassen

Vom Lebenszyklus von Flow haben wir die korrekte Nutzungshaltung von Flow eingeführt, um Ressourcenverschwendung zu vermeiden, über die Umwandlung von gewöhnlichem Kaltfluss in Heißfluss bis hin zu StateFlow, der LiveData und dessen Klebrigkeitsproblem ersetzt, und dann das Klebrigkeitsproblem durch gelöst SharedFlow, dann allgemeine Anwendungsszenarien und schließlich die Vorsichtsmaßnahmen für die Verwendung von Flow, decken im Wesentlichen die meisten Funktionen und Anwendungsszenarien von Flow ab. Dies ist auch das letzte Kapitel des Flow-Lernens.
Es ist nicht einfach, es zu erstellen, aber es ist schwierig, es zu mögenLiken, sammeln und kommentieren Sie, um zu ermutigen
Referenzartikel
Kotlin Flow reaktive Programmierung, StateFlow und SharedFlow

Kotlin |. Mehrere Nutzungsszenarien des Flow-Datenflusses