Suspensión sobre bloqueo

Este artículo tiene como objetivo mostrar cómo usar Kotlin Coroutines y eliminar Reaxtive eXtensions (Rx) .


Beneficios


Para comenzar, consideremos cuatro beneficios de Coroutines sobre Rx:


Suspensión sobre bloqueo


Para ejecutar código sin bloqueo usando Rx, escribirías algo como esto:


Observable.interval(1, TimeUnit.SECONDS) .subscribe { textView.text = "$it seconds have passed" } 

Lo que efectivamente está creando un nuevo hilo. Los hilos son objetos pesados ​​en términos de memoria y rendimiento.


Ambos son críticos en el mundo del desarrollo móvil.


Puede lograr el mismo comportamiento con el siguiente fragmento:


 launch { var i = 0 while (true){ textView.text = "${it++} seconds have passed" delay(1000) } } 

Esencialmente, las Coroutinas son hilos livianos pero no creamos ningún hilo real.
Aquí estamos usando la función delay () sin bloqueo, que es una función de suspensión especial que no bloquea un hilo sino que suspende la rutina.


Manejo de contrapresión natural sobre manual


La contrapresión es cuando los observables producen elementos más rápidamente de lo que sus observadores los consumen.
Al usar Rx, debe especificar explícitamente cómo tratará la contrapresión.
Hay 2 enfoques básicos:


  • Utilice aceleradores, amortiguadores u operadores de Windows
  • El modelo de tracción reactiva

Mientras que las Coroutinas pueden suspender, proporcionan una respuesta natural al manejo de la contrapresión.
Por lo tanto, no se requieren acciones adicionales.


Estilo de código de sincronización sobre asíncrono


La naturaleza básica de una aplicación móvil es reaccionar a las acciones del usuario. Es por eso que Reactive eXtensions sería una buena opción.


Sin embargo, debe escribir un código en un estilo funcional. Si solía escribir en un estilo imperativo, podría ser un poco difícil.


Mientras que Coroutines le permite escribir código asíncrono como si fuera una función de sincronización habitual. Por ejemplo,


 suspend fun showTextFromRemote() { val text = remote.getText() textView.text = text } 

Incluso si estoy trabajando con estilo funcional durante mucho tiempo, aún es más fácil leer y depurar un código imperativo.


Nativo sobre lib de terceros


Las corutinas son una característica incorporada nativa de Kotlin.


No tiene que agregar ninguna dependencia adicional. Actualmente, todas las bibliotecas principales podrían ocuparse de las rutinas.


Por ejemplo,


Retrofit


 interface Api { @Get("users") suspend fun loadUsers() : List<User> } 

Habitacion


 interface Dao { @Update suspend fun update(user: UserEntity) } 

Por lo tanto, puede crear una aplicación que se suspenda por completo, comenzando la capa de interfaz de usuario, a través del dominio y terminando en la capa de datos.


La aplicación


Bajemos a los negocios. Crearemos una aplicación clásica de detalles maestros.
La primera página contendría una lista infinita de entregas.
Al hacer clic en el elemento, abriremos una página de detalles.
Además, admitiremos el modo fuera de línea: todos los datos se almacenarán en caché.
Además, utilizaré la arquitectura MVVM, donde el papel de ViewModel lo desempeña Fragment en lugar de ViewModel de AAC. Hay varias razones:
Los fragmentos suelen ser muy calvos, solo vincula viewModel a XML.


Las características como configurar el color de la barra de estado no se podían hacer en AAC ViewModel: debe activar el método de fragmento. El uso de fragmentos como ViewModel nos permitiría almacenar toda la funcionalidad relacionada (administrar una pantalla determinada) en una clase.


Primero, creemos BaseViewModel:


 abstract class BaseViewModel<B : BaseBindings, V : ViewDataBinding> : Fragment(), CoroutineScope by CoroutineScope(Dispatchers.IO){ protected abstract val layoutId: Int protected abstract val bindings: B protected lateinit var viewBinding: V override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) retainInstance = true } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { viewBinding = DataBindingUtil.inflate(inflater, layoutId, container, false) return viewBinding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewBinding.lifecycleOwner = viewLifecycleOwner viewBinding.setVariable(BR.bindings, bindings) } override fun onDestroy() { cancel() super.onDestroy() } } 

