Práticas recomendadas para a Kotlin Coroutine para Android

O é um conjunto de boas práticas continuamente mantido para o uso de Kotlin Coroutines no Android. Por favor, comente abaixo se você tiver alguma sugestão sobre algo que deve ser adicionado.

  1. Manipulando Ciclos de Vida do Android

Da mesma forma que você usa CompositeDisposables com RxJava, as Kotlin Coroutines precisam ser canceladas no momento certo, com conhecimento dos Ciclos de vida do Android com atividades e fragmentos.

a) Usando modelos de exibição do Android

Essa é a maneira mais fácil de configurar corotinas para que sejam desligadas no momento certo, mas funciona apenas dentro de um Android ViewModel que possui uma função onCleared na qual as tarefas de corotina podem ser canceladas com segurança de:

private val viewModelJob = Job ()
private val uiScope = CoroutineScope (Dispatchers.Main + viewModelJob)
substituir diversão onCleared () {
 super.onCleared ()
 uiScope.coroutineContext.cancelChildren ()
}

Nota: a partir do ViewModels 2.1.0-alpha01, isso não é mais necessário. Não é mais necessário que seu viewmodel implemente o CoroutineScope, onCleared ou adicione um trabalho. Basta usar "viewModelscope.launch {}". Observe que 2.x significa que seu aplicativo precisará estar no AndroidX porque não tenho certeza de que eles planejam fazer o backport para a versão 1.x do ViewModels.

b) Usando observadores do ciclo de vida

Essa outra técnica cria um escopo que você anexa a uma atividade ou fragmento (ou qualquer outra coisa que implemente um ciclo de vida do Android):

/ **
 * Contexto da corotina que é automaticamente cancelado quando a interface do usuário é destruída
 * /
classe UiLifecycleScope: CoroutineScope, LifecycleObserver {

    private lateinit var job: Trabalho
    Substituir val coroutineContext: CoroutineContext
        get () = job + Dispatchers.Main

    @OnLifecycleEvent (Lifecycle.Event.ON_START)
    divertido onCreate () {
        job = Job ()
    }

    @OnLifecycleEvent (Lifecycle.Event.ON_PAUSE)
    divertido destruir () = job.cancel ()
}
... dentro Atividade ou fragmento de Lib de suporte
valor privado uiScope = UiLifecycleScope ()
substituir diversão onCreate (savedInstanceState: bundle) {
  super.onCreate (savedInstanceState)
  lifecycle.addObserver (uiScope)
}

c) GlobalScope

Se você usa o GlobalScope, é um escopo que dura a vida útil do aplicativo. Você usaria isso para sincronização em segundo plano, atualizações de recompra etc. (não vinculado a um ciclo de vida da atividade).

d) Serviços

Os serviços podem cancelar seus trabalhos no onDestroy:

private val serviceJob = Job ()
private val serviceScope = CoroutineScope (Dispatchers.Main + serviceJob)
substituir diversão onCleared () {
 super.onCleared ()
 serviceJob.cancel ()
}

2. Tratamento de exceções

a) Em assíncrono x lançamento x runBlocking

É importante observar que as exceções em um bloco de inicialização {} travam o aplicativo sem um manipulador de exceções. Sempre configure um manipulador de exceções padrão para passar como um parâmetro para iniciar.

Uma exceção em um bloco runBlocking {} trava o aplicativo, a menos que você adicione uma tentativa de captura. Sempre adicione um try / catch se você estiver usando o runBlocking. Idealmente, use apenas runBlocking para testes de unidade.

Uma exceção lançada dentro de um bloco assíncrono {} não será propagada ou executada até que o bloco seja aguardado porque é realmente um Java Adiado por baixo. A função / método de chamada deve capturar exceções.

b) Capturando exceções

Se você usa o assíncrono para executar o código que pode gerar exceções, é necessário agrupar o código em um coroutineScope para capturar as exceções corretamente (graças ao LouisC, por exemplo):

experimentar {
    coroutineScope {
        val mayFailAsync1 = assíncrono {
            mayFail1 ()
        }
        val mayFailAsync2 = assíncrono {
            mayFail2 ()
        }
        useResult (mayFailAsync1.await (), mayFailAsync2.await ())
    }
} captura (e: IOException) {
    // lida com isso
    throw MyIoException ("Erro ao fazer IO", e)
} captura (e: AnotherException) {
    // lida com isso também
    throw MyOtherException ("Erro ao fazer algo", e)
}

Quando você capturar a exceção, envolva-a em outra exceção (semelhante ao que você faz para o RxJava) para obter a linha de rastreamento de pilha em seu próprio código, em vez de ver um rastreamento de pilha com apenas código de rotina.

c) Exceções de log

Se você estiver usando GlobalScope.launch ou um ator, sempre passe um manipulador de exceções que possa registrar exceções. Por exemplo.

val errorHandler = CoroutineExceptionHandler {_, exceção ->
  // log para Crashlytics, logcat, etc.
}
val job = GlobalScope.launch (errorHandler) {
...
}

Quase sempre, você deve escopos estruturados no Android e um manipulador deve ser usado:

