Cómo usar corutinas en la comida y dormir tranquilo por la noche

Las rutinas son una herramienta poderosa para la ejecución de código asíncrono. Trabajan en paralelo, se comunican entre sí y consumen pocos recursos. Parecería que sin temor, las corutinas pueden introducirse en la producción. Pero hay miedos e interfieren.

El informe de Vladimir Ivanov sobre AppsConf trata sobre el hecho de que el diablo no es tan terrible y que puedes usar corutinas en este momento:



Sobre el orador : Vladimir Ivanov ( dzigoro ) es un desarrollador líder de Android en EPAM con 7 años de experiencia, es aficionado a la arquitectura de soluciones, el desarrollo de React Native y iOS, y también tiene un certificado de Google Cloud Architect .

Todo lo que lees es un producto de producción de experiencia y varios estudios, así que tómalo como está, sin ninguna garantía.

Coroutines, Kotlin y RxJava


Para información: el estado actual de la corutina se encuentra en el lanzamiento, dejó Beta. Se lanzó Kotlin 1.3 , las corutinas se declararon estables y hay paz en el mundo.



Recientemente realicé una encuesta en Twitter que dice que las personas que usan corutina:

  • 13% de las corutinas en los alimentos. Todo esta bien;
  • El 25% los prueba en el proyecto de mascotas;
  • 24% - ¿Qué es Kotlin?
  • La mayor parte del 38% de RxJava está en todas partes.

Las estadísticas no son felices. Creo que RxJava es una herramienta demasiado compleja para tareas en las que los desarrolladores la usan comúnmente. Las rutinas son más adecuadas para controlar la operación asincrónica.

En mis informes anteriores, hablé sobre cómo refactorizar desde RxJava a corutinas en Kotlin, por lo que no me detendré en esto en detalle, sino que solo recordaré los puntos principales.

¿Por qué usamos corutinas?


Porque si usamos RxJava, los ejemplos de implementación habituales se verían así:

interface ApiClientRx { fun login(auth: Authorization) : Single<GithubUser> fun getRepositories (reposUrl: String, auth: Authorization) : Single<List<GithubRepository>> } //RxJava 2 implementation 

Tenemos una interfaz, por ejemplo, escribimos un cliente GitHub y queremos realizar un par de operaciones para él:

  1. Usuario de inicio de sesión.
  2. Obtenga una lista de repositorios de GitHub.

En ambos casos, las funciones devolverán objetos comerciales individuales: GitHubUser o una lista de GitHubRepository.

El código de implementación para esta interfaz es el siguiente:

 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 no haya pérdida de memoria.
- Agregar una llamada al primer método.
- Utilizamos operadores convenientes para obtener el usuario, por ejemplo flatMap .
- Obtenemos una lista de sus repositorios.
- Escribimos un Boilerplate para que se ejecute en los hilos correctos.
- Cuando todo esté listo, mostramos la lista de repositorios para el usuario conectado.

Dificultades del Código RxJava:

  • Complejidad En mi opinión, el código es demasiado complicado para la simple tarea de dos llamadas de red y mostrar algo en la interfaz de usuario .
  • Rastros de pila sin consolidar. Los seguimientos de pila casi no están relacionados con el código que escribe.
  • Recursos excesivos . RxJava genera muchos objetos debajo del capó y el rendimiento puede disminuir.

¿Cuál será el mismo código con las rutinas hasta la versión 0.26?

En 0.26, la API ha cambiado, y estamos hablando de producción. Nadie ha logrado aplicar 0.26 en productos, pero estamos trabajando en ello.

Con las rutinas, nuestra interfaz cambiará significativamente . Las funciones dejarán de devolver los Singles y otros objetos auxiliares. Inmediatamente devolverán objetos comerciales: GitHubUser y una lista de GitHubRepository. Las funciones GitHubUser y GitHubRepository tendrán modificadores de suspensión . Esto es bueno, porque suspender casi no nos obliga a nada:

 interface ApiClient { suspend fun login(auth: Authorization) : GithubUser suspend fun getRepositories (reposUrl: String, auth: Authorization) : List<GithubRepository> } //Base interface 

Si observa el código que ya utiliza la implementación de esta interfaz, cambiará significativamente en comparación con 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) } } } 

- La acción principal se lleva a cabo cuando llamamos al generador de coroutine async , esperamos una respuesta y obtenemos userlnfo .
- Utilizamos datos de este objeto.
- Realice otra llamada asincrónica y la llamada en espera .

