Compartilhamento de tecnologia

Guia de estudo do fluxo kotlin (3) Capítulo final

2024-07-12

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

Prefácio

Os dois primeiros artigos apresentaram o que é Flow, como usá-lo e os avanços relacionados ao operador. O próximo artigo apresenta principalmente o uso do Flow em projetos reais.

Ciclo de vida do fluxo

Antes de apresentar os cenários reais de aplicação do Flow, vamos primeiro revisar o exemplo de temporizador apresentado no primeiro artigo do Flow. Definimos um fluxo de dados timeFlow no 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

Em seguida, na Activity, receba o fluxo de dados definido anteriormente.

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

Deixe-me executá-lo para ver o efeito real:

fluxo1.gif

Você notou que quando o App muda para segundo plano, o log ainda está sendo impresso. Isso não é um desperdício de recursos. Vamos modificar o código de recebimento:

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

Vamos alterar o método de inicialização da corrotina de launch para launchWhenStarted e executá-la novamente para ver o efeito:

fluxo2.gif

Podemos observar que quando o botão HOME é clicado e retornado ao fundo, o log não é mais impresso. Percebe-se que a alteração entrou em vigor, mas o stream foi cancelado. Vamos voltar para a recepção para pegar? um olhar:

fluxo3.gif

Mudando para o primeiro plano, podemos ver que o contador não inicia em 0, então na verdade ele não cancela a recepção, apenas pausa o recebimento de dados em segundo plano. O pipeline do Flow ainda retém os dados anteriores. A API foi abandonada. O Google RepeatOnLifecycle é mais recomendado e não tem o problema de reter dados antigos no pipeline.
Vamos tentar transformar o código correspondente:

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

Execute novamente para ver o efeito:

fluxo4.gif

Podemos observar que ao passar do fundo para o primeiro plano, os dados recomeçam do 0, o que significa que ao passar para o fundo, o Flow cancela o trabalho e todos os dados originais são apagados.

Estamos usando o Flow e, por meio do repeatOnLifecycle, podemos garantir melhor a segurança do nosso programa.

StateFlow substitui LiveData

As introduções anteriores são exemplos de fluxo frio. A seguir, apresentaremos alguns cenários de aplicação comuns de fluxo quente.
Ainda usando o exemplo anterior do temporizador, o que acontecerá se a tela for alternada entre telas horizontais e verticais?

fluxo5.gif

Podemos ver que após alternar entre as telas horizontais e verticais, a atividade é recriada. Após a recriação, o timeFlow será coletado novamente, o fluxo frio será coletado novamente e executado novamente e então o cronômetro será iniciado. contando a partir de 0. Muitas vezes, queremos alternar entre telas horizontais e verticais. Neste momento, esperamos que o status da página permaneça inalterado, pelo menos dentro de um determinado período de tempo. Aqui modificamos o fluxo frio para um. fluxo quente e tente:

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

Aqui nos concentramos nos três parâmetros em stateIn. O primeiro é o escopo da corrotina, o segundo é o tempo efetivo máximo para o fluxo manter seu estado de funcionamento. Se exceder o fluxo, ele irá parar de funcionar. parâmetro é o valor inicial.

Execute novamente para ver o efeito:

fluxo6.gif

Aqui podemos ver o log impresso após alternar entre as telas horizontal e vertical. O cronômetro não iniciará em 0.
Apresentamos acima como transformar um fluxo frio em um fluxo quente. Ainda não apresentamos como o stateFlow pode substituir o LiveData. Aqui está uma introdução sobre como o stateFlow substitui o 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 um fluxo quente StateFlow e, em seguida, alteramos o valor stateFlow por meio de um método startTimer(), semelhante ao LiveData setData. Quando o botão é clicado, começamos a alterar o valor StateFlow e coletamos os valores do fluxo correspondente, semelhante ao. Método LiveData Observe para monitorar alterações de dados.
Vamos dar uma olhada no efeito real da corrida:

fluxo7.gif

Neste ponto, apresentamos o uso básico do StateFlow e agora apresentaremos o SharedFlow.

Fluxo compartilhado

Para entender o SharedFlow, primeiro conhecemos o conceito de eventos persistentes. Literalmente, quando um observador se inscreve em uma fonte de dados, se a fonte de dados já tiver os dados mais recentes, os dados serão enviados ao observador imediatamente. A julgar pela explicação acima, o LiveData está em conformidade com essa característica fixa. E quanto ao StateFlow? Vamos escrever uma demonstração simples 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

Primeiro definimos um clickCountFlow no MainViewModel, depois na Activity, alteramos os dados do clickCountFlow clicando no Button, e então recebemos o clickCountFlow e exibimos os dados no texto.
Vamos dar uma olhada no efeito de corrida:

fluxo8.gif

Podemos ver que ao alternar entre telas horizontais e verticais, a atividade é recriada e o clickCountFlow é coletado novamente. Os dados ainda começam a partir dos 4 anteriores, indicando que o StateFlow está fixo. Parece não haver problema aqui, mas vamos. veja outro exemplo. Simulamos um cenário de clique para fazer login, clique no botão de login para fazer login e fazer login:

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

O código acima simula um login de clique e, em seguida, avisa que o login foi bem-sucedido. Vamos dar uma olhada no efeito real da operação:

fluxo9.gif

Você viu que depois de alternar entre as telas horizontal e vertical, o prompt de login bem-sucedido aparece novamente. Não passamos pelo processo de novo login. Este é o problema de recepção repetida de dados causada por eventos persistentes. SharedFlow e experimente:

    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

Mudamos StateFlow para SharedFlow. Podemos ver que SharedFlow não requer um valor inicial. O método emit é adicionado ao local de login para enviar dados, e o local onde os dados são recebidos permanece inalterado.

fluxo10.gif

Aqui podemos ver que o uso do SharedFlow não causará esse problema de aderência. Na verdade, o SharedFlow possui muitos parâmetros que podem ser configurados:

    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

Existem mais usos do SharedFlow esperando que todos descubram, mas não entrarei em detalhes aqui.

Outros cenários de aplicação comuns

Anteriormente, apresentamos o fluxo frio básico ao fluxo quente, bem como o uso comum e os cenários aplicáveis ​​de StateFlow e SharedFlow. A seguir, nos concentraremos em vários exemplos práticos para examinar outros cenários de aplicação comuns de fluxo.

Lide com lógica complexa e demorada

Geralmente fazemos alguma lógica complexa e demorada, processamos em um subthread e, em seguida, mudamos para o thread principal para exibir a IU. O Flow também oferece suporte à alternância de threads e o flowOn pode colocar operações anteriores no subthread correspondente para processamento. .
Implementamos um local de leituraAssetssob o diretórioperson.jsonarquivo e analisá-lo,jsonConteúdo do arquivo:

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

Em seguida, analise o arquivo:

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

O fluxo lê arquivos:

/**
 * 通过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 final de impressão:

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

Solicitações de interface com dependências

Freqüentemente encontramos solicitações de interface que dependem dos resultados de outra solicitação, que são as chamadas solicitações aninhadas. Se houver muitas solicitações aninhadas, ocorrerá um inferno de retorno de chamada.

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 dados de múltiplas interfaces

Qual é o cenário de combinar dados de múltiplas interfaces? Por exemplo, solicitamos múltiplas interfaces e, em seguida, combinamos seus resultados para exibi-los uniformemente ou usamos-os como parâmetros de solicitação para outra interface.
A primeira é solicitar um por um e depois mesclá-los;
O segundo tipo é solicitar simultaneamente e depois mesclar todas as solicitações.
Obviamente, o segundo efeito é mais eficiente. Vejamos o 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

Primeiro, simulamos e definimos diversas solicitações de rede no ViewModel e depois mesclamos as solicitações:

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 da operação:
fluxo11.png
Vemos que o tempo total gasto é basicamente igual ao tempo de solicitação mais longo.

Precauções ao usar o Flow

múltiploFlownão pode ser colocado em umlifecycleScope.launchIr para dentrocollect{}, porque entrandocollect{}Equivalente a um loop infinito, a próxima linha de código nunca será executada se você quiser escrever um;lifecycleScope.launch{}Entre, você pode ligá-lo novamente por dentrolaunch{}A sub-rotina é executada.
Exemplo de erro:

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

Maneira correta de escrever:

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

Do ciclo de vida do Flow, introduzimos a postura correta de uso do fluxo para evitar o desperdício de recursos, até a conversão do fluxo frio comum em fluxo quente, até o StateFlow substituir o LiveData e seu problema de aderência, e então resolvemos o problema de aderência por meio de SharedFlow e, em seguida, cenários de aplicativos comuns e, finalmente, as precauções para usar o Flow, cobrem basicamente a maioria dos recursos e cenários de aplicativos do Flow. Este também é o capítulo final do aprendizado do Flow.
Não é fácil de criar, mas é difícil gostarCurta, colete e comente para incentivar
Artigo de referência
Programação reativa Kotlin Flow, StateFlow e SharedFlow

Kotlin | Vários cenários de uso do fluxo de dados Flow