Se resumen las rutinas de Kotlin y su uso en Android (cuatro corutinas se usan en combinación con Retrofit, Room, WorkManager)

Inserte la descripción de la imagen aquí

0 Pensando en diseñar una nueva arquitectura de aplicación de Android

He visto artículos como este antes: si rediseñara una aplicación, qué arquitectura usaría, qué bibliotecas de tres partes usaría, cómo encapsular un módulo para que otros lo usen, y desde Jetpack en Android, estos problemas parecen tener Respuesta más clara

Algunos motivos para estas consideraciones:

  • Simple y robusto

El uso de todos los frameworks o bibliotecas debería reducir la generación de código de plantilla, e incluso lograr el código de plantilla 0. No hay duda de que el lenguaje Kotlin se convertirá en la primera opción para el desarrollo nativo. A través de la seguridad vacía incorporada, biblioteca de funciones estándar, atributos extendidos y conveniente DSL personalizado, proxy de atributo y proxy de clase con soporte incorporado, alineación de métodos, clase de datos, etc.

Además del nivel de lenguaje, al usar algunas de las clases funcionales que vienen con la API de Android (como Lifecycle con soporte incorporado) y el uso de componentes de la arquitectura Jetpack (como LiveData y ViewModel), puede pasar al sistema la lógica de control que no está relacionada con la lógica comercial del programa. Procesamiento interno, reduzca su propio código para lidiar con fugas de memoria y punteros nulos.

Muchos otros componentes en Jetpack, como la biblioteca de enlace de vista ViewBinding, la sala de base de datos, el procesamiento de tareas en segundo plano WorkManager, etc., nos permiten reducir el código de la plantilla, al tiempo que proporcionan mecanismos de seguridad integrados para hacer que nuestro código sea más robusto

  • Fácil de leer y mantenible

Al mismo tiempo que el código es conciso, también debe ser fácil de leer, lo que definitivamente requiere que los miembros del equipo comprendan las nuevas tecnologías relacionadas.
Al mismo tiempo, son esenciales los casos de prueba completos, que pueden reducir en gran medida el tiempo de depuración y los riesgos de seguridad causados ​​por los cambios de código. Al mismo tiempo, el uso de la detección de código Lint de Android Studio y las reglas de detección de Lint personalizadas también pueden mejorar aún más el código Mantenibilidad

  • Hilo principal seguridad mian-safe

Este punto es principalmente para explicar el uso de las rutinas. Además de que todos sepan que puede reducir el anidamiento de la devolución de llamada y usar código aparentemente sincrónico para escribir lógica asincrónica, la seguridad del hilo principal también es un aspecto muy importante. Al transformar la operación que consume mucho tiempo en una función de suspensión, y el escritor de funciones utiliza el Despachador para especificar el subproceso utilizado por la función, entonces la persona que llama no necesita considerar si llamar a esta función afectará la seguridad del subproceso principal.

El siguiente es el enfoque de este artículo, cómo usar las rutinas combinadas con algunas bibliotecas de funciones para simplificar el código, mejorar la concisión del código (el código escrito por usted mismo se reduce) y la estabilidad (la estabilidad está garantizada por la lógica incorporada de cada biblioteca de funciones, en lugar de usarlo usted mismo) Control de código).

El enlace completo del código del proyecto del contenido involucrado en el artículo:
git clone https://github.com/googlecodelabs/kotlin-coroutines.git
se encuentra en el código_completo de la ruta coroutines-codelab

1 Corutinas en la habitación y modernización

Si no comprende Room, puede aprenderlo primero. Aquí hay un ejemplo directo.

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)

Al agregar una suspendpalabra clave frente a la función , Room proporcionará seguridad de subprocesos principales, a saber, main-safe, y la ejecutará automáticamente en un subproceso de fondo. Por supuesto, esta función solo se puede llamar en una rutina en este momento.

No, el uso de corutinas en Room es muy simple.

Sobre Retrofit, todos deberían estar familiarizados con él y dar ejemplos directos.

// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String

interface MainNetwork {
   @GET("next_title.json")
   suspend fun fetchNextTitle(): String
}

Además de agregar una suspendpalabra clave a la función en la interfaz , para la forma de valor de retorno de la función, el resultado original ajustado a la llamada se cambia a un tipo de resultado directo, al igual que String se devuelve arriba, por supuesto, también puede ser su Json personalizado Clase de datos.

El código antes de la transformación puede verse así:

suspend fun refreshTitle() {
   // interact with *blocking* network and IO calls from a coroutine
   withContext(Dispatchers.IO) {
       val result = try {
           // Make network request using a blocking call
           network.fetchNextTitle().execute()
       } catch (cause: Throwable) {
           // If the network throws an exception, inform the caller
           throw TitleRefreshError("Unable to refresh title", cause)
       }
      
       if (result.isSuccessful) {
           // Save it to database
           titleDao.insertTitle(Title(result.body()!!))
       } else {
           // If it's not successful, inform the callback of the error
           throw TitleRefreshError("Unable to refresh title", null)
       }
   }
}

