Compartir tecnología

Guía de estudio de flujo de Kotlin (3) Capítulo final

2024-07-12

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

Prefacio

Los dos primeros artículos presentaron qué es Flow, cómo usarlo y los avances de los operadores relacionados. El siguiente artículo presenta principalmente el uso de Flow en proyectos reales.

Ciclo de vida del flujo

Antes de presentar los escenarios de aplicación reales de Flow, primero revisemos el ejemplo del temporizador presentado en el primer artículo de Flow. Definimos un flujo de datos timeFlow en 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

Luego, en la Actividad, reciba el flujo de datos definido previamente.

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

Déjame ejecutarlo para ver el efecto real:

flujo1.gif

¿Ha notado que cuando la aplicación cambia a segundo plano, el registro aún se imprime? Esto no es un desperdicio de recursos. Modifiquemos el código de recepción.

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

Cambiemos el método de inicio de la rutina desde el inicio hasta el lanzamientoCuandoInicie, y ejecútelo nuevamente para ver el efecto:

flujo2.gif

Podemos ver que cuando se hace clic en el botón INICIO y se regresa al fondo, el registro ya no se imprime. Se puede ver que el cambio ha tenido efecto, pero ¿se canceló la transmisión? una mirada:

flujo3.gif

Pasando al primer plano, podemos ver que el contador no comienza desde 0, por lo que, de hecho, no cancela la recepción, simplemente detiene la recepción de datos en segundo plano. La canalización de Flow aún conserva los datos anteriores. La API se ha abandonado. En su lugar, se recomienda Google RepeatOnLifecycle y no tiene el problema de retener datos antiguos en el proceso.
Intentemos transformar el código correspondiente:

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

Vuelva a ejecutar para ver el efecto:

flujo4.gif

Podemos ver que al cambiar del fondo al primer plano, los datos comienzan desde 0 nuevamente, lo que significa que al cambiar al fondo, Flow cancela el trabajo y se borran todos los datos originales.

Estamos utilizando Flow y, a través de repetirOnLifecycle, podemos garantizar mejor la seguridad de nuestro programa.

StateFlow reemplaza a LiveData

Las introducciones anteriores son todas ejemplos de flujo en frío. A continuación, presentaremos algunos escenarios de aplicación comunes de flujo en caliente.
Siguiendo usando el ejemplo del temporizador anterior, ¿qué pasará si la pantalla cambia entre pantallas horizontales y verticales?

flujo5.gif

Podemos ver que después de cambiar entre pantallas horizontales y verticales, la Actividad se recrea. Después de la recreación, se volverá a recopilar timeFlow, se volverá a recopilar y ejecutará el flujo en frío, y luego se iniciará el temporizador. contando desde 0. Muchas veces queremos cambiar entre pantallas horizontales y verticales. En este momento, esperamos que el estado de la página permanezca sin cambios, al menos dentro de un cierto período de tiempo. Aquí cambiamos el flujo de frío a caliente. fluye y prueba:

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

Aquí nos centramos en los tres parámetros en stateIn. El primero es el alcance de la corrutina, el segundo es el tiempo máximo efectivo para que el flujo mantenga su estado de funcionamiento. Si excede el flujo, dejará de funcionar. El parámetro es el valor inicial.

Vuelva a ejecutar para ver el efecto:

flujo6.gif

Aquí podemos ver el registro impreso después de cambiar entre pantallas horizontales y verticales. El temporizador no comenzará desde 0.
Hemos presentado anteriormente cómo modificar un flujo frío a un flujo caliente. Aún no hemos presentado cómo stateFlow puede reemplazar a LiveData. Aquí hay una introducción a cómo stateFlow reemplaza a 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

Definimos un flujo activo de StateFlow y luego cambiamos el valor de stateFlow mediante un método startTimer (), similar a LiveData setData. Cuando se hace clic en el botón, comenzamos a cambiar el valor de StateFlow y recopilamos los valores del flujo correspondiente, similar a. Método LiveData Observe para monitorear los cambios de datos.
Echemos un vistazo al efecto de ejecución real:

flujo7.gif

Hasta ahora, hemos introducido el uso básico de StateFlow y ahora presentaremos SharedFlow.

Flujo compartido

Para comprender SharedFlow, primero conocemos el concepto de eventos fijos. Literalmente, cuando un observador se suscribe a una fuente de datos, si la fuente de datos ya tiene los datos más recientes, los datos se enviarán al observador inmediatamente. A juzgar por la explicación anterior, LiveData se ajusta a esta característica pegajosa. ¿Qué pasa con StateFlow? Escribamos una demostración simple para verificar:


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

Primero definimos un clickCountFlow en MainViewModel, luego en la Actividad, cambiamos los datos de clickCountFlow haciendo clic en el Botón, y luego recibimos el clickCountFlow y mostramos los datos en el texto.
Echemos un vistazo al efecto de carrera:

