As corotinas são uma ferramenta poderosa para execução de código assíncrono. Eles trabalham em paralelo, se comunicam e consomem poucos recursos. Parece que, sem medo, as corotinas podem ser introduzidas na produção. Mas há medos e eles interferem.
O
relatório de Vladimir Ivanov no
AppsConf é sobre o fato de que o diabo não é tão terrível e que você pode usar corotinas agora:
Sobre o palestrante : Vladimir Ivanov (
dzigoro ) é um desenvolvedor líder de Android na
EPAM, com 7 anos de experiência, gosta de Arquitetura de Soluções, React Native e desenvolvimento iOS e também possui um certificado do
Google Cloud Architect .
Tudo o que você lê é um produto da produção de experiência e de vários estudos; portanto, tome como está, sem qualquer garantia.
Corotinas, Kotlin e RxJava
Para informações: o status atual de corutin está no release, deixado Beta.
O Kotlin 1.3 foi lançado, as corotinas são declaradas estáveis e há paz no mundo.

Recentemente, conduzi uma pesquisa no Twitter de que as pessoas que usam corotina:
- 13% das corotinas nos alimentos. Tudo está bem;
- 25% experimentá-los no projeto pet;
- 24% - O que é Kotlin?
- A maior parte do 38% RxJava está em toda parte.
As estatísticas não são felizes. Acredito que o
RxJava é uma ferramenta muito complexa para tarefas nas quais é comumente usada por desenvolvedores. As corotinas são mais adequadas para controlar a operação assíncrona.
Nos meus relatórios anteriores, falei sobre como refatorar o RxJava para as corotinas em Kotlin, por isso não vou me debruçar sobre isso em detalhes, mas apenas relembrar os pontos principais.
Por que usamos corotinas?
Como se usarmos o RxJava, os exemplos de implementação comuns serão assim:
interface ApiClientRx { fun login(auth: Authorization) : Single<GithubUser> fun getRepositories (reposUrl: String, auth: Authorization) : Single<List<GithubRepository>> }
Temos uma interface, por exemplo, escrevemos um cliente GitHub e queremos executar algumas operações para ele:
- Usuário de logon.
- Obtenha uma lista dos repositórios do GitHub.
Nos dois casos, as funções retornarão Objetos de negócios únicos: GitHubUser ou uma lista de GitHubRepository.
O código de implementação para esta interface é o seguinte:
private fun attemptLoginRx () { showProgress(true) compositeDisposable.add(apiClient.login(auth) .flatMap { user -> apiClient.getRepositories(user.repos_url, auth) } .map { list -> list.map { it.full_name } } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doFinally { showProgress(false) } .subscribe( { list -> showRepositories(this, list) }, { error -> Log.e("TAG", "Failed to show repos", error) } )) }
- Tomamos
CompositeDisposable para que não haja vazamento de memória.
- Adicione uma chamada ao primeiro método.
- Utilizamos operadores convenientes para obter o usuário, por exemplo
flatMap .
- Nós temos uma lista de seus repositórios.
- Nós escrevemos um
Boilerplate para que ele
funcione nos threads certos.
- Quando tudo estiver pronto, mostramos a lista de repositórios para o usuário conectado.
Dificuldades com o código RxJava:- Complexidade Na minha opinião, o código é muito complicado para a tarefa simples de duas chamadas de rede e exibir algo na interface do usuário .
- Rastreios de pilha não acoplados. Rastreios de pilha quase não estão relacionados ao código que você escreve.
- Recursos em excesso . O RxJava gera muitos objetos sob o capô e o desempenho pode diminuir.
Qual será o mesmo código com corotinas até a versão 0.26?Em 0,26, a API mudou e estamos falando sobre produção. Ninguém conseguiu aplicar 0,26 no produto, mas estamos trabalhando nisso.
Com as corotinas, nossa interface mudará bastante . As funções interromperão o retorno de Singles e outros objetos auxiliares. Eles retornarão imediatamente objetos de negócios: GitHubUser e uma lista de GitHubRepository. As funções GitHubUser e GitHubRepository terão modificadores de
suspensão . Isso é bom, porque suspender quase não nos obriga a nada:
interface ApiClient { suspend fun login(auth: Authorization) : GithubUser suspend fun getRepositories (reposUrl: String, auth: Authorization) : List<GithubRepository> }
Se você observar o código que já usa a implementação dessa interface, ele será alterado significativamente em comparação com o RxJava:
private fun attemptLogin () { launch(UI) { val auth = BasicAuthorization(login, pass) try { showProgress(true) val userlnfo = async { apiClient.login(auth) }.await() val repoUrl = userlnfo.repos_url val list = async { apiClient.getRepositories(repoUrl, auth) }.await() showRepositories( this, list.map { it -> it.full_name } ) } catch (e: RuntimeException) { showToast("Oops!") } finally { showProgress(false) } } }
- A ação principal ocorre quando chamamos de
coroutine builder async , aguardamos uma resposta e obtemos
userlnfo .
- Usamos dados deste objeto.
- Faça outra chamada
assíncrona e
aguarde .
Tudo parece que nenhum trabalho assíncrono está acontecendo, e apenas escrevemos os comandos na coluna e eles são executados. No final, fazemos o que precisa ser feito na interface do usuário.
Por que as corotinas são melhores?- Este código é mais fácil de ler. Está escrito como se fosse consistente.
- Provavelmente, o desempenho desse código é melhor do que no RxJava.
- É muito simples escrever testes, mas vamos encontrá-los um pouco mais tarde.
2 passos para o lado
Vamos discordar um pouco, há algumas coisas que ainda precisam ser discutidas.
Etapa 1. withContext vs launch / async
Além do
assíncrono do construtor de corotina, há o
construtor de corotina com o Contexto .
Iniciar ou
assíncrono cria um novo
contexto de Coroutine , que nem sempre é necessário. Se você tiver um contexto de Coroutine que deseja usar em todo o aplicativo, não precisará recriá-lo. Você pode simplesmente reutilizar um existente. Para fazer isso, você precisará de um construtor de corotina com Contexto. Ele simplesmente reutiliza o contexto existente da Coroutine. Será 2-3 vezes mais rápido, mas agora é uma pergunta sem princípios. Se os números exatos forem interessantes,
aqui está a pergunta sobre o
stackoverflow com benchmarks e detalhes.
Regra geral: use withContext sem dúvida onde ele se encaixa semanticamente. Mas se você precisar de carregamento paralelo, por exemplo, várias fotos ou dados, então async / waitit é a sua escolha.
Etapa 2. Refatoração
E se você refatorar uma cadeia RxJava realmente complexa? Me deparei com isso na produção:
observable1.getSubject().zipWith(observable2.getSubject(), (t1, t2) -> {
Eu tinha uma corrente complicada com um
assunto público , com
zíper e
efeitos colaterais em cada
zíper que enviavam outra coisa para o ônibus do evento. A tarefa pelo menos era livrar-se do barramento de eventos. Fiquei um dia sentado, mas não pude refatorar o código para resolver o problema.
A decisão certa acabou descartando tudo e reescrevendo o código na corotina em 4 horas .
O código abaixo é muito semelhante ao que recebi:
try { val firstChunkJob = async { call1 } val secondChunkJob = async { call2 } val thirdChunkJob = async { call3 } return Result( firstChunkJob.await(), secondChunkJob.await(), thirdChunkJob.await()) } catch (e: Exception) {
- Assíncrono para uma tarefa, para a segunda e terceira.
- Estamos aguardando o resultado e colocamos tudo em um objeto.
- Feito!
Se você tem cadeias complexas e existem corotinas, basta refatorar. É realmente rápido.
O que impede os desenvolvedores de usar corotinas no prod?
Na minha opinião, atualmente, como desenvolvedores, somos impedidos de usar corotinas apenas pelo medo de algo novo:
- Não sabemos o que fazer com o ciclo de vida , atividade e ciclo de vida do fragmento. Como trabalhar com corotinas nesses casos?
- Não há experiência na solução de tarefas complexas diárias na produção usando corutin.
- Ferramentas insuficientes. Um monte de bibliotecas e funções foram escritas para o RxJava. Por exemplo RxFCM . O próprio RxJava possui muitos operadores, o que é bom, mas e a rotina?
- Nós realmente não entendemos como testar corotinas.
Se nos livrarmos desses quatro medos, podemos dormir em paz à noite e usar corotinas na produção.
Vamos ponto por ponto.
1. Gerenciamento do ciclo de vida
- As corotinas podem vazar como descartáveis ou AsyncTask . Este problema deve ser resolvido manualmente.
- Para evitar uma exceção aleatória de ponteiro nulo, as rotinas devem ser interrompidas.
Parar
Você está familiarizado com o
Thread.stop () ? Se você o usou, não por muito tempo. No
JDK 1.1, o método foi imediatamente declarado obsoleto, pois é impossível pegar e parar um determinado trecho de código e não há garantias de que ele será concluído corretamente. Provavelmente você terá apenas
corrupção de memória .
Portanto,
Thread.stop () não funciona . Você precisa que o cancelamento seja cooperativo, ou seja, o código do outro lado para saber que você está cancelando.
Como aplicamos paradas com o RxJava:
private val compositeDisposable = CompositeDisposable() fun requestSmth() { compositeDisposable.add( apiClientRx.requestSomething() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> {}) } override fun onDestroy() { compositeDisposable.dispose() }
No RxJava,
usamos CompositeDisposable .
- Adicione a variável
compositeDisposable à atividade no fragmento ou no apresentador, onde usamos o RxJava.
- Em
onDestro, adicione
Dispose e todas as exceções desaparecem sozinhas.
Aproximadamente o mesmo princípio com corotinas:
private val job: Job? = null fun requestSmth() { job = launch(UI) { val user = apiClient.requestSomething() … } } override fun onDestroy() { job?.cancel() }
Considere um exemplo de uma
tarefa simples .
Normalmente, os
construtores de corotina retornam um
trabalho e, em alguns casos,
Adiados .
- Podemos memorizar este trabalho.
- Dê o comando
"launch" coroutine builder . O processo inicia, algo acontece, o resultado da execução é lembrado.
- Se não passarmos mais nada, o comando "launch" inicia a função e retorna um link para o trabalho.
- Jó é lembrado e, no onDestroy, dizemos
"cancelar" e tudo funciona bem.
Qual é o problema da abordagem? Cada trabalho precisa de um campo. Você precisa manter uma lista de trabalhos para cancelá-los todos juntos. A abordagem leva à duplicação de código, não faça isso.
A boa notícia é que temos
alternativas :
CompositeJob e
trabalho que reconhece o ciclo de vida .
CompositeJob é um análogo de compositeDisposable. Parece algo como isto
: private val job: CompositeJob = CompositeJob() fun requestSmth() { job.add(launch(UI) { val user = apiClient.requestSomething() ... }) } override fun onDestroy() { job.cancel() }
- Para um fragmento, começamos um trabalho.
- Colocamos todos os
trabalhos no CompositeJob e damos o comando:
"job.cancel () para todos!" .
A abordagem é facilmente implementada em 4 linhas, sem contar a declaração de classe:
Class CompositeJob { private val map = hashMapOf<String, Job>() fun add(job: Job, key: String = job.hashCode().toString()) = map.put(key, job)?.cancel() fun cancel(key: String) = map[key]?.cancel() fun cancel() = map.forEach { _ ,u -> u.cancel() } }
Você precisará de:
-
mapa com uma chave de cadeia,
- método
add , no qual você adicionará trabalho,
- parâmetro
chave opcional.
Se você deseja usar a mesma chave para o mesmo trabalho - por favor. Caso contrário, o
hashCode resolverá o nosso problema. Adicione o trabalho ao mapa pelo qual passamos e cancele o anterior com a mesma chave. Se cumprirmos demais a tarefa, o resultado anterior não nos interessa. Nós a cancelamos e dirigimos novamente.
Cancelar é simples: conseguimos o trabalho com a tecla e cancelamos. O segundo cancelamento de todo o mapa cancela tudo. Todo o código é escrito em meia hora em quatro linhas e funciona. Se você não quiser escrever, siga o exemplo acima.
Trabalho com reconhecimento do ciclo de vida
Você já usou o
Android Lifecycle ,
proprietário ou
observador do Lifecycle ?

Nossa
atividade e
fragmentos têm certos estados. Destaques:
criados, iniciados e
retomados . Existem diferentes transições entre estados.
O LifecycleObserver permite que você assine essas transições e faça alguma coisa quando uma delas ocorrer.
Parece bem simples:
public class MyObserver implements LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) public void connectListener() { ... } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) public void disconnectListener() { … } }
Você desliga a anotação com algum parâmetro no método, e ela é chamada com a transição correspondente. Basta usar esta abordagem para a rotina:
class AndroidJob(lifecycle: Lifecycle) : Job by Job(), LifecycleObserver { init { lifecycle.addObserver(this) } @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun destroy() { Log.d("AndroidJob", "Cancelling a coroutine") cancel() } }
- Você pode escrever a classe base
AndroidJob .
- Transferiremos o
Ciclo de vida da turma.
- A interface
LifecycleObserver implementará o trabalho.
Tudo o que precisamos:
- No construtor, adicione ao Lifecycle como um Observer.
- Assine
ON_DESTROY ou qualquer outra coisa que nos interesse.
- Faça o cancelamento em ON_DESTROY.
-
Obtenha um
job pai no seu fragmento.
- Chame o construtor
Joy jobs ou o
ciclo de vida do seu fragmento de atividade. Não faz diferença.
- Passe este
parentJob como
pai .
O código final é assim:
private var parentJob = AndroidJob(lifecycle) fun do() { job = launch(UI, parent = parentJob) {
Quando você cancela o pai, todas as rotinas filho são canceladas e você não precisa mais escrever nada no fragmento. Tudo acontece automaticamente, não é mais ON_DESTROY. O principal não se esqueça de passar
parent = parentJob .
Se você usar, poderá escrever uma regra simples de fiapos que destacará você: "Ah, você esqueceu seus pais!"
Com
Gerenciamento do ciclo de vida resolvido. Temos algumas ferramentas que permitem que você faça isso de maneira fácil e confortável.
E os cenários complexos e as tarefas não triviais na produção?
2. Casos de uso complexos
Cenários complexos e tarefas não triviais são:
-
Operadores - operadores complexos no RxJava: flatMap, debounce, etc.
-
Tratamento de erros - tratamento complexo de erros. Não apenas
tente ... capturar , mas aninhado por exemplo.
- O
armazenamento em cache é uma tarefa não trivial. Na produção, encontramos um cache e queríamos obter uma ferramenta para resolver facilmente o problema de cache com corotinas.
Repetir
Quando pensamos nos operadores da corotina, a primeira opção foi
repeatWhen () .
Se algo deu errado e Corutin não conseguiu acessar o servidor interno, queremos tentar várias vezes com algum tipo de fallback exponencial. Talvez o motivo seja uma conexão ruim e obteremos o resultado desejado repetindo a operação várias vezes.
Com corotinas, esta tarefa é facilmente implementada:
suspend fun <T> retryDeferredWithDelay( deferred: () -> Deferred<T>, tries: Int = 3, timeDelay: Long = 1000L ): T { for (i in 1..tries) { try { return deferred().await() } catch (e: Exception) { if (i < tries) delay(timeDelay) else throw e } } throw UnsupportedOperationException() }
Implementação do operador:
- Ele adia
adiado .
- Você precisará chamar
async para obter esse objeto.
- Em vez de
adiado, você pode passar um bloco de suspensão e geralmente qualquer
função de suspensão.- O loop
for - você está aguardando o resultado da sua rotina. Se algo acontecer e o contador de repetições não estiver esgotado, tente novamente com
Atraso . Se não, então não.
A função pode ser facilmente personalizada: coloque um atraso exponencial ou passe uma função lambda que calculará o atraso dependendo das circunstâncias.
Use-o, ele funciona!
Fechos
Nós também frequentemente os encontramos. Aqui, novamente, tudo é simples:
suspend fun <T1, T2, R> zip( source1: Deferred<T1>, source2: Deferred<T2>, zipper: BiFunction<T1, T2, R>): R { return zipper.apply(sourcel.await(), source2.await()) } suspend fun <T1, T2, R> Deferred<T1>.zipWith( other: Deferred<T2>, zipper: BiFunction<T1, T2, R>): R { return zip(this, other, zipper) }
- Use o
zíper e ligue em espera no seu adiado.
- Em vez de adiado, você pode usar a função de suspensão e o construtor de corotina com withContext. Você transmitirá o contexto necessário.
Isso funciona novamente e espero ter removido esse medo.
Cache
Você tem uma implementação de cache em produção com o RxJava? Nós usamos o RxCache.

No diagrama à esquerda:
View e
ViewModel . À direita estão as fontes de dados: chamadas de rede e banco de dados.
Se queremos que algo seja armazenado em cache, o cache será outra fonte de dados.
Tipos de cache:
- Fonte de rede para chamadas de rede.
- Cache na memória .
- Cache persistente com expiração para ser armazenado no disco para que o cache sobreviva à reinicialização do aplicativo.
Vamos escrever um
cache simples e primitivo para o terceiro caso. O construtor Coroutine withContext vem em socorro novamente.
launch(UI) { var data = withContext(dispatcher) { persistence.getData() } if (data == null) { data = withContext(dispatcher) { memory.getData() } if (data == null) { data = withContext(dispatcher) { network.getData() } memory.cache(url, data) persistence.cache(url, data) } } }
- Você executa cada operação com withContext e verifica se algum dado está chegando.
- Se os dados da
persistência não vierem, você está tentando obtê-los do
memory.cache .
- Se também não houver memory.cache, entre em contato com a
fonte da
rede e obtenha seus dados. Não se esqueça, é claro, de colocar todos os caches.
Essa é uma implementação bastante primitiva e há muitas perguntas, mas o método funciona se você precisar de um cache em um único local. Para tarefas de produção, esse cache não é suficiente. Algo mais complicado é necessário.
Rx tem RxCache
Para aqueles que ainda usam o RxJava, você pode usar o RxCache. Nós ainda o usamos também.
RxCache é uma biblioteca especial. Permite armazenar em cache dados e gerenciar seu ciclo de vida.
Por exemplo, você quer dizer que esses dados expirarão após 15 minutos: "Por favor, após esse período de tempo, não envie dados do cache, mas envie-me novos dados".
A biblioteca é maravilhosa por apoiar declarativamente a equipe. A declaração é muito semelhante ao que você faz com o
Retrofit :
public interface FeatureConfigCacheProvider { @ProviderKey("features") @LifeCache(duration = 15, timeUnit = TimeUnit.MINUTES) fun getFeatures( result: Observable<Features>, cacheName: DynamicKey ): Observable<Reply<Features>> }
- Você diz que possui um
CacheProvider .
- Inicie um método e diga que a vida útil do
LifeCache é de 15 minutos. A chave pela qual estará disponível é
Recursos .
- Retorna
Observável <Responder , em que
Responder é um objeto de biblioteca auxiliar para trabalhar com cache.
O uso é bastante simples:
val restObservable = configServiceRestApi.getFeatures() val features = featureConfigCacheProvider.getFeatures( restObservable, DynamicKey(CACHE_KEY) )
- No cache Rx, acesse
RestApi .
-
Vá para
CacheProvider .
- Alimente-o com um observável.
- A própria biblioteca descobrirá o que fazer: vá para o cache ou não, se o tempo acabar, vá para
Observable e execute outra operação.
Usar a biblioteca é muito conveniente e eu gostaria de obter uma similar para a corotina.
Cache de Coroutine em desenvolvimento
No EPAM, estamos escrevendo a biblioteca
Coroutine Cache , que executará todas as funções do RxCache. Nós escrevemos a primeira versão e executamos dentro da empresa. Assim que o primeiro lançamento for lançado, terei prazer em publicá-lo no meu Twitter. Ficará assim:
val restFunction = configServiceRestApi.getFeatures() val features = withCache(CACHE_KEY) { restFunction() }
Teremos uma função de suspensão,
getFeatures . Passaremos a função como um bloco para uma função especial de ordem superior com o
Cache , que
descobrirá o que precisa ser feito.
Talvez façamos a mesma interface para suportar funções declarativas.
Tratamento de erros

O tratamento simples de erros é frequentemente encontrado pelos desenvolvedores e geralmente é resolvido de maneira simples. Se você não tem coisas complicadas, pegue uma
exceção e veja o que aconteceu lá, escreva no log ou mostre o erro ao usuário. Na interface do usuário, você pode fazer isso facilmente.
Em casos simples, tudo é esperado de maneira simples - o tratamento de erros com corotinas é feito através do
try-catch-finalmente .
Na produção, além de casos simples, existem:
-
try-catch aninhado,
- Muitos tipos diferentes de
exceções ,
- erros na rede ou na lógica de negócios,
- erros de usuário. Ele novamente fez algo errado e foi o culpado por tudo.
Nós devemos estar preparados para isso.
Existem 2 soluções:
CoroutineExceptionHandler e a abordagem com as
classes Result .
Manipulador de Exceção de Coroutine
Esta é uma classe especial para lidar com casos complexos de erros.
ExceptionHandler permite que você tome sua
exceção como argumento como um erro e lide com isso.
Como geralmente lidamos com erros complexos?
O usuário pressionou alguma coisa, o botão não funcionou. Ele precisa dizer o que deu errado e direcioná-lo para uma ação específica: verifique a Internet, o Wi-Fi, tente mais tarde ou exclua o aplicativo e nunca mais o use. Dizer isso ao usuário pode ser bastante simples:
val handler = CoroutineExceptionHandler(handler = { , error -> hideProgressDialog() val defaultErrorMsg = "Something went wrong" val errorMsg = when (error) { is ConnectionException -> userFriendlyErrorMessage(error, defaultErrorMsg) is HttpResponseException -> userFriendlyErrorMessage(Endpoint.EndpointType.ENDPOINT_SYNCPLICITY, error) is EncodingException -> "Failed to decode data, please try again" else -> defaultErrorMsg } Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show() })
- Vamos receber a mensagem padrão: "Algo deu errado!" e analise a exceção.
- Se for uma
ConnectionException, receberemos uma mensagem localizada dos recursos: “Cara, ligue o Wi-Fi e seus problemas desaparecerão. Eu garanto.
- Se o
servidor disser algo errado , você precisará informar ao cliente: “Desconecte-se e efetue login novamente” ou “Não faça isso em Moscou, faça isso em outro país” ou “Desculpe, camarada. Tudo o que posso fazer é dizer que algo deu errado.
- Se este é um
erro completamente
diferente , por exemplo,
falta de memória , dizemos: "Algo deu errado, desculpe".
- Todas as mensagens são exibidas.
O que você escreve no
CoroutineExceptionHandler será executado no mesmo
Dispatcher em que você executa a corotina. Portanto, se você der o comando da interface do usuário "launch", tudo acontecerá na interface do usuário. Você não precisa de
expedição separada
, o que é muito conveniente.
O uso é simples:
launch(uiDispatcher + handler) { ... }
Existe um operador
positivo . No contexto da Coroutine, adicione um
manipulador e tudo funcionará, o que é muito conveniente. Nós usamos isso por um tempo.
Classes de resultado
Mais tarde, percebemos que o CoroutineExceptionHandler pode estar ausente. O resultado, formado pelo trabalho da corotina, pode consistir em vários dados, de diferentes partes ou processar várias situações.
A abordagem de
classes Result ajuda a lidar com esse problema:
sealed class Result { data class Success(val payload: String) : Result() data class Error(val exception: Exception) : Result() }
- Na sua lógica de negócios, você inicia uma
classe Result .
- Marcar como
selado .
- Você herda da classe duas outras classes de dados:
Sucesso e
Erro .
—
Success , .
—
Error exception.
- :
override suspend fun doTask(): Result = withContext(CommonPool) { if ( !isSessionValidForTask() ) { return@withContext Result.Error(Exception()) } … try { Result.Success(restApi.call()) } catch (e: Exception) { Result.Error(e) } }
Coroutine context — Coroutine builder withContex .
, :
— , error. .
— RestApi -.
— ,
Result.Success .
— ,
Result.Error .
- , ExceptionHandler .
Result classes , . Result classes, ExceptionHandler try-catch.
3.
, .
unit- , , . unit-.
, . , unit-, 2 :
- Replacing context . , ;
- Mocking coroutines . .
Replacing context
presenter:
val login() { launch(UI) { … } }
,
login , UI-. , ,
. , , unit-.
:
val login (val coroutineContext = UI) { launch(coroutineContext) { ... } }
— login coroutineContext. , . Kotlin , UI .
— Coroutine builder Coroutine Contex, .
unit- :
fun testLogin() { val presenter = LoginPresenter () presenter.login(Unconfined) }
—
LoginPresenter login - , , Unconfined.
—
Unconfined , , . .
Mocking coroutines
— .
Mockk unit-. unit- Kotlin, . suspend-
coEvery -.
login
githubUser :
coEvery { apiClient.login(any()) } returns githubUser
Mockito-kotlin , — . , , :
given { runBlocking { apiClient.login(any()) } }.willReturn (githubUser)
runBlocking .
given- , .
Presenter :
fun testLogin() { val githubUser = GithubUser('login') val presenter = LoginPresenter(mockApi) presenter.login (Unconfined) assertEquals(githubUser, presenter.user()) }
— -, ,
GitHubUser .
— LoginPresenter API, . .
—
presenter.login Unconfined , Presenter , .
E isso é tudo! .
- Rx- . . , RxJava RxJava. - — , .
- . , . Unit- — , , , . — welcome!
- . , , , , . .
Links úteis
Notícias
30 Mail.ru . , .
AppsConf , .
, , , .
youtube- AppsConf 2018 — :)