Como usar corotinas na comida e dormir tranquilamente à noite

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>> } //RxJava 2 implementation 

Temos uma interface, por exemplo, escrevemos um cliente GitHub e queremos executar algumas operações para ele:

  1. Usuário de logon.
  2. 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> } //Base interface 

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) -> { // side effects return true; }).doOnError { // handle errors } .zipWith(observable3.getSubject(), (t3, t4) -> { // side effects return true; }).doOnComplete { // gather data } .subscribe() 

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) { // handle errors } 

- 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) { // code } } 

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 .
- 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 :

  1. Replacing context . , ;
  2. 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 — :)

Source: https://habr.com/ru/post/pt429908/


All Articles