flujo8.gif

Podemos ver que al cambiar entre pantallas horizontales y verticales, la actividad se vuelve a crear y se vuelve a recopilar clickCountFlow. Los datos aún comienzan desde los 4 anteriores, lo que indica que StateFlow es fijo. Parece que no hay ningún problema aquí, pero sigamos. Mire otro ejemplo. Simulamos un escenario de hacer clic para iniciar sesión, haga clic en el botón de inicio de sesión para iniciar sesión e iniciar sesión:

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

El código anterior en realidad simula un inicio de sesión con un clic y luego indica que el inicio de sesión se realizó correctamente. Echemos un vistazo al efecto de la operación real:

flujo9.gif

¿Viste que después de cambiar entre pantallas horizontales y verticales, aparece nuevamente el mensaje de inicio de sesión exitoso? No realizamos el proceso de volver a iniciar sesión. Este es el problema de la recepción repetida de datos causada por eventos fijos. SharedFlow y pruébalo:

    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

Cambiamos StateFlow a SharedFlow. Podemos ver que SharedFlow no requiere un valor inicial. El método de emisión se agrega al lugar de inicio de sesión para enviar datos y el lugar donde se reciben los datos permanece sin cambios.

flujo10.gif

Aquí podemos ver que este problema de rigidez no ocurre cuando se usa SharedFlow. De hecho, SharedFlow tiene muchos parámetros que se pueden configurar:

    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

Hay más usos de SharedFlow esperando que todos los descubran, pero no entraré en detalles aquí.

Otros escenarios de aplicación comunes

Anteriormente, presentamos el flujo en frío básico al flujo en caliente, así como el uso común y los escenarios aplicables de StateFlow y SharedFlow. A continuación, nos centraremos en varios ejemplos prácticos para observar otros escenarios de aplicación de flujo comunes.

Manejar una lógica compleja y que requiere mucho tiempo

Por lo general, hacemos una lógica compleja que requiere mucho tiempo, la procesamos en un subproceso y luego cambiamos al subproceso principal para mostrar la interfaz de usuario. Flow también admite el cambio de subproceso, y flowOn puede colocar las operaciones anteriores en el subproceso correspondiente para su procesamiento. .
Implementamos una lectura local.Assetsbajo el directorioperson.jsonarchivarlo y analizarlo,jsonContenido del archivo:

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

Luego analiza el archivo:

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

El flujo lee archivos:

/**
 * 通过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 de impresión final:

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

Solicitudes de interfaz con dependencias

A menudo nos encontramos con solicitudes de interfaz que dependen de los resultados de otra solicitud, que son las llamadas solicitudes anidadas. Si hay demasiadas solicitudes anidadas, se producirá un infierno de devolución de llamada. Usamos FLow para implementar requisitos similares.

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

Combine datos de múltiples interfaces

¿Cuál es el escenario de combinar datos de múltiples interfaces? Por ejemplo, solicitamos múltiples interfaces y luego combinamos sus resultados para mostrarlos de manera uniforme o los usamos como parámetros de solicitud para otra interfaz.
La primera es solicitarlos uno por uno y luego fusionarlos;
El segundo tipo es solicitar simultáneamente y luego fusionar todas las solicitudes.
Obviamente, el segundo efecto es más eficiente. Veamos el código:

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

Primero, simulamos y definimos varias solicitudes de red en ViewModel y luego fusionamos las solicitudes:

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

resultado de la operación:
flujo11.png
Vemos que el tiempo total invertido es básicamente el mismo que el tiempo de solicitud más largo.

Precauciones al usar Flow

MúltipleFlowno se puede colocar en unolifecycleScope.launchVe adentrocollect{}, porque entrarcollect{}Equivalente a un bucle infinito, la siguiente línea de código nunca se ejecutará si solo desea escribir un;lifecycleScope.launch{}Entra, puedes encenderlo nuevamente desde adentro.launch{}Se ejecuta la subcorrutina.
Ejemplo de error:

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

Forma correcta de escribir:

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

Resumir

Desde el ciclo de vida de Flow, introdujimos la postura de uso correcta del flujo para evitar el desperdicio de recursos, hasta la conversión del flujo frío ordinario en flujo caliente, hasta que StateFlow reemplace LiveData y su problema de rigidez, y luego resolvimos el problema de adherencia a través de SharedFlow, luego a escenarios de aplicación comunes y, finalmente, las precauciones para usar Flow, básicamente cubren la mayoría de las características y escenarios de aplicación de Flow. Este es también el capítulo final del aprendizaje de Flow.
No es fácil de crear, pero es problemático que te guste.Me gusta, recopila y comenta para animar
Artículo de referencia
Programación reactiva de Kotlin Flow, StateFlow y SharedFlow

Kotlin | Varios escenarios de uso del flujo de datos Flow