Todo parece que no está sucediendo un trabajo asincrónico, y simplemente escribimos los comandos en la columna y se ejecutan. Al final, hacemos lo que hay que hacer en la interfaz de usuario.

¿Por qué son mejores las corutinas?

  • Este código es más fácil de leer. Está escrito como si fuera consistente.
  • Lo más probable es que el rendimiento de este código sea mejor que en RxJava.
  • Es muy sencillo escribir pruebas, pero las veremos un poco más tarde.

2 pasos a un lado


Digámonos un poco, hay un par de cosas que aún necesitan ser discutidas.

Paso 1. withContext vs launch / async


Además del generador de rutina asíncrono, hay un generador de rutina con Contexto .

Iniciar o asíncrono crea un nuevo contexto de rutina , que no siempre es necesario. Si tiene un contexto de rutina que desea utilizar en toda la aplicación, no necesita volver a crearlo. Simplemente puede reutilizar uno existente. Para hacer esto, necesitará un generador de rutina con Contexto. Simplemente reutiliza el contexto actual de Coroutine. Será 2-3 veces más rápido, pero ahora es una pregunta sin principios. Si los números exactos son interesantes, aquí está la pregunta sobre stackoverflow con puntos de referencia y detalles.

Regla general: Use withContext sin lugar a dudas donde encaja semánticamente. Pero si necesita una carga paralela, por ejemplo, varias imágenes o datos, la opción asíncrono / espera es su elección.

Paso 2. Refactorización


¿Qué pasa si refactoriza una cadena RxJava realmente compleja? Encontré esto en producción:

 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() 

Tenía una cadena complicada con un tema público , con cremallera y efectos secundarios en cada cremallera que enviaba algo más al autobús del evento. La tarea al menos era deshacerse del autobús del evento. Me senté por un día, pero no pude refactorizar el código para resolver el problema. La decisión correcta resultó arrojar todo y reescribir el código en la rutina en 4 horas .

El siguiente código es muy similar a lo que obtuve:

 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 } 

- Hacemos asíncrono para una tarea, para la segunda y la tercera.
- Estamos esperando el resultado y lo ponemos todo en un objeto.
- Listo!

Si tiene cadenas complejas y hay corutinas, simplemente refactorice. Es realmente rapido.

¿Qué impide que los desarrolladores usen corutinas en productos?


En mi opinión, a nosotros, como desarrolladores, actualmente se nos impide usar corutinas solo por temor a algo nuevo:

  • No sabemos qué hacer con el ciclo de vida , la actividad y el ciclo de vida fragmentado. ¿Cómo trabajar con corutinas en estos casos?
  • No hay experiencia en la resolución de tareas diarias complejas en la producción utilizando corutina.
  • No hay suficientes herramientas. Se han escrito un montón de bibliotecas y funciones para RxJava. Por ejemplo RxFCM . RxJava en sí tiene muchos operadores, lo cual es bueno, pero ¿qué pasa con la rutina?
  • Realmente no entendemos cómo probar las corutinas.

Si nos deshacemos de estos cuatro miedos, podemos dormir tranquilos por la noche y usar corutinas en la producción.

Vamos punto por punto.

1. Gestión del ciclo de vida.


  • Las rutinas pueden tener fugas como desechables o AsyncTask . Este problema debe resolverse manualmente.
  • Para evitar una excepción aleatoria de puntero nulo, se deben detener las corutinas.

Para


¿Estás familiarizado con Thread.stop () ? Si lo usaste, no por mucho tiempo. En JDK 1.1, el método se declaró obsoleto de inmediato, ya que es imposible tomar y detener una determinada pieza de código y no hay garantías de que se complete correctamente. Lo más probable es que solo obtenga daños en la memoria .

Por lo tanto, Thread.stop () no funciona . Necesita que la cancelación sea cooperativa, es decir, el código del otro lado para saber que la está cancelando.

Cómo aplicamos paradas con RxJava:

 private val compositeDisposable = CompositeDisposable() fun requestSmth() { compositeDisposable.add( apiClientRx.requestSomething() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> {}) } override fun onDestroy() { compositeDisposable.dispose() } 


En RxJava usamos CompositeDisposable .