Marcamos nuestro ViewModel como CoroutineScope para que podamos comenzar las corutinas dentro de los modelos de vista y cualquier corutina lanzada se limitaría al ciclo de vida de un fragmento.


Tenemos que especificar explícitamente el fin del ciclo de vida del alcance llamando al método cancel() para cancelar todas las solicitudes en ejecución para evitar pérdidas de memoria.


Configuramos retainInstance = true para que en la configuración los fragmentos no se retainInstance = true crear para que podamos completar todas las solicitudes de larga duración.


Además, tenemos que establecer lifecycleOwner en enlace para activar el enlace de datos bidireccional .


Manejo de excepciones


De acuerdo con la documentación de Coroutines:


 Coroutine builders come in two flavors: propagating exceptions automatically (launch and actor) or exposing them to users (async and produce). The former treat exceptions as unhandled, similar to Java's Thread.uncaughtExceptionHandler 

Dado que estamos utilizando el generador de lanzamiento en la mayoría de los casos, tenemos que especificar CoroutineExceptionHandler
CoroutineExceptionHandler es CoroutineContext.Element que podría usarse para construir un contexto de rutina utilizando el operador más.
Declararé el controlador estático de la siguiente manera:


 val exceptionHandler = CoroutineExceptionHandler { _, throwable -> Timber.e(throwable) } 

Y cambie BaseViewModel:


 abstract class BaseViewModel<B : BaseBindings, V : ViewDataBinding> : Fragment(), CoroutineScope by CoroutineScope(Dispatchers.IO + exceptionHandler) 

De aquí en adelante, cualquier excepción que se produzca en la rutina lanzada dentro del alcance de ViewModel se entregará al controlador dado.
A continuación, necesito declarar mi API y DAO:


 interface DeliveriesApi { @GET("deliveries") suspend fun getDeliveries(@Query("offset") offset: Int, @Query("limit") limit: Int): List<DeliveryResponse> } @Dao interface DeliveryDao { @Query("SELECT * FROM ${DeliveryEntity.TABLE_NAME}") fun getAll(): DataSource.Factory<Int, DeliveryEntity> @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(delivery: DeliveryEntity) } 

Como puede ver, marqué los métodos como suspendidos para que podamos declarar los objetos de respuesta esperados. Además, la cancelación de la rutina de los padres también cancelará las llamadas a la red.
Lo mismo para DAO.
La única diferencia es que queremos proporcionar la capacidad de observar la base de datos.
La forma más fácil es utilizar el soporte de datos en vivo incorporado. Pero si marcamos getAll () como suspendido, causaría un error de compilación
error:


 Not sure how to convert a Cursor to this method's return type ... 

Aquí no necesitamos suspender porque:


  • Las solicitudes de base de datos se realizan en segundo plano de forma predeterminada
  • LiveData resultante es consciente del ciclo de vida, por lo que no necesitamos cancelarlo manualmente

Tenemos que combinar de alguna manera fuentes de datos remotas y locales.
Vale la pena recordarlo: debe haber un único punto de verdad.
Según el diseño fuera de línea , sería el almacenamiento local. Entonces, observaríamos el estado de la base de datos. Cuando no hay nada que recuperar, solicitamos datos del control remoto y los insertamos en la base de datos.
Introduciremos la clase Listado


 data class Listing<T>( val pagedList: LiveData<PagedList<T>>, val dataState: LiveData<DataState>, val refreshState: LiveData<DataState>, val refresh: () -> Unit, val retry: () -> Unit ) 