El código modificado se ve así:

//TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

La transformación sigue siendo bastante simple. Aquí, Room utilizará el conjunto de consultas y el ejecutor de transacciones para ejecutar el cuerpo de rutina. Retrofit creará un nuevo objeto Call en el hilo de fondo y llamará a la cola para enviar la solicitud de forma asincrónica. Cuando el resultado regrese Reanudar la ejecución de la corutina.

Al mismo tiempo, Room y Retrofit proporcionan seguridad de subprocesos principales main-safe, por lo que no necesitamos usar withContext (Dispatcher.IO) al llamar ,

2 Use corutinas en funciones de orden superior

En el código anterior, aunque se ha simplificado mucho, pero si hay varias lógicas de solicitud, entonces necesita escribir un conjunto de lógica try-catch e inicialización de estado y asignación de excepciones. Estos también son códigos de plantilla. El código es el siguiente:

// MainViewModel.kt

fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           // 假设_spinner.value的赋值和其他异常逻辑都是通用的
           // 那么下面这行代码才是唯一需要关注的,
           repository.refreshTitle() 
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

En este momento, podemos usar el estilo de programación funcional para escribir una función de orden superior, encapsulando la lógica general de procesamiento comercial, de la siguiente manera:

private fun launchDataLoad(block: suspend () -> Unit): Job {
   return viewModelScope.launch {
       try {
           _spinner.value = true
           block()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Entonces, cuando finalmente lo llamamos en ViewModel, solo queda una línea clave de código:

// MainViewModel.kt

fun refreshTitle() {
   launchDataLoad {
       repository.refreshTitle()
   }
}

De hecho, al igual que usamos la programación funcional para escribir otras funciones de orden superior, aquí hay una suspendmodificación de palabra clave a los parámetros . Es decir, el suspendlambda puede llamar a la función de suspensión, que es cómo se implementan el inicio del generador de rutina y el bloqueo de ejecución.

// suspend lambda

block: suspend () -> Unit

3 Use corutinas y WorkManager juntos

WorkManager es parte de Android Jetpack . Usando la API de WorkManager, puede programar fácilmente tareas asincrónicas diferidas que deberían ejecutarse incluso cuando la aplicación se cierra o el dispositivo se reinicia.

Función principal:

  • Mayor compatibilidad con versiones anteriores de API 14
    • Use JobScheduler en dispositivos con API 23 y superior
    • Use BroadcastReceiver y AlarmManager en dispositivos que ejecutan API 14-22
  • Agregue restricciones de trabajo como la disponibilidad de la red o el estado de carga
  • Programe tareas asincrónicas periódicas o únicas
  • Monitoree y gestione las tareas planificadas
  • Enlace de tareas
  • Asegure la ejecución de la tarea, incluso cuando la aplicación o el dispositivo se reinicie
  • Siga las funciones de ahorro de energía, como el modo de bajo consumo de energía

WorkManager está diseñado para tareas que pueden ejecutarse de manera diferida (es decir, no necesitan ejecutarse de inmediato) y deben poder ejecutarse de manera confiable cuando la aplicación se cierra o el dispositivo se reinicia. Por ejemplo:

  • Enviar registros o analizar datos a servicios de fondo
  • Sincronice periódicamente los datos de la aplicación con el servidor

WorkManager no es adecuado para ejecutar trabajos en segundo plano que pueden finalizar de forma segura al final del proceso de aplicación, ni para tareas que deben ejecutarse de inmediato.

Tome el uso directamente como un CoroutineWorkerejemplo aquí , personalice una clase para RefreshMainDataWorkheredar CoroutineWorkery copie el doworkmétodo de la siguiente manera:

override suspend fun doWork(): Result {
   val database = getDatabase(applicationContext)
   val repository = TitleRepository(network, database.titleDao)

   return try {
       repository.refreshTitle()
       Result.success()
   } catch (error: TitleRefreshError) {
       Result.failure()
   }
}

Nota * CoroutineWorker.doWork () * es una función de suspensión, que es diferente del grupo de subprocesos de configuración utilizado por la clase Worker normal. Utiliza el despachador en el contexto de coroutine para controlar la programación de subprocesos (el valor predeterminado es Dispatchers.Default).

4 Sobre el manejo de la cancelación de la rutina y el tiempo de espera

Ninguno de los códigos que escribimos anteriormente tiene lógica sobre la cancelación de las corutinas, pero esto también es una parte esencial de la solidez del código. Aunque en la mayoría de los casos, podemos usar viewModelScope y lifecycleScope proporcionados por Android para cancelar la rutina interna al final del ciclo de vida de la página, todavía hay algunas situaciones que requieren que manejemos la lógica de cancelación y tiempo de espera nosotros mismos.

Esta parte puede referirse a la introducción del sitio web oficial de Kotlin: cancelación y tiempos de espera

Usando los métodos cancel y join o el método cancelAndJoin, podemos cancelar un trabajo de la siguiente manera:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // 在外部协程体中延迟1300毫秒,上面的job会先执行
    println("main: I'm tired of waiting!")
    job.cancel() // 取消当前的job
    job.join() // 等待直到这个job完成后结束 
    println("main: Now I can quit.")    
}

El registro impreso es:

trabajo: estoy durmiendo 0 ...
trabajo: estoy durmiendo 1 ...
trabajo: estoy durmiendo 2 ...
principal: ¡Estoy cansado de esperar!
principal: ahora puedo dejar de fumar.

Echemos un vistazo al código fuente de los métodos de cancelación y unión de Job:

abstract fun cancel(
    cause: CancellationException? = null
): Unit (source

abstract suspend fun join(): Unit (source)

Al cancelar, se puede proporcionar un parámetro de causa opcional para especificar un mensaje de error o proporcionar otra información detallada sobre el motivo de la cancelación de la depuración.
En cuanto a esta función de suspensión de unión y la función cancelAndJoin esperarán a que se complete toda la ejecución del cuerpo de rutina, incluida la lógica en el bloque try-finally.

Después de llamar al método de cancelación de un trabajo de rutina, solo marca su estado como cancelado y su lógica interna continuará ejecutándose. Este no debería ser el resultado que esperamos, como el siguiente código:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) { // computation loop, just wastes CPU
            // print a message twice a second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")    
}

El registro impreso es:

trabajo: estoy durmiendo 0 ...
trabajo: estoy durmiendo 1 ...
trabajo: estoy durmiendo 2 ...
principal: ¡Estoy cansado de esperar!
trabajo: estoy durmiendo 3 ...
trabajo: estoy durmiendo 4 ...
principal: ahora puedo dejar de fumar.

Hay dos formas de resolver el problema anterior: la primera es llamar periódicamente a la función de suspensión para verificar la cancelación. Para esto, una función de rendimiento es una buena opción. El otro es verificar explícitamente el estado de cancelación. Probemos el último método:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) { // 通过使用CoroutineScope的扩展属性isActive来使得该计算循环可取消
            // print a message twice a second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")    
}

El registro impreso es:

trabajo: estoy durmiendo 0 ...
trabajo: estoy durmiendo 1 ...
trabajo: estoy durmiendo 2 ...
principal: ¡Estoy cansado de esperar!
principal: ahora puedo dejar de fumar.

Use la función de suspensión nuevamente o arroje una excepción de Cancelar en el bloque try-finally, porque el cuerpo de rutina se ha cancelado en este momento. Aunque las operaciones comunes de liberación y cierre de recursos no bloquean y no introducirán llamadas a la función de suspensión, en casos extremos, al usar withContext (NonCancellable), la rutina cancelada puede suspenderse nuevamente, y luego Puede continuar llamando a la función de suspensión:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            withContext(NonCancellable) {
                println("job: I'm running finally")
                delay(1000L)
                println("job: And I've just delayed for 1 sec because I'm non-cancellable")
            }
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")    
}