- Agregue la variable compositeDisposable a la actividad en el fragmento o en el presentador, donde usamos RxJava.
- En onDestro, agregue Dispose y todas las excepciones desaparecen por sí solas.

Aproximadamente el mismo principio con las corutinas:

 private val job: Job? = null fun requestSmth() { job = launch(UI) { val user = apiClient.requestSomething() … } } override fun onDestroy() { job?.cancel() } 

Considere un ejemplo de una tarea simple .

Por lo general, los constructores de rutina devuelven un trabajo y, en algunos casos, difieren .

- Podemos memorizar este trabajo.
- Dar el comando "lanzar" generador de corutina . El proceso comienza, algo sucede, se recuerda el resultado de la ejecución.
- Si no pasamos nada más, "iniciar" inicia la función y nos devuelve un enlace al trabajo.
- Job es recordado, y en onDestroy decimos "cancelar" y todo funciona bien.

¿Cuál es el problema del enfoque? Cada trabajo necesita un campo. Debe mantener una lista de trabajos para cancelarlos todos juntos. El enfoque conduce a la duplicación de código, no lo haga.

La buena noticia es que tenemos alternativas : CompositeJob y un trabajo consciente del ciclo de vida .

CompositeJob es un análogo de compositeDisposable. Se parece a esto :

 private val job: CompositeJob = CompositeJob() fun requestSmth() { job.add(launch(UI) { val user = apiClient.requestSomething() ... }) } override fun onDestroy() { job.cancel() } 

- Para un fragmento, comenzamos un trabajo.
- Ponemos todos los trabajos en CompositeJob y le damos el comando: "job.cancel () para todos". .

El enfoque se implementa fácilmente en 4 líneas, sin contar la declaración de clase:

 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() } } 


Necesitarás:

- mapa con una clave de cadena,
- agregar método, en el que agregará trabajo,
- parámetro clave opcional.

Si desea utilizar la misma clave para el mismo trabajo, por favor. Si no, entonces hashCode resolverá nuestro problema. Agregue el trabajo al mapa, que pasamos, y cancele el anterior con la misma clave. Si llenamos demasiado la tarea, entonces el resultado anterior no nos interesa. Lo cancelamos y lo manejamos nuevamente.

Cancelar es simple: obtenemos el trabajo por clave y cancelamos. La segunda cancelación para todo el mapa cancela todo. Todo el código está escrito en media hora en cuatro líneas y funciona. Si no desea escribir, tome el ejemplo anterior.

Trabajo consciente del ciclo de vida


¿Has utilizado Android Lifecycle , Lifecycle owner u observador ?


Nuestra actividad y fragmentos tienen ciertos estados. Aspectos destacados: creado, iniciado y reanudado . Hay diferentes transiciones entre estados. LifecycleObserver le permite suscribirse a estas transiciones y hacer algo cuando ocurre una de las transiciones.

Se ve bastante simple:

 public class MyObserver implements LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) public void connectListener() { ... } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) public void disconnectListener() { … } } 

Cuelga la anotación con algún parámetro en el método, y se llama con la transición correspondiente. Simplemente use este enfoque para la rutina:

 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() } } 

- Puedes escribir la clase base AndroidJob .
- Transferiremos Lifecycle a la clase.
- La interfaz de LifecycleObserver implementará el trabajo.

Todo lo que necesitamos:

- En el constructor, agregue a Lifecycle como observador.
- Suscríbase a ON_DESTROY o cualquier otra cosa que nos interese.
- Hacer cancelar en ON_DESTROY.
- Obtenga un parentJob en su fragmento.
- Llame al constructor Joy jobs o al ciclo de vida de su fragmento de actividad. No hay diferencia
- Pase este parentJob como padre .