Vamos val por val:


  • pagedList: los datos principales que se construyen como PagedList para permitir el desplazamiento infinito y se envuelven con LiveData para permitir la observación de datos
  • dataState: uno de los tres estados en los que nuestros datos pueden ser: Correcto, En ejecución, Error. También envuelto en LiveData para observar cambios
  • refreshState: cuando activamos la actualización de datos mediante deslizar para actualizar, necesitamos alguna herramienta con la que podamos distinguir entre los comentarios de solicitud de actualización y los comentarios de solicitud de la página siguiente. Para el primero, queremos mostrar un error al final de la lista, pero para un error de actualización, queremos mostrar un mensaje brindis y ocultar un cargador.
  • refresh (): devolución de llamada para activar al deslizar para actualizar
  • retry (): devolución de llamada para desencadenar en pagedList error de carga
    A continuación, modelo de vista de lista:


     class DeliveryListViewModel : BaseViewModel<DeliveryListBindings, DeliveryListBinding>(), DeliveryListBindings, DeliveryListItemBindings, DeliveryListErrorBindings { override val layoutId: Int = R.layout.delivery_list override val bindings: DeliveryListBindings = this private val deliveryGateway: DeliveryGateway by inject { parametersOf(this) } private val listing = deliveryGateway.getDeliveries() override val dataState = listing.dataState override val isRefreshing = Transformations.switchMap(listing.refreshState) { MutableLiveData(it == DataState.Loading) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupList() setupRefresh() } private fun setupList() { val adapter = DeliveriesAdapter(this, this) viewBinding.deliveries.adapter = adapter viewBinding.deliveries.setHasFixedSize(true) listing.pagedList.observe(viewLifecycleOwner, Observer { adapter.submitList(it) }) listing.dataState.observe(viewLifecycleOwner, Observer { adapter.updateDataState(it) }) } private fun setupRefresh() { listing.refreshState.observe(viewLifecycleOwner, Observer { if (it is DataState.Error) { Toast.makeText(context, it.message, LENGTH_SHORT).show() } }) } override fun refresh() { listing.refresh() } override fun onDeliveryClicked(delivery: Delivery) { view?.findNavController()?.navigate(DeliveryListViewModelDirections.toDetails(delivery)) } override fun onRetryClicked() { listing.retry() } } 

    Comencemos por la declaración de clase.



En primer lugar, DeliveryListBindings y DeliveryListBinding. Primero está nuestra interfaz declarada para pegar el modelo de vista con vista XML. La segunda es la clase autogenerada basada en XML. Necesitamos el segundo para configurar nuestra interfaz de enlaces y nuestro ciclo de vida en XML.


Además, es una buena práctica hacer referencia a vistas utilizando este enlace autogenerado en lugar de utilizar el sintético de Kotlin.


Podría existir el caso cuando la referencia a través de la vista sintética no existe en la vista actual. Con el enlace de datos, fallará rápidamente incluso en la etapa de compilación.


A continuación, tres interfaces: DeliveryListBindings, DeliveryListItemBindings, DeliveryListErrorBindings.


  1. DeliveryListBindings : enlaces para la pantalla misma. Por ejemplo, contiene el método refresh () que se llama al deslizar verticalmente.
  2. DeliveryListItemBindings : enlaces para un elemento de la lista. Por ejemplo, onClicked ()
  3. DeliveryListErrorBindings : enlaces para la vista de error, que también es el elemento de la lista que se muestra en el estado de error. Por ejemplo, contiene el método retry ()

Por lo tanto, estamos manejando todo en el modelo de vista única, ya que es una pantalla única pero también siguiendo el principio de segregación de interfaz


Pongamos especial atención a esta línea:


 private val deliveryGateway: DeliveryGateway by inject { parametersOf(this) } 

DeliveryGateway necesita realizar solicitudes fuera del hilo principal. Por lo tanto, necesita declarar los métodos como suspendidos o CoroutineScope para lanzar nuevas rutinas en este ámbito. Elegiríamos el segundo enfoque ya que necesitamos nuestros LiveData desde el principio y luego solo esperaríamos las actualizaciones. Es muy similar a suscribirse a la instancia de liveData cuando pasamos lifecycleOwner (que a menudo se refiere a 'esto'). Aquí estamos de la misma manera que estamos pasando 'esto' como CoroutineScope