val errorHandler = CoroutineExceptionHandler {_, exceção ->
  // log para o Crashlytics, logcat, etc .; pode ser injetado dependência
}
val supervisor = SupervisorJob () // cancelado com ciclo de vida da atividade
with (CoroutineScope (coroutineContext + supervisor)) {
  val algo = lançamento (errorHandler) {
    ...
  }
}

E se você estiver usando assíncrono e aguardando, sempre envolva try / catch conforme descrito acima, mas faça logon conforme necessário.

d) Considere a classe selada por resultado / erro

Considere usar uma classe selada de resultado que pode conter um erro em vez de lançar exceções:

classe selada Resultado  {
  classe de dados Success (val data: T): Result ()
  classe de dados Erro (erro val: E): Resultado ()
}

e) Nome do contexto da rotina

Ao declarar um lambda assíncrono, você também pode nomeá-lo assim:

async (CoroutineName ("MyCoroutine"))) {}

Se você estiver criando seu próprio encadeamento para executar, também poderá nomeá-lo ao criar este executor de encadeamento:

newSingleThreadContext ("MyCoroutineThread")

3. Pools de Executores e Tamanhos de Pool Padrão

O Coroutines é realmente multitarefa cooperativa (com assistência do compilador) em um tamanho limitado de pool de encadeamentos. Isso significa que, se você fizer algo bloqueando a sua rotina (por exemplo, usar uma API de bloqueio), amarrará o encadeamento inteiro até que a operação de bloqueio seja concluída. A corotina também não será suspensa, a menos que você produza um atraso ou atrase; portanto, se você tiver um longo ciclo de processamento, verifique se a corotina foi cancelada (chame "assegureActive ()" no escopo) para poder liberar o segmento; isso é semelhante ao modo como o RxJava funciona.

As corotinas da Kotlin possuem alguns despachantes embutidos (equivalentes aos agendadores no RxJava). O expedidor principal (se você não especificar nada para executar) é a interface do usuário; você só deve alterar os elementos da interface do usuário nesse contexto. Também existe um Dispatchers. Não confinado, que pode alternar entre a interface do usuário e os threads de plano de fundo, para que não fique em um único thread; isso geralmente não deve ser usado, exceto em testes de unidade. Existe um Dispatchers.IO para manipulação de E / S (chamadas de rede que são frequentemente suspensas). Finalmente, há um Dispatchers.Default, que é o pool principal de threads em segundo plano, mas isso é limitado ao número de CPUs.

Na prática, você deve usar uma interface para despachantes comuns que são transmitidos por meio do construtor de sua classe, para que você possa trocar diferentes para teste. Por exemplo.:

interface CoroutineDispatchers {
  UI val: Dispatcher
  val IO: expedidor
  val Computation: Dispatcher
  fun newThread (nome do val: String): Dispatcher
}

4. Evitando corrupção de dados

Não tem funções de suspensão modificar dados fora da função. Por exemplo, isso pode ter modificação não intencional de dados se os dois métodos forem executados a partir de diferentes threads:

val list = mutableListOf (1, 2)
suspender o divertido updateList1 () {
  lista [0] = lista [0] + 1
}
suspender o divertido updateList2 () {
  list.clear ()
}

Você pode evitar esse tipo de problema:
- fazer com que suas corotinas retornem um objeto imutável em vez de estender a mão e mudar um
- execute todas essas corotinas em um único contexto encadeado criado por: newSingleThreadContext ("contextname")

5. Faça o Proguard feliz

Estas regras devem ser adicionadas para compilações de versão do seu aplicativo:

-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepclassmembernames class kotlinx. ** {volatile ; }

6. Interoperabilidade com Java

Se você estiver trabalhando em um aplicativo herdado, sem dúvida terá uma parte significativa do código Java. É possível chamar corotinas do Java retornando um CompletableFuture (não se esqueça de incluir o artefato kotlinx-coroutines-jdk8):

doSomethingAsync (): CompletableFuture > =
   GlobalScope.future {doSomething ()}

7. A atualização não é necessária com o Contexto

Se você estiver usando o adaptador Retrotit Coroutines, receberá um adiado que usa a chamada assíncrona do okhttp sob o capô. Portanto, você não precisa adicionar withContext (Dispatchers.IO), como no RxJava, para garantir que o código seja executado em um encadeamento de E / S; se você não usar o adaptador de corotinas Retrofit e ligar diretamente para uma chamada de retrofit, precisará doContext.

O banco de dados da sala de componentes do arco do Android também funciona automaticamente em um contexto que não seja da interface do usuário; portanto, você não precisa do Contexto.

Referências:

  • https://medium.com/capital-one-tech/kotlin-coroutines-on-android-things-i-wish-i-knew-at-the-beginning-c2f0b1f16cff
  • https://speakerdeck.com/elizarov/fresh-async-with-kotlin
  • https://medium.com/@michaelbukachi/coroutines-and-idling-resources-c1866bfa5b5d
  • https://blog.kotlin-academy.com/kotlin-coroutines-cheat-sheet-8cf1e284dc35
  • https://medium.com/androiddevelopers/room-coroutines-422b786dc4c5?linkId=63267803
  • https://proandroiddev.com/managing-exceptions-in-nested-coroutine-scopes-9f23fd85e61