El código terminado se ve así:

 private var parentJob = AndroidJob(lifecycle) fun do() { job = launch(UI, parent = parentJob) { // code } } 

Cuando cancela padre, todas las corutinas hijo se cancelan y ya no necesita escribir nada en el fragmento. Todo sucede automáticamente, no más ON_DESTROY. Lo principal no olvide pasar parent = parentJob .

Si lo usa, puede escribir una regla de pelusa simple que lo resaltará: "¡Oh, olvidó a sus padres!"

Con   Gestión del ciclo de vida resuelto. Tenemos un par de herramientas que le permiten hacer todo esto de manera fácil y cómoda.

¿Qué pasa con los escenarios complejos y las tareas no triviales en la producción?

2. Casos de uso complejos


Los escenarios complejos y las tareas no triviales son:

- Operadores - operadores complejos en RxJava: flatMap, debounce, etc.
- Manejo de errores - manejo de errores complejos. No solo try..catch , sino también anidado, por ejemplo.
- El almacenamiento en caché es una tarea no trivial. En producción, nos encontramos con un caché y queríamos obtener una herramienta para resolver fácilmente el problema de almacenamiento en caché con las rutinas.

Repetir


Cuando pensamos en los operadores para la rutina, la primera opción era repetirCuando () .

Si algo salió mal y Corutin no pudo alcanzar el servidor interno, entonces queremos intentarlo varias veces con algún tipo de respaldo exponencial. Quizás la razón sea una conexión deficiente y obtendremos el resultado deseado repitiendo la operación varias veces.

Con las rutinas, esta tarea se implementa fácilmente:

 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() } 


Implementación del operador:

- Él toma diferido .
- Deberá llamar a async para obtener este objeto.
- En lugar de diferido, puede pasar un bloque de suspensión y, en general, cualquier función de suspensión.
- El bucle for : está esperando el resultado de su rutina. Si sucede algo y el contador de repetición no está agotado, intente nuevamente con Retraso . Si no, entonces no.

La función se puede personalizar fácilmente: ponga un Retardo exponencial o pase una función lambda que calculará el Retardo según las circunstancias.

¡Úselo, funciona!

Cremalleras


También a menudo los encontramos. Aquí nuevamente, todo es simple:

 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 la cremallera y la llamada en espera en su diferido.
- En lugar de diferido, puede usar la función de suspensión y el generador de corutina con withContext. Transmitirá el contexto que necesita.

Esto nuevamente funciona y espero haber eliminado este miedo.

Caché



¿Tiene una implementación de caché en producción con RxJava? Usamos RxCache.


En el diagrama de la izquierda: Ver y ViewModel . A la derecha están las fuentes de datos: llamadas de red y la base de datos.

Si queremos que algo se almacene en caché, entonces el caché será otra fuente de datos.

Tipos de caché:

  • Fuente de red para llamadas de red.
  • Caché en memoria .
  • El caché persistente con caducidad se almacenará en el disco para que el caché sobreviva al reinicio de la aplicación.

Escribamos un caché simple y primitivo para el tercer caso. El constructor de rutinas con Contexto viene al rescate nuevamente.

 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) } } } 

- Ejecutas cada operación con withContext y ves si hay datos.
- Si los datos de persistencia no llegan, está intentando obtenerlos de memory.cache .
- Si tampoco hay memoria.caché, póngase en contacto con la fuente de red y obtenga sus datos. No olvide, por supuesto, poner en todos los cachés.

Esta es una implementación bastante primitiva y hay muchas preguntas, pero el método funciona si necesita un caché en un lugar. Para tareas de producción, este caché no es suficiente. Se necesita algo más complicado.

Rx tiene RxCache


Para aquellos que todavía usan RxJava, pueden usar RxCache. Todavía lo usamos también. RxCache es una biblioteca especial. Le permite almacenar datos en caché y administrar su ciclo de vida.

Por ejemplo, desea decir que estos datos caducarán después de 15 minutos: "Por favor, después de este período de tiempo, no envíe datos desde el caché, sino envíeme datos nuevos".

La biblioteca es maravillosa porque declarativamente apoya al equipo. La declaración es muy similar a lo que haces con Retrofit :

 public interface FeatureConfigCacheProvider { @ProviderKey("features") @LifeCache(duration = 15, timeUnit = TimeUnit.MINUTES) fun getFeatures( result: Observable<Features>, cacheName: DynamicKey ): Observable<Reply<Features>> } 

- Dices que tienes un CacheProvider .
- Inicie un método y diga que la vida útil de LifeCache es de 15 minutos. La clave por la cual estará disponible es Características .
- Devuelve Observable <Reply , donde Reply es un objeto de biblioteca auxiliar para trabajar con caché.

El uso es bastante simple:

 val restObservable = configServiceRestApi.getFeatures() val features = featureConfigCacheProvider.getFeatures( restObservable, DynamicKey(CACHE_KEY) ) 

- Desde el caché Rx, acceda a RestApi .
- Diríjase a CacheProvider .
- Aliméntalo con un observable.
- La biblioteca en sí determinará qué hacer: ir al caché o no, si se agota el tiempo, recurrir a Observable y realizar otra operación.