La interfaz CoroutineScope consta de un único campo: CoroutineContext. En esencia, un alcance y un contexto son las mismas cosas. La diferencia entre un contexto y un alcance está en su propósito previsto.


Para obtener más información sobre esto, recomendaría un artículo de Roman Elizarov. Por lo tanto, proporcionar alcance a DeliveryGateway también dará como resultado el uso del mismo contexto. Específicamente el manejador de hilos, trabajos y excepciones.
Ahora echemos un vistazo a DeliveryGateway:


 class DeliveryBoundGateway( private val db: DataBase, private val api: DeliveriesApi, private val deliveryDao: DeliveryDao, private val coroutineScope: CoroutineScope ) : DeliveryGateway { private val boundaryCallback = DeliveriesBoundaryCallback( api = api, coroutineScope = coroutineScope, handleResponse = { insertIntoDatabase(it) } ) @MainThread override fun getDeliveries(): Listing<Delivery> { val refreshTrigger = MutableLiveData<Unit>() val refreshState = Transformations.switchMap(refreshTrigger) { refresh() } val pagingConfig = Config( initialLoadSizeHint = PAGE_SIZE, pageSize = PAGE_SIZE, prefetchDistance = PAGE_SIZE ) val deliveries = deliveryDao.getAll() .toLiveData( config = pagingConfig, boundaryCallback = boundaryCallback ) return Listing( pagedList = deliveries, dataState = boundaryCallback.dataState, retry = { boundaryCallback.helper.retryAllFailed() }, refresh = { refreshTrigger.value = null }, refreshState = refreshState ) } /** * When refresh is called, we simply run a fresh network request and when it arrives, clear * the database table and insert all new items in a transaction. * <p> * Since the PagedList already uses a database bound data source, it will automatically be * updated after the database transaction is finished. */ @MainThread private fun refresh(): LiveData<DataState> { boundaryCallback.refresh() val dataState = MutableLiveData<DataState>() dataState.value = DataState.Loading coroutineScope.launch { try { val deliveries = api.getDeliveries(0, PAGE_SIZE) db.withTransaction { deliveryDao.clear() insertIntoDatabase(deliveries) } dataState.postValue(DataState.Loaded) } catch (throwable: Throwable) { Timber.w(throwable) dataState.postValue(DataState.Error(throwable.message)) } } return dataState } private suspend fun insertIntoDatabase(deliveries: List<DeliveryResponse>) { deliveries.forEach { delivery -> val entity = deliveryConverter.fromNetwork(delivery) deliveryDao.insert(entity) } } companion object { const val PAGE_SIZE = 20 } } 

Aquí estamos construyendo la estructura de LiveData desde el principio y luego usando datos de carga de corutinas y publicarlo en LiveData. Además, estamos utilizando la implementación de PagedList.BoundaryCallback () para pegar la base de datos local y la API remota. Cuando llegamos al final de la lista paginada, se activa boundaryCallback y carga la siguiente porción de datos.

Como puede ver, estamos usando coroutineScope para lanzar nuevas corutinas.


Dado que este alcance es igual al ciclo de vida del fragmento, todas las solicitudes pendientes se cancelarían en la devolución de llamada onDestroy() del fragmento.


La página de detalles de entrega es bastante sencilla: simplemente pasamos un objeto de entrega como Parcelable desde la pantalla maestra usando el complemento de guardado de args del componente de navegación. En la pantalla de detalles, simplemente enlace dado un objeto a un XML.


 class DeliveryViewModel : BaseViewModel<DeliveryBindings, DeliveryBinding>(), DeliveryBindings { override val layoutId: Int = R.layout.delivery override val bindings: DeliveryBindings = this private val args: DeliveryViewModelArgs by navArgs() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewBinding.delivery = args.delivery viewBinding.image.clipToOutline = true } } 

Contactame


Aquí está el enlace al código fuente de github.


Puede dejar comentarios y cuestiones abiertas.

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


All Articles