Seguir las tendencias o avanzar hacia RxJava y LiveData



En el patio año 2018. Cada vez más, se encuentran las palabras RxJava y LiveData. Pero si sucedió que su aplicación aún se rige por soluciones anticuadas como la biblioteca de prioridad de trabajo de Android o AsyncTask (sí, sucede), entonces este artículo es especialmente para usted. Comparto estos enfoques basados ​​en su filosofía. El primero implica una cierta dependencia del trabajo en la pantalla, el segundo: la ejecución de una tarea en la que View la escucha y no interrumpe dependiendo de los eventos del ciclo de vida (por ejemplo, cuando se gira la pantalla). Bajo el corte, sugiero considerar la migración a un montón de RxJava y LiveData para ambos enfoques.

Para ilustrar, usaré la pequeña clase Work, que es una operación larga que necesita moverse al hilo de fondo. En una aplicación de prueba, coloqué un ProgressBar giratorio en la pantalla para ver cuándo se realizaría el trabajo en el hilo principal.

class Work { fun doWork() = try { for (i in 0 until 10) { Thread.sleep(500) } "work is done" } catch (e: InterruptedException) { "work is cancelled" } } 

AsyncTask


Con este enfoque, cada AsyncTask se crea para cada tarea, que se cancela en onPause () o onStop (). Esto se hace para que el contexto de actividad no se filtre. Para mostrar lo que significa esto, bosquejé un pequeño ejemplo.

Primero, modificamos un poco la AsyncTask estándar para que pueda deshacerse y devolver un error:

 class AsyncTaskCancellable<Params, Result>( private val job: Job<Params, Result>, private var callback: AsyncTaskCallback<Result>?) : AsyncTask<Params, Void, AsyncTaskCancellable.ResultContainer<Result>>(), WorkManager.Cancellable { interface Job<Params, Result> { fun execute(params: Array<out Params>): Result } override fun doInBackground(vararg params: Params): AsyncTaskCancellable.ResultContainer<Result> { return try { ResultContainer(job.execute(params)) } catch (throwable: Throwable) { ResultContainer(throwable) } } override fun onPostExecute(result: AsyncTaskCancellable.ResultContainer<Result>) { super.onPostExecute(result) if (result.error != null) { callback?.onError(result.error!!) } else { callback?.onDone(result.result!!) } } override fun cancel() { cancel(true) callback = null } class ResultContainer<T> { var error: Throwable? = null var result: T? = null constructor(result: T) { this.result = result } constructor(error: Throwable) { this.error = error } } } 

Agregue el lanzamiento del trabajo al administrador:

 class WorkManager { fun doWorkInAsyncTask(asyncTaskCallback: AsyncTaskCallback<String>): Cancellable { return AsyncTaskCancellable(object : AsyncTaskCancellable.Job<Void, String> { override fun execute(params: Array<out Void>) = Work().doWork() }, asyncTaskCallback).apply { execute() } } } 

Comenzamos la tarea, cancelando previamente la actual, si hay una:

 class MainActivity : AppCompatActivity() { ... loadWithAsyncTask.setOnClickListener { asyncTaskCancellable?.cancel() asyncTaskCancellable = workManager.doWorkInAsyncTask(object : AsyncTaskCallback<String> { override fun onDone(result: String) { onSuccess(result) } override fun onError(throwable: Throwable) { this@MainActivity.onError(throwable) } }) } ... } 

No olvides cancelarlo en onPause ():

 override fun onPause() { asyncTaskCancellable?.cancel() super.onPause() } 

Aquí AsyncTask se detiene y la devolución de llamada se restablece para borrar el enlace a MainActivity. Este enfoque es aplicable cuando necesita realizar una tarea rápida e insignificante, cuyo resultado no da miedo perder (por ejemplo, cuando voltea la pantalla cuando se recrea la actividad).

En RxJava, una implementación similar no diferirá mucho.

También creamos un Observable, que se ejecutará en Schedulers.computation () , y lo devolveremos para una suscripción adicional.

 class WorkManager { ... fun doWorkInRxJava(): Observable<String> { return Observable.fromCallable { Work().doWork() }.subscribeOn(Schedulers.computation()) } ... } 

Enviamos devoluciones de llamada a la transmisión principal y nos suscribimos al trabajo:

 class MainActivity : AppCompatActivity() { ... loadWithRx.setOnClickListener { _ -> rxJavaSubscription?.dispose() rxJavaSubscription = workManager.doWorkInRxJava() .observeOn(AndroidSchedulers.mainThread()) .subscribe({ onSuccess(it) }, { onError(it) }) } ... } 

No olvides limpiar por ti mismo en onPause ():

 override fun onPause() { rxJavaSubscription?.dispose() super.onPause() } 

En general, la implementación con RxJava puede complementarse ligeramente utilizando la biblioteca RxBinding. Proporciona enlaces reactivos para componentes de Android. En particular, en este caso, puede usar RxView.clicks () para obtener un Observable que le permita escuchar los clics de los botones:

 class MainActivity : AppCompatActivity() { ... rxJavaSubscription = RxView.clicks(loadWithRx) .concatMap { workManager.doWorkInRxJava() .observeOn(AndroidSchedulers.mainThread()) .doOnNext { result -> onSuccess(result) } .onErrorReturn { error -> onError(error) "" } } .subscribe() ... } 

El manejo de errores ocurre en la instrucción onErrorReturn para no terminar el flujo de eventos de clic en un botón. Por lo tanto, si se produce un error durante la ejecución del trabajo, no alcanzará la suscripción final y los clics continuarán procesándose.
Al implementar este enfoque, debe recordarse que el almacenamiento de Disposable, que devuelve subscribe (), en estático, debe abordarse con precaución. Hasta que se llame al método dispose (), puede almacenar enlaces implícitos a sus suscriptores, lo que puede provocar pérdidas de memoria.
También debe tener cuidado con el manejo de errores para no terminar accidentalmente la transmisión original.

Android-Priority-Jobqueue


Aquí tenemos un cierto gerente que administra las operaciones, y la pantalla se suscribe a su estado actual. LiveData, que está vinculado al ciclo de vida, es excelente para el papel de una capa entre dicho administrador y la interfaz de usuario. Para hacer el trabajo directamente en este ejemplo, sugiero usar RxJava, lo que facilita la transferencia de la ejecución del código al hilo de fondo.

También necesitaremos la clase auxiliar de contenedor de recursos, que contendrá información sobre el estado de la operación, el error y el resultado.

 class Resource<T> private constructor(val status: Status, val data: T?, val error: Throwable?) { constructor(data: T) : this(Status.SUCCESS, data, null) constructor(error: Throwable) : this(Status.ERROR, null, error) constructor() : this(Status.LOADING, null, null) enum class Status { SUCCESS, ERROR, LOADING } } 

Ahora estamos listos para escribir una clase WorkViewModel que contendrá la instancia de LiveData y notificarle los cambios en el estado del trabajo utilizando Resource. En el ejemplo, hice un poco de trampa e hice de WorkViewModel un singleton. Uso RxJava en estática, pero me suscribiré a través de LiveData, por lo que no habrá fugas.

 class WorkViewModel private constructor() { companion object { val instance = WorkViewModel() } private val liveData: MutableLiveData<Resource<String>> = MutableLiveData() private var workSubscription: Disposable? = null fun startWork(work: Work) { liveData.value = Resource() workSubscription?.dispose() workSubscription = Observable.fromCallable { work.doWork() } .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ liveData.value = Resource(it) }, { liveData.value = Resource(it) }) } fun getWork(): LiveData<Resource<String>> = liveData } 

Complementamos el WorkManager iniciando trabajos para mantener la uniformidad, es decir para que el trabajo siempre comience a través de este administrador:

 class WorkManager { ... fun doOnLiveData() { WorkViewModel.instance.startWork(Work()) } ... } 

Y agregamos interacción con todo esto en MainActivity. Desde WorkViewModel obtenemos el estado del trabajo actual, mientras la pantalla está animada, y comenzamos un nuevo trabajo presionando el botón:

 class MainActivity : AppCompatActivity() { ... WorkViewModel.instance.getWork().observe(this, Observer { when { it?.status == Resource.Status.SUCCESS -> onSuccess(it.data!!) it?.status == Resource.Status.ERROR -> onError(it.error!!) it?.status == Resource.Status.LOADING -> loadWithLiveData.isEnabled = false } }) loadWithLiveData.setOnClickListener { workManager.doOnLiveData() } ... } 

Aproximadamente de la misma manera, esto puede implementarse usando el Asunto de RxJava. Sin embargo, en mi opinión, LiveData maneja mejor el procesamiento del ciclo de vida, porque originalmente se ajustó para esto, mientras que con el Asunto puede tener muchos problemas para detener la transmisión y el manejo de errores. Creo que la simbiosis de RxJava y LiveData es la más viable: la primera recibe y procesa flujos de datos y notifica los cambios de la segunda, a la que ya puede suscribirse con un ojo en el ciclo de vida.

Por lo tanto, examinamos la transición de las bibliotecas arcaicas a las más modernas para los dos métodos más comunes de trabajo en el hilo de fondo. Para una operación menor de una sola vez, RxJava es perfecto, ya que le permite trabajar de manera muy flexible con los datos y controlar los flujos en los que esto debería ocurrir. Al mismo tiempo, si se requiere una interacción más fina con el ciclo de vida, es mejor usar LiveData, que fue diseñado originalmente para resolver dicho problema.

La versión completa de la fuente se puede encontrar aquí: GitHub

¡Estaré encantado de responder a sus preguntas en los comentarios!

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


All Articles