Olá Habr!
Lembramos que já temos uma
pré-encomenda do livro tão esperado na língua Kotlin da famosa série Big Nerd Ranch Guides. Hoje decidimos chamar a atenção para a tradução de um artigo que conta sobre as corotinas Kotlin e sobre o trabalho correto com fluxos no Android. O tópico está sendo discutido muito ativamente; portanto, para ser completo, também recomendamos que você consulte
este artigo da Habr e
esta publicação detalhada do blog da Axmor Software.
A estrutura competitiva moderna em Java / Android inflige muito em retornos de chamada e leva a estados de bloqueio, já que o Android não tem uma maneira bastante simples de garantir a segurança do encadeamento.
As corotinas Kotlin são um conjunto de ferramentas muito eficaz e completo que torna o gerenciamento da concorrência muito mais fácil e produtivo.
Pausar e bloquear: qual a diferençaAs corotinas não substituem os threads, mas fornecem uma estrutura para gerenciá-los. A filosofia da corutin é definir um contexto que permita
aguardar a conclusão das operações em segundo plano sem bloquear o encadeamento principal.
O objetivo de Corutin, neste caso, é dispensar retornos de chamada e simplificar a concorrência.
Exemplo mais simplesPara começar, vamos usar o exemplo mais simples: execute coroutine no contexto de
Main
(main thread). Nele, extrairemos a imagem do fluxo de
IO
e enviaremos esta imagem para processamento de volta ao
Main
.
launch(Dispatchers.Main) { val image = withContext(Dispatchers.IO) { getImage() }
O código é simples como uma função de thread único. Além disso, enquanto
getImage
é executado no pool alocado de threads de
IO
, o thread principal é gratuito e pode executar qualquer outra tarefa! A função withContext interrompe a atual rotina enquanto sua ação está em execução (
getImage()
). Assim que
getImage()
retornar e o
looper
do encadeamento principal se tornar disponível, a corotina continuará o trabalho no encadeamento principal e chamará
imageView.setImageBitmap(image)
.
O segundo exemplo: agora precisamos concluir duas tarefas em segundo plano para que possam ser usadas. Usaremos o dueto async / waitit para que essas duas tarefas sejam executadas em paralelo e usaremos seu resultado no thread principal assim que as duas tarefas estiverem prontas:
val job = launch(Dispatchers.Main) { val deferred1 = async(Dispatchers.Default) { getFirstValue() } val deferred2 = async(Dispatchers.IO) { getSecondValue() } useValues(deferred1.await(), deferred2.await()) } job.join()
async
é semelhante ao
launch
, mas retorna
deferred
(uma entidade Kotlin equivalente a
Future
), para que seu resultado possa ser obtido usando
await()
. Quando chamado sem parâmetros, ele funciona no contexto padrão para o escopo atual.
Novamente, o encadeamento principal permanece livre enquanto aguardamos nossos 2 valores.
Como você pode ver, a função de
launch
retorna
Job
, que pode ser usado para aguardar a conclusão da operação - isso é feito usando a função
join()
. Funciona como em qualquer outro idioma, com a ressalva de que simplesmente
suspende a corotina e não bloqueia o fluxo .
DespacharDespachar é um conceito-chave ao trabalhar com corotinas. Esta ação permite "pular" de um segmento para outro.
Considere como é o equivalente ao envio no
Main
em java, ou seja,
runOnUiThread: public final void runOnUiThread(Runnable action) { if (Thread.currentThread() != mUiThread) { mHandler.post(action);
Main
Implementação do Contexto
Main
para Android é um
Handler
baseado em
Handler
. Portanto, esta é realmente uma implementação muito adequada:
launch(Dispatchers.Main) { ... } vs launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) { ... }
launch(Dispatchers.Main)
envia
Runnable
para
Handler
, para que seu código não seja executado imediatamente.
launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED)
executará imediatamente sua expressão lambda no encadeamento atual.
Dispatchers.Main
assegura que, quando a corotina retomar o trabalho, ela será direcionada para o thread principal ; Além disso, Handler é usado aqui como uma implementação nativa do Android para enviar para o loop de eventos do aplicativo.
A implementação exata é assim:
val Main: HandlerDispatcher = HandlerContext(mainHandler, "Main")
Aqui está um bom artigo para ajudá-lo a entender os meandros da expedição no Android:
Noções básicas sobre o Android Core: Looper, Handler e HandlerThread .
Contexto da CorotinaO contexto da corotina (também conhecido como gerenciador de corotina) determina em qual segmento seu código será executado, o que fazer se uma exceção for lançada e se refere ao contexto pai para propagar o cancelamento.
val job = Job() val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> whatever(throwable) } launch(Disaptchers.Default+exceptionHandler+job) { ... }
job.cancel()
cancelará todas as corotinas cujo pai é
job
. Um exceptionHandler receberá todas as exceções lançadas nessas corotinas.
Âmbito de aplicaçãoA interface
coroutineScope
simplifica o tratamento de erros:
Se alguma de suas corotinas filhas falhar, o escopo inteiro e todas as corotinas filhas também serão canceladas.
No exemplo
async
, se não foi possível extrair o valor, enquanto outra tarefa continuava funcionando, temos um estado danificado e precisamos fazer algo sobre isso.
Ao trabalhar com
coroutineScope
, a função
useValues
será chamada apenas se a extração de ambos os valores for bem-sucedida. Além disso, se o
deferred2
falhar, o
deferred1
será cancelado.
coroutineScope { val deferred1 = async(Dispatchers.Default) { getFirstValue() } val deferred2 = async(Dispatchers.IO) { getSecondValue() } useValues(deferred1.await(), deferred2.await()) }
Você também pode "colocar no escopo" uma classe inteira para definir um
CoroutineContext
padrão e usá-lo.
Um exemplo de classe que implementa a interface
CoroutineScope
:
open class ScopedViewModel : ViewModel(), CoroutineScope { protected val job = Job() override val coroutineContext = Dispatchers.Main+job override fun onCleared() { super.onCleared() job.cancel() } }
Corutin em execução no CoroutineScope:
O gerenciador de
launch
ou
async
padrão agora se torna o gerenciador de escopo atual.
launch { val foo = withContext(Dispatchers.IO) { … }
Lançamento autônomo da corotina (fora de qualquer CoroutineScope):
GlobalScope.launch(Dispatchers.Main) {
Você pode até definir o escopo de um aplicativo definindo o distribuidor
Main
padrão:
object AppScope : CoroutineScope by GlobalScope { override val coroutineContext = Dispatchers.Main.immediate }
Observações- Corotinas limitam a interoperabilidade com Java
- Limitar a mutabilidade para evitar bloqueios
- As corotinas são projetadas para esperar, não para organizar threads
- Evite E / S em
Dispatchers.Default
(e Main
...) - é para isso que Dispatchers.IO é - Os fluxos consomem recursos, portanto, contextos de thread único são usados
Dispatchers.Default
baseado no ForkJoinPool
, introduzido no Android 5+- Corotinas podem ser usadas através de canais
Livrar-se de bloqueios e retornos de chamada usando canaisDefinição de canal da documentação do JetBrains:
Canal Channel
conceitualmente muito semelhante ao BlockingQueue
. A principal diferença é que ela não bloqueia a operação de venda, fornece uma suspensão de send
(ou offer
sem bloqueio) e, em vez de bloquear a operação de entrega, fornece um receive
suspensão.
AtoresConsidere uma ferramenta simples para trabalhar com canais:
Actor
.
Actor
, novamente, é muito semelhante ao
Handler
: definimos o contexto da corotina (ou seja, o encadeamento no qual vamos executar as ações) e trabalhamos com ela em uma ordem seqüencial.
A diferença, é claro, é que as corutinas são usadas aqui;
Você pode especificar a potência e o código executado - pausa .
Em princípio, o
actor
redirecionará qualquer comando para o canal da corotina.
Garante a execução de um comando e restringe as operações em seu contexto . Essa abordagem ajuda perfeitamente a se livrar das chamadas
synchronize
e a manter todos os threads livres!
protected val updateActor by lazy { actor<Update>(capacity = Channel.UNLIMITED) { for (update in channel) when (update) { Refresh -> updateList() is Filter -> filter.filter(update.query) is MediaUpdate -> updateItems(update.mediaList as List<T>) is MediaAddition -> addMedia(update.media as T) is MediaListAddition -> addMedia(update.mediaList as List<T>) is MediaRemoval -> removeMedia(update.media as T) } } }
Neste exemplo, usamos as classes seladas de Kotlin, escolhendo qual ação executar.
sealed class Update object Refresh : Update() class Filter(val query: String?) : Update() class MediaAddition(val media: Media) : Update()
Além disso, todas essas ações serão enfileiradas, nunca serão executadas em paralelo. Essa é uma maneira conveniente de atingir
limites de variabilidade .
Ciclo de vida do Android + CorotinasOs atores também podem ser muito úteis para controlar a interface do usuário do Android, simplificar o cancelamento de tarefas e impedir a sobrecarga do encadeamento principal.
Vamos implementar isso e chamar
job.cancel()
quando a atividade for destruída.
class MyActivity : AppCompatActivity(), CoroutineScope { protected val job = SupervisorJob()
A classe
SupervisorJob
é semelhante à
Job
regular, com a única exceção de que o cancelamento se estende apenas na direção downstream.
Portanto, não cancelamos todas as corotinas em uma
Activity
quando uma delas falha.
As coisas estão um pouco melhores com
uma função de extensão que permite acessar esse
CoroutineContext
partir de qualquer
View
no
CoroutineScope
.
val View.coroutineContext: CoroutineContext? get() = (context as? CoroutineScope)?.coroutineContext
Agora podemos combinar tudo isso, a função
setOnClick
cria um ator combinado para controlar suas ações
onClick
. No caso de vários toques, as ações intermediárias serão ignoradas, eliminando erros de ANR (o aplicativo não responde) e essas ações serão executadas no escopo da
Activity
. Portanto, quando a atividade é destruída, tudo isso será cancelado.
fun View.setOnClick(action: suspend () -> Unit) {
Neste exemplo, configuramos o
Channel
como
Conflated
para que ele ignore alguns eventos, se houver muitos deles. Você pode substituí-lo por
Channel.UNLIMITED
se preferir enfileirar eventos sem perder nenhum deles, mas ainda assim desejar proteger o aplicativo contra erros de ANR.
Você também pode combinar as estruturas e as estruturas de ciclo de vida para automatizar o cancelamento de tarefas relacionadas à interface com o usuário:
val LifecycleOwner.untilDestroy: Job get() { val job = Job() lifecycle.addObserver(object: LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroy() { job.cancel() } }) return job }
Simplifique a situação com retornos de chamada (parte 1)Veja como transformar o uso de APIs baseadas em retorno de chamada com o
Channel
.
A API funciona assim:
requestBrowsing(url, listener)
analisa a pasta localizada em url
.- O
listener
recebe onMediaAdded(media: Media)
para qualquer arquivo de mídia encontrado nesta pasta. listener.onBrowseEnd()
é chamado ao analisar a pasta
Aqui está a função de
refresh
antiga no provedor de conteúdo para o navegador VLC:
private val refreshList = mutableListOf<Media>() fun refresh() = requestBrowsing(url, refreshListener) private val refreshListener = object : EventListener{ override fun onMediaAdded(media: Media) { refreshList.add(media)) } override fun onBrowseEnd() { val list = refreshList.toMutableList() refreshList.clear() launch { dataset.value = list parseSubDirectories() } } }
Como melhorá-lo?Crie um canal que será executado na
refresh
. Agora, os retornos de chamada do navegador direcionarão apenas a mídia para esse canal e depois o fecharão.
Agora a função de
refresh
ficou mais clara. Ela cria um canal, liga para o navegador VLC, depois forma uma lista de arquivos de mídia e os processa.
Em vez das
consumeEach
select
ou
consumeEach
você pode usar
for
aguardar a mídia, e esse loop será interrompido assim que o
browserChannel
fechar.
private lateinit var browserChannel : Channel<Media> override fun onMediaAdded(media: Media) { browserChannel.offer(media) } override fun onBrowseEnd() { browserChannel.close() } suspend fun refresh() { browserChannel = Channel(Channel.UNLIMITED) val refreshList = mutableListOf<Media>() requestBrowsing(url)
Simplificando a situação com retornos de chamada (parte 2): RetrofitA segunda abordagem: não usamos corotinas da kotlinx, mas usamos uma estrutura principal da corotina.
Veja como as corotinas realmente funcionam!
A função
retrofitSuspendCall
agrupa uma solicitação de
Retrofit Call
para torná-la uma função de
suspend
.
Usando
suspendCoroutine
chamamos o método
Call.enqueue
e pausamos a corotina. O retorno de chamada fornecido dessa maneira chamará
continuation.resume(response)
para retomar a corotina com uma resposta do servidor assim que for recebida.
Em seguida, precisamos apenas combinar nossas funções Retrofit em
retrofitSuspendCall
para retornar resultados da consulta usando-as.
suspend inline fun <reified T> retrofitSuspendCall(request: () -> Call <T> ) : Response <T> = suspendCoroutine { continuation -> request.invoke().enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { continuation.resume(response) } override fun onFailure(call: Call<T>, t: Throwable) { continuation.resumeWithException(t) } }) } suspend fun browse(path: String?) = retrofitSuspendCall { ApiClient.browse(path) }
Assim, a chamada que bloqueia a rede é feita no encadeamento Retrofit dedicado, a corotina está aqui, aguardando uma resposta do servidor e não há lugar para usá-lo no aplicativo!
Esta implementação é inspirada na
biblioteca gildor / kotlin-coroutines-retrofit .
Há também um
adaptador JakeWharton / retrofit2-kotlin-coroutines com outra implementação que fornece um resultado semelhante.
EpílogoChannel
pode ser usado de várias outras maneiras; Confira
BroadcastChannel para implementações mais poderosas que você pode achar úteis.
Você também pode criar canais usando a função
Produzir .
Por fim, usando canais, é conveniente organizar a comunicação entre os componentes da interface do usuário: o adaptador pode transmitir eventos de clique para seu fragmento / atividade via
Channel
ou, por exemplo, através do
Actor
.