Usar la biblioteca es muy conveniente y me gustaría obtener uno similar para la rutina.

Caché de rutina en desarrollo


Dentro de EPAM, estamos escribiendo la biblioteca Coroutine Cache , que realizará todas las funciones de RxCache. Escribimos la primera versión y la ejecutamos dentro de la empresa. Tan pronto como salga el primer lanzamiento, estaré encantado de publicarlo en mi Twitter. Se verá así:

 val restFunction = configServiceRestApi.getFeatures() val features = withCache(CACHE_KEY) { restFunction() } 

Tendremos una función de suspensión getFeatures . Pasaremos la función como un bloque a una función especial de orden superior con Cache , que determinará lo que hay que hacer.

Quizás haremos la misma interfaz para admitir funciones declarativas.

Manejo de errores




Los desarrolladores a menudo encuentran un manejo simple de errores y generalmente se resuelve de manera bastante simple. Si no tiene cosas complicadas, entonces en Captura, captura la excepción y mira lo que sucedió allí, escribe en el registro o muestra un error al usuario. En la interfaz de usuario, puede hacer esto fácilmente.

En casos simples, se espera que todo sea simple: el manejo de errores con corutinas se realiza a través de try-catch-finally .

En producción, además de casos simples, hay:

- Try -catch anidado,
- Muchos tipos diferentes de excepciones ,
- Errores en la red o en la lógica empresarial,
- Errores de usuario. Nuevamente hizo algo mal y tuvo la culpa de todo.

Debemos estar preparados para esto.

Hay 2 soluciones: CoroutineExceptionHandler y el enfoque con las clases de resultados .

Manejador de excepciones de rutina


Esta es una clase especial para manejar casos complejos de errores. ExceptionHandler le permite tomar su Excepción como un argumento como un error y manejarlo.

¿Cómo solemos manejar los errores complejos?

El usuario presionó algo, el botón no funcionó. Necesita decir qué salió mal y dirigirlo a una acción específica: verifique Internet, Wi-Fi, intente más tarde o elimine la aplicación y nunca la vuelva a usar. Decirle esto al usuario puede ser bastante simple:

 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() }) 

- Recibamos el mensaje predeterminado: "¡Algo salió mal!" y analizar la excepción.
- Si se trata de una ConnectionException, tomamos un mensaje localizado de los recursos: “Hombre, enciende el Wi-Fi y tus problemas desaparecerán. Te lo garantizo.
- Si el servidor dijo algo incorrecto , entonces debe decirle al cliente: “Cierre sesión e inicie sesión nuevamente”, o “No haga esto en Moscú, haga esto en otro país”, o “Lo siento, camarada. Todo lo que puedo hacer es decir que algo salió mal ".
- Si este es un error completamente diferente , por ejemplo, sin memoria , decimos: "Algo salió mal, lo siento".
- Se muestran todos los mensajes.

Lo que escriba en CoroutineExceptionHandler se ejecutará en el mismo Dispatcher donde ejecuta la corutina. Por lo tanto, si le da el comando de "inicio" a la interfaz de usuario, entonces todo sucede en la interfaz de usuario. No necesita un envío por separado , lo cual es muy conveniente.

El uso es simple:

 launch(uiDispatcher + handler) { ... } 

Hay un operador más . En el contexto de Coroutine, agregue un controlador y todo funciona, lo cual es muy conveniente. Usamos esto por un tiempo.

Clases de resultados


Más tarde nos dimos cuenta de que podría faltar el CoroutineExceptionHandler. El resultado, que está formado por el trabajo de la rutina, puede consistir en varios datos, de diferentes partes o procesar varias situaciones.

El enfoque de las clases de resultados ayuda a hacer frente a este problema:

 sealed class Result { data class Success(val payload: String) : Result() data class Error(val exception: Exception) : Result() } 

- En su lógica de negocios, comienza una clase de resultados .
- Marcar como sellado .
- Hereda de la clase otras dos clases de datos: Éxito y Error .
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 , .

¡Y eso es todo! .




  • Rx- . . , RxJava RxJava. - — , .
  • . , . Unit- — , , , . — welcome!
  • . , , , , . .


Enlaces utiles



Noticias

30 Mail.ru . , .

AppsConf , .

, , , .

youtube- AppsConf 2018 — :)

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


All Articles