Control de tiempo de espera

Por ejemplo, tenemos una solicitud de red que especifica 15 segundos como tiempo de espera. Después del tiempo de espera, necesitamos mostrar la interfaz de usuario de tiempo de espera. Luego podemos usar la función withTimeout de la siguiente manera:

import kotlinx.coroutines.*

fun main() = runBlocking {
    withTimeout(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
}

El registro impreso es el siguiente:

Estoy durmiendo 0 ...
estoy durmiendo 1 ...
estoy durmiendo 2 ...
Excepción en el hilo "main" kotlinx.coroutines.TimeoutCancellationException: Tiempo de espera agotado para 1300 ms

Entonces podemos usar * try {...} catch (e: TimeoutCancellationException) {...} * para manejar la lógica de tiempo de espera.
Al usar withTimeoutOrNull se devolverá nulo después del tiempo de espera. Con esta característica, también se puede usar para manejar la lógica de tiempo de espera.

fun main() = runBlocking {
    val result = withTimeoutOrNull(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
        "Done" // will get cancelled before it produces this result
    }
    println("Result is $result")
}

El registro impreso es el siguiente:

Estoy durmiendo 0 ...
estoy durmiendo 1 ...
estoy durmiendo 2 ... El
resultado es nulo

5 Sobre escribir casos de prueba

Después de todo, las corutinas siguen siendo algo nuevo para nosotros, y es inevitable que algo salga mal, por lo que para el código de corutina que escribimos, es esencial escribir más pruebas unitarias. La biblioteca kotlinx-coroutines-test puede ayudarnos a probar el código de corutina, aunque Está en la etapa de prueba, pero aún puede aprenderlo.
En vista del espacio limitado, las instrucciones oficiales de redacción de casos de prueba se publican temporalmente aquí:
Pruebas de las rutinas a través del comportamiento
Pruebas de las rutinas directamente

: :
Uso de Kotlin Coroutines en su aplicación de Android
Advanced Coroutines con Kotlin Flow y LiveData

82 artículos originales publicados · Me gusta 86 · Visita 110,000+

Supongo que te gusta

Origin blog.csdn.net/unicorn97/article/details/105170501
Recomendado
Clasificación