As corotinas Kotlin e seu uso no Android são resumidas (quatro corotinas são usadas em combinação com Retrofit, Room, WorkManager)

Insira a descrição da imagem aqui

0 Pensando em projetar uma nova arquitetura de aplicativo Android

Já vi artigos como esse: se você redesenhasse um aplicativo, que arquitetura você usaria, quais bibliotecas de terceiros usaria, como encapsular um módulo para que outros usassem e, desde o Jetpack no Android, esses problemas parecem ter Resposta mais clara.

Alguns motivos para essas considerações:

  • Simples e robusto

O uso de todas as estruturas ou bibliotecas deve reduzir a geração do código do modelo e até atingir o código 0. Não há dúvida de que a linguagem Kotlin se tornará a primeira escolha para o desenvolvimento nativo.Por meio da segurança vazia incorporada, biblioteca de funções padrão, atributos estendidos, convenientes DSL personalizado, proxy de atributo e proxy de classe com suporte interno, inserção de método, classe de dados, etc.

Além do nível de idioma, usando algumas das classes funcionais que acompanham a API do Android (como o Lifecycle com suporte interno) e o uso dos componentes da arquitetura Jetpack (como LiveData e ViewModel), você pode passar para o sistema uma lógica de controle que não está relacionada à lógica de negócios do programa. Processamento interno, reduza seu próprio código para lidar com vazamentos de memória e ponteiros nulos.

Muitos outros componentes do Jetpack, como a biblioteca de ligação de exibição ViewBinding, a sala de banco de dados, o WorkManager de processamento de tarefas em segundo plano etc., permitem reduzir o código do modelo, fornecendo mecanismos de segurança internos para tornar nosso código mais robusto

  • Fácil de ler e de manutenção

Ao mesmo tempo em que o código é conciso, ele também precisa ser fácil de ler, o que definitivamente exige que os membros da equipe entendam as novas tecnologias relacionadas.
Ao mesmo tempo, casos de teste completos são essenciais, o que pode reduzir bastante o tempo de depuração e reduzir os riscos de segurança causados ​​pelas alterações de código.Ao mesmo tempo, o uso das próprias regras de detecção de código Lint e de detecção personalizada de Lint do Android Studio também pode melhorar ainda mais o código Manutenção.

  • Segurança da linha principal mian-safe

Esse ponto é principalmente para explicar o uso de corotinas.Além de todos saberem que isso pode reduzir o aninhamento de retorno de chamada e usar código aparentemente síncrono para escrever lógica assíncrona, a segurança do encadeamento principal também é um aspecto muito importante. Ao transformar a operação demorada em uma função de suspensão, e o gravador da função usa o Dispatcher para especificar o encadeamento usado pela função, o chamador não precisa considerar se a chamada dessa função afetará a segurança do encadeamento principal.

A seguir, o foco deste artigo, como usar corotinas combinadas com algumas bibliotecas de funções para simplificar o código, melhorar a concisão do código (o código escrito por você é reduzido) e estabilidade (a estabilidade é garantida pela lógica interna de cada biblioteca de funções, em vez de usar Controle de código).

O link completo do código do projeto do conteúdo envolvido no artigo:
git clone https://github.com/googlecodelabs/kotlin-coroutines.git
está localizado no código_do_terminal do coroutines-codelab

1 Coroutines em Sala e Retrofit

Se você não entende o Room, pode aprender primeiro. Aqui está um exemplo direto.

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)

Ao adicionar uma suspendpalavra - chave na frente da função , o Room fornecerá segurança de thread principal, principalmente main-safe, e a executará automaticamente em um thread de segundo plano.Claro, essa função só pode ser chamada em uma rotina neste momento.

Não, o uso de corotinas no Room é tão simples.

Sobre o Retrofit, todos devem estar familiarizados com ele e dar exemplos diretos.

// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String

interface MainNetwork {
   @GET("next_title.json")
   suspend fun fetchNextTitle(): String
}

Além de adicionar uma suspendpalavra - chave à função na interface , para o formato de valor de retorno da função, o resultado original agrupado por chamada é alterado para um tipo de resultado direto, assim como String é retornado acima, é claro, também pode ser seu Json personalizado Classe de dados.

O código antes da transformação pode ser assim:

suspend fun refreshTitle() {
   // interact with *blocking* network and IO calls from a coroutine
   withContext(Dispatchers.IO) {
       val result = try {
           // Make network request using a blocking call
           network.fetchNextTitle().execute()
       } catch (cause: Throwable) {
           // If the network throws an exception, inform the caller
           throw TitleRefreshError("Unable to refresh title", cause)
       }
      
       if (result.isSuccessful) {
           // Save it to database
           titleDao.insertTitle(Title(result.body()!!))
       } else {
           // If it's not successful, inform the callback of the error
           throw TitleRefreshError("Unable to refresh title", null)
       }
   }
}

O código modificado é assim:

//TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

A transformação ainda é bastante simples: aqui, o Room usará o executor de consulta e transação configurados para executar o corpo da rotina. O Retrofit criará um novo objeto de chamada no thread de segundo plano e chamará a fila para enviar a solicitação de forma assíncrona. Quando o resultado retornar Continue a execução da rotina.

Ao mesmo tempo, Room e Retrofit fornecem segurança principal de segurança principal, portanto, não precisamos usar withContext (Dispatcher.IO) ao chamar ,

2 Use corotinas em funções de ordem superior

No código acima, embora tenha sido muito simplificado, mas se houver várias lógicas de solicitação, você precisará escrever um conjunto de lógica try-catch e inicialização de estado e atribuição de exceção, além de códigos de modelo. O código é o seguinte:

// MainViewModel.kt

fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           // 假设_spinner.value的赋值和其他异常逻辑都是通用的
           // 那么下面这行代码才是唯一需要关注的,
           repository.refreshTitle() 
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Neste momento, podemos usar o estilo de programação funcional para escrever uma função de alta ordem, encapsulando a lógica geral de processamento de negócios, da seguinte maneira:

private fun launchDataLoad(block: suspend () -> Unit): Job {
   return viewModelScope.launch {
       try {
           _spinner.value = true
           block()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Portanto, quando finalmente o chamamos no ViewModel, resta apenas uma linha de código chave:

// MainViewModel.kt

fun refreshTitle() {
   launchDataLoad {
       repository.refreshTitle()
   }
}

De fato, assim como usamos a programação funcional para escrever outras funções de ordem superior, aqui está apenas uma suspendmodificação de palavra - chave nos parâmetros . Ou seja, o suspendlambda pode chamar a função suspend, que é como o launcher e runBlocking do construtor de corotina são implementados.

// suspend lambda

block: suspend () -> Unit

3 Use corotinas e WorkManager juntos

O WorkManager faz parte do Android Jetpack . Usando a API do WorkManager, você pode agendar facilmente tarefas assíncronas adiadas que devem ser executadas mesmo quando o aplicativo sair ou o dispositivo reiniciar.

Função principal:

  • Maior compatibilidade com versões anteriores da API 14
    • Use JobScheduler em dispositivos executando a API 23 e acima
    • Use BroadcastReceiver e AlarmManager em dispositivos executando a API 14-22
  • Adicione restrições de trabalho, como disponibilidade de rede ou estado de cobrança
  • Agendar tarefas assíncronas únicas ou periódicas
  • Monitorar e gerenciar tarefas planejadas
  • Vincular tarefas
  • Garanta a execução da tarefa, mesmo quando o aplicativo ou dispositivo reiniciar
  • Siga os recursos de economia de energia, como o modo de baixo consumo de energia

O WorkManager foi projetado para tarefas que podem ser adiadas (ou seja, não precisam ser executadas imediatamente) e devem poder ser executadas de maneira confiável quando o aplicativo sair ou o dispositivo reiniciar. Por exemplo:

  • Envie logs ou analise dados para serviços de back-end
  • Sincronizar periodicamente os dados do aplicativo com o servidor

O WorkManager não é adequado para executar trabalhos em segundo plano que podem ser finalizados com segurança no final do processo do aplicativo, nem para tarefas que precisam ser executadas imediatamente.

Tome o uso diretamente como CoroutineWorkerexemplo aqui , personalize uma classe da qual RefreshMainDataWorkherdar CoroutineWorkere copie o doworkmétodo da seguinte maneira:

override suspend fun doWork(): Result {
   val database = getDatabase(applicationContext)
   val repository = TitleRepository(network, database.titleDao)

   return try {
       repository.refreshTitle()
       Result.success()
   } catch (error: TitleRefreshError) {
       Result.failure()
   }
}

Nota * CoroutineWorker.doWork () * é uma função de suspensão, diferente do pool de encadeamentos de configuração usado pela classe Worker comum, que usa o despachante no coroutineContext para controlar o agendamento do encadeamento (o padrão é Dispatchers.Default).

4 Sobre o manuseio de cancelamento e tempo limite de corotina

Nenhum dos códigos que escrevemos acima tem lógica sobre o cancelamento de corotinas, mas essa também é uma parte essencial da robustez do código. Embora na maioria dos casos, possamos usar o viewModelScope e o lifecycleScope fornecidos pelo Android para cancelar a corrotina interna no final do ciclo de vida da página, ainda existem algumas situações que exigem que lidemos com a lógica de cancelamento e tempo limite.

Esta parte pode se referir à introdução do site oficial do kotlin: Cancelamento e Tempos Limites

Usando os métodos cancel e join ou o método cancelAndJoin, podemos cancelar um trabalho da seguinte maneira:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // 在外部协程体中延迟1300毫秒,上面的job会先执行
    println("main: I'm tired of waiting!")
    job.cancel() // 取消当前的job
    job.join() // 等待直到这个job完成后结束 
    println("main: Now I can quit.")    
}

O log impresso é:

trabalho: estou dormindo 0 ...
trabalho: estou dormindo 1 ...
trabalho: estou dormindo 2 ...
principal: estou cansado de esperar!
main: Agora eu posso sair.

Vamos dar uma olhada no código fonte dos métodos cancel e join de Job:

abstract fun cancel(
    cause: CancellationException? = null
): Unit (source

abstract suspend fun join(): Unit (source)

Ao cancelar, um parâmetro opcional de causa pode ser fornecido para especificar uma mensagem de erro ou fornecer outras informações detalhadas sobre o motivo do cancelamento da depuração.
Quanto a essa função de suspensão de junção, a função cancelAndJoin aguardará a conclusão de toda a execução do corpo da rotina, incluindo a lógica no bloco try-finally.

Depois de chamar o método cancel de um trabalho de rotina, ele somente marca seu status como cancelado e sua lógica interna continuará sendo executada.Este não deve ser o resultado que esperamos, como o seguinte código:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) { // computation loop, just wastes CPU
            // print a message twice a second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")    
}

O log impresso é:

trabalho: estou dormindo 0 ...
trabalho: estou dormindo 1 ...
trabalho: estou dormindo 2 ...
principal: estou cansado de esperar!
trabalho: eu estou dormindo 3 ...
trabalho: eu estou dormindo 4 ...
principal: agora eu posso sair.

Há duas maneiras de resolver o problema acima: A primeira é chamar periodicamente a função de suspensão para verificar o cancelamento. Para isso, uma função de rendimento é uma boa escolha. O outro é verificar explicitamente o status do cancelamento. Vamos tentar o último método:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) { // 通过使用CoroutineScope的扩展属性isActive来使得该计算循环可取消
            // print a message twice a second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")    
}

O log impresso é:

trabalho: estou dormindo 0 ...
trabalho: estou dormindo 1 ...
trabalho: estou dormindo 2 ...
principal: estou cansado de esperar!
main: Agora eu posso sair.

Use a função de suspensão novamente ou lance uma exceção Cancel no bloco try-finally, porque o corpo da corotina foi cancelado no momento. Embora as operações comuns de liberação e fechamento de recursos não sejam bloqueadas e não introduzam chamadas de função de suspensão, em casos extremos, usando withContext (NonCancellable), a corotina cancelada pode ser suspensa novamente e, em seguida, Você pode continuar chamando a função de suspensão:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            withContext(NonCancellable) {
                println("job: I'm running finally")
                delay(1000L)
                println("job: And I've just delayed for 1 sec because I'm non-cancellable")
            }
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")    
}

Controle de tempo limite

Por exemplo, temos uma solicitação de rede que especifica 15 segundos como um tempo limite. Após o tempo limite, precisamos exibir a interface do usuário do tempo limite. Em seguida, podemos usar a função withTimeout da seguinte maneira:

import kotlinx.coroutines.*

fun main() = runBlocking {
    withTimeout(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
}

O log impresso é o seguinte:

Estou dormindo 0…
Estou dormindo 1…
Estou dormindo 2…
Exceção no segmento “main” kotlinx.coroutines.TimeoutCancellationException: Tempo limite excedido, esperando 1300 ms

Em seguida, podemos usar * try {…} catch (e: TimeoutCancellationException) {…} * para manipular a lógica do tempo limite.
O uso de withTimeoutOrNull retornará nulo após o tempo limite.Com esse recurso, ele também pode ser usado para lidar com a lógica do tempo limite.

fun main() = runBlocking {
    val result = withTimeoutOrNull(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
        "Done" // will get cancelled before it produces this result
    }
    println("Result is $result")
}

O log impresso é o seguinte:

Estou dormindo 0…
Estou dormindo 1…
Estou dormindo 2… O
resultado é nulo

5 Sobre como escrever casos de teste

Afinal, as corotinas ainda são uma coisa nova para nós, e é inevitável que algo dê errado, portanto, para o código de corotina que escrevemos, é essencial escrever mais testes de unidade.A biblioteca kotlinx-coroutines-test pode nos ajudar a testar o código de corotina, embora ainda seja Está na fase de testes, mas você ainda pode aprender.
Em vista do espaço limitado, as instruções oficiais de redação de casos de teste são postadas temporariamente aqui:
Testando corotinas através do comportamento
Testando corotinas diretamente

Using :
Usando Kotlin Coroutines no seu aplicativo Android
Coroutines avançados com Kotlin Flow e LiveData

Publicado 82 artigos originais · Gosto 86 · Visite mais de 110.000

Acho que você gosta

Origin blog.csdn.net/unicorn97/article/details/105170501
Recomendado
Clasificación