Condivisione della tecnologia

Guida allo studio del flusso kotlin (3) Capitolo finale

2024-07-12

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

Prefazione

I primi due articoli hanno introdotto cos'è Flow, come usarlo e i relativi progressi dell'operatore. L'articolo successivo introduce principalmente l'uso di Flow nei progetti reali.

Ciclo di vita del flusso

Prima di introdurre gli scenari applicativi reali di Flow, esaminiamo innanzitutto l'esempio del timer introdotto nel primo articolo di Flow. Abbiamo definito un flusso di dati timeFlow in ViewModel:

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

Quindi nell'Attività, ricevi il flusso di dati definito in precedenza.

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

Lasciamelo eseguire per vedere l'effetto reale:

flusso1.gif

Hai notato che quando l'App passa in background, il log continua a essere stampato? Questo non è uno spreco di risorse. Modifichiamo il codice di ricezione:

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

Cambiamo il metodo di avvio della coroutine da launch a launchWhenStarted ed eseguiamolo di nuovo per vedere l'effetto:

flusso2.gif

Possiamo vedere che quando si fa clic sul pulsante HOME e si torna in background, il registro non viene più stampato. Si può vedere che la modifica ha avuto effetto, ma lo streaming è stato annullato? Torniamo alla reception per riprenderlo uno sguardo:

flusso3.gif

Passando in primo piano, possiamo vedere che il contatore non parte da 0, quindi di fatto non annulla la ricezione, ma semplicemente mette in pausa la ricezione dei dati in background. La pipeline Flow conserva infatti i dati precedenti L'API è stata invece abbandonata. Google RepeatOnLifecycle è invece più consigliato e non ha il problema di conservare i vecchi dati in pipeline.
Proviamo a trasformare il codice corrispondente:

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

Riesegui per vedere l'effetto:

flusso4.gif

Possiamo vedere che quando si passa dallo sfondo al primo piano, i dati ripartono da 0, il che significa che quando si passa allo sfondo, Flow annulla il lavoro e tutti i dati originali vengono cancellati.

Stiamo utilizzando Flow e, tramite RepeatOnLifecycle, possiamo garantire meglio la sicurezza del nostro programma.

StateFlow sostituisce LiveData

Le introduzioni precedenti sono tutti esempi di flusso a freddo. Successivamente, introdurremo alcuni scenari applicativi comuni di flusso a caldo.
Sempre utilizzando l'esempio precedente del timer, cosa accadrà se lo schermo viene commutato tra lo schermo orizzontale e quello verticale?

flusso5.gif

Possiamo vedere che dopo il passaggio tra le schermate orizzontale e verticale, l'attività viene ricreata Dopo la ricreazione, timeFlow verrà raccolto nuovamente, il flusso freddo verrà nuovamente raccolto e rieseguito, quindi verrà avviato il timer. contando da 0. Molte volte, vogliamo passare dalla schermata orizzontale a quella verticale. In questo momento, speriamo che lo stato della pagina rimanga invariato, almeno entro un certo periodo di tempo. Qui modifichiamo il flusso freddo in a flusso caldo e prova:

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

Qui ci concentriamo sui tre parametri in stateIn. Il primo è l'ambito della coroutine, il secondo è il tempo massimo effettivo affinché il flusso mantenga il suo stato di funzionamento. Se supera il flusso, smetterà di funzionare il parametro è il valore iniziale.

Riesegui per vedere l'effetto:

flusso6.gif

Qui possiamo vedere il registro stampato dopo il passaggio dalla schermata orizzontale a quella verticale. Il timer non partirà da 0.
In precedenza abbiamo introdotto come modificare un flusso freddo in un flusso caldo. Non abbiamo ancora introdotto come stateFlow possa sostituire LiveData. Ecco un'introduzione su come stateFlow sostituisce 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

Definiamo un flusso caldo StateFlow, quindi modifichiamo il valore stateFlow tramite un metodo startTimer(), simile a LiveData setData. Quando si fa clic sul pulsante, iniziamo a modificare il valore StateFlow e raccogliamo i valori del flusso corrispondente, simile al Metodo LiveData Observe per monitorare le modifiche dei dati.
Diamo un’occhiata all’effettivo effetto di corsa:

flusso7.gif

A questo punto abbiamo introdotto l'utilizzo di base di StateFlow e ora introdurremo SharedFlow.

Flusso condiviso

Per comprendere SharedFlow, conosciamo innanzitutto il concetto di eventi persistenti. Letteralmente, quando un osservatore si iscrive a un'origine dati, se l'origine dati dispone già dei dati più recenti, i dati verranno immediatamente inviati all'osservatore. A giudicare dalla spiegazione di cui sopra, LiveData è conforme a questa caratteristica appiccicosa. Che dire di StateFlow? Scriviamo una semplice demo per verificare:


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

Per prima cosa definiamo un clickCountFlow nel MainViewModel, quindi nell'Attività, modifichiamo i dati clickCountFlow facendo clic sul pulsante, quindi riceviamo il clickCountFlow e visualizziamo i dati nel testo.
Diamo un’occhiata all’effetto corsa:

flusso8.gif

Possiamo vedere che quando si passa dalla schermata orizzontale a quella verticale, l'attività viene ricreata e clickCountFlow viene nuovamente raccolto. I dati iniziano ancora dai 4 precedenti, indicando che StateFlow è bloccato. Sembra che non ci siano problemi qui, ma procediamo guarda un altro esempio Simuliamo uno scenario click-to-login, fai clic sul pulsante di accesso per accedere ed effettuare l'accesso:

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

Il codice sopra simula effettivamente un login tramite clic e quindi richiede che l'accesso abbia avuto esito positivo. Diamo un'occhiata all'effetto effettivo dell'operazione:

flusso9.gif

Hai visto che dopo il passaggio dalla schermata orizzontale a quella verticale, viene nuovamente visualizzata la richiesta di accesso riuscito? Non abbiamo eseguito il processo di nuovo accesso Questo è il problema della ricezione ripetuta dei dati causata da eventi persistenti SharedFlow e provalo:

    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

Abbiamo cambiato StateFlow in SharedFlow Possiamo vedere che SharedFlow non richiede un valore iniziale. Il metodo emit viene aggiunto al luogo di accesso per inviare i dati e il luogo in cui vengono ricevuti i dati rimane invariato.

flusso10.gif

Qui possiamo vedere che l'utilizzo di SharedFlow non causerà questo problema di viscosità. Infatti, SharedFlow ha molti parametri che possono essere configurati:

    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

Ci sono altri usi di SharedFlow che aspettano di essere scoperti da tutti, ma non entrerò nei dettagli qui.

Altri scenari applicativi comuni

In precedenza, abbiamo introdotto il flusso da freddo a caldo di base, nonché l'utilizzo comune e gli scenari applicabili di StateFlow e SharedFlow. Successivamente, ci concentreremo su diversi esempi pratici per esaminare altri scenari applicativi comuni del flusso.

Gestire logiche complesse e dispendiose in termini di tempo

Di solito eseguiamo una logica complessa che richiede tempo, la elaboriamo in un thread secondario e quindi passiamo al thread principale per visualizzare l'interfaccia utente. Flow supporta anche il cambio di thread e flowOn può inserire le operazioni precedenti nel thread secondario corrispondente per l'elaborazione .
Implementiamo una lettura localeAssetssotto la directoryperson.jsonarchiviarlo e analizzarlo,jsonContenuto del file:

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

Quindi analizzare il file:

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

Il flusso legge i file:

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

Registro di stampa finale:

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

Richieste di interfaccia con dipendenze

Spesso incontriamo richieste di interfaccia che dipendono dai risultati di un'altra richiesta, le cosiddette richieste annidate. Se ci sono troppe richieste annidate, si verificherà l'inferno del callback. Usiamo FLow per implementare un requisito simile:

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

Combina dati da più interfacce

Qual è lo scenario di combinazione dei dati da più interfacce? Ad esempio, richiediamo più interfacce e quindi combiniamo i risultati per visualizzarli in modo uniforme o utilizzarli come parametri di richiesta per un'altra interfaccia.
Il primo è richiederli uno per uno e poi unirli;
Il secondo tipo consiste nel richiedere contemporaneamente e quindi unire tutte le richieste.
Ovviamente il secondo effetto è più efficace. Diamo un’occhiata al codice:

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

Innanzitutto, abbiamo simulato e definito diverse richieste di rete in ViewModel, quindi abbiamo unito le richieste:

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

risultato dell'operazione:
flusso11.png
Vediamo che il tempo totale impiegato è sostanzialmente uguale al tempo di richiesta più lungo.

Precauzioni per l'utilizzo di Flow

multiploFlownon può essere inserito in unolifecycleScope.launchAndare all'internocollect{}, perché entrandocollect{}Equivalente a un ciclo infinito, la riga di codice successiva non verrà mai eseguita se si desidera scrivere alifecycleScope.launch{}Entra, puoi riaccenderlo dall'internolaunch{}Viene eseguita la sub-coroutine.
Esempio di errore:

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

Modo corretto di scrivere:

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

Riassumere

Dal ciclo di vita di Flow, abbiamo introdotto la corretta postura di utilizzo del flusso per evitare lo spreco di risorse, alla conversione del normale flusso freddo in flusso caldo, alla sostituzione di StateFlow LiveData e al suo problema di vischiosità, quindi abbiamo risolto il problema di vischiosità attraverso SharedFlow, quindi gli scenari applicativi comuni e infine le precauzioni per l'utilizzo di Flow, coprono sostanzialmente la maggior parte delle funzionalità e degli scenari applicativi di Flow. Questo è anche il capitolo finale dell'apprendimento di Flow.
Non è facile da creare, ma è difficile apprezzarloMetti mi piace, raccogli e commenta per incoraggiare
Articolo di riferimento
Programmazione reattiva di Kotlin Flow, StateFlow e SharedFlow

Kotlin |. Diversi scenari di utilizzo del flusso di dati Flow