Experiencia con Coroutines y Retrofit2

Que es esto


¿Quién no ha leído la documentación todavía? Le recomiendo que se familiarice.


Lo que escribe Jetbrain:


Las rutinas simplifican la programación asincrónica, dejando todas las complicaciones dentro de las bibliotecas. La lógica del programa se puede expresar secuencialmente en corutinas, y la biblioteca base lo implementará de forma asincrónica para nosotros. La biblioteca puede ajustar las partes correspondientes del código de usuario en devoluciones de llamada que se suscriben a los eventos correspondientes y enviar la ejecución a diferentes subprocesos (¡o incluso a diferentes máquinas!). El código seguirá siendo tan simple como si se ejecutara estrictamente secuencialmente.

En términos simples, esta es una biblioteca para la ejecución de código síncrono / asíncrono.


Por qué


Porque RxJava ya no está de moda (es broma).


En primer lugar, quería probar algo nuevo y, en segundo lugar, me encontré con un artículo : una comparación de la velocidad de la corutina y otros métodos.


Ejemplo


Por ejemplo, debe realizar una operación en segundo plano.


Para comenzar, agregue a nuestra dependencia build.gradle de corutin:


dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"
....
}
view raw build.gradle hosted with ❤ by GitHub

Usamos el método en nuestro código:


suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
)
view raw gistfile1.txt hosted with ❤ by GitHub

Donde en contexto, especificamos el grupo de subprocesos que necesitamos, en casos simples, estos son IO, Main y Default

IO: para operaciones simples con la API, operaciones con la base de datos, preferencias compartidas, etc.
Principal: subproceso de interfaz de usuario, desde donde podemos acceder a la vista
Predeterminado: para operaciones pesadas con alta carga de CPU
(Más en este artículo )


Bloque: la lambda que queremos ejecutar


var result = 1.0
withContext(IO) {
for (i in 1..1000) {
result += i * i
}
}
Log.d("coroutines example", "result = $result")
view raw Example.kt hosted with ❤ by GitHub

En principio, eso es todo, obtenemos el resultado de la suma de cuadrados de 1 a 1000 y al mismo tiempo no bloqueamos el hilo principal, lo que significa que no hay ANR


Sin embargo, si nuestra rutina se ejecuta durante 20 segundos y durante este tiempo hemos realizado 2 vueltas del dispositivo, tendremos 3 bloques ejecutados simultáneamente. Ups


Y si pasamos un enlace a la actividad al bloque, una fuga y la falta de la capacidad de realizar operaciones con vista en bloques antiguos. Dos veces oops.


Entonces que hacer?


Mejor


private var viewModelJob = Job()
private val viewModelScope = CoroutineScope(Dispatchers.Main + viewModelJob)
fun doWork() {
var result = 1.0
viewModelScope.launch {
withContext(IO) {
for (i in 1..1000) {
result += i * i
}
}
}
Log.d("coroutines example", " result = $result")
}
fun cancelJob() {
viewModelJob.cancel()
}
view raw Example.kt hosted with ❤ by GitHub

Por lo tanto, pudimos detener la ejecución de nuestro código en la transmisión, por ejemplo, cuando se gira la pantalla.


CoroutineScope hizo posible combinar el alcance de todas las corutinas anidadas y, al llamar a job.cancel (), detuvo su ejecución


Si planea reutilizar el alcance después de detener la ejecución, debe usar job.cancelChildren () en lugar de job.cancel () gracias Neikist por su comentario


Al mismo tiempo, todavía tenemos la oportunidad de controlar los flujos:


fun doWork() {
var result = 1.0
var result2 = 1.0
viewModelScope.launch {
withContext(IO) {
for (i in 1..1000) {
result += i * i
}
}
withContext(Default) {
for (i in 1..1000) {
result2 += i * i
}
}
}
Log.d("coroutines example", "running result = $result, result 2 = $result2")
}
fun cancelJob() {
viewModelJob.cancel()
}
view raw Example.kt hosted with ❤ by GitHub

Conectamos retrofit2


Añadir dependencias al granizo:


dependencies {
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
implementation "com.squareup.retrofit2:converter-moshi:$converterMoshiVersion"
implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$retrofitCoroutinesVersion"
...
}
view raw build.gradle hosted with ❤ by GitHub

Usamos la pluma https://my-json-server.typicode.com/typicode/demo/posts como ejemplo


Describimos la interfaz de actualización:


interface RetrofitPosts {
@GET("posts")
fun getPosts(): Deferred<Response<List<Post>>>
}
view raw RetrofitPosts.kt hosted with ❤ by GitHub

Describa el modelo de publicación devuelto:


data class Post(val id: Int, val title: String)
view raw Post.kt hosted with ❤ by GitHub

Nuestro BaseRepository:


abstract class BaseRepository<Params, Result> {
abstract suspend fun doWork(params: Params): Result
}
view raw BaseRepository.kt hosted with ❤ by GitHub

Implementación de PostsRepository:


class PostsRepository :
BaseRepository<PostsRepository.Params, PostsRepository.Result>() {
override suspend fun doWork(params: Params): Result {
val retrofitPosts = Retrofit
.Builder()
.baseUrl("https://jsonplaceholder.typicode.com")
.addConverterFactory(MoshiConverterFactory.create())
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
.create(RetrofitPosts::class.java)
val result = retrofitPosts
.getPosts()
.await()
return Result(result.body())
}
class Params
data class Result(val posts: List<Post>?)
}
view raw PostsRepository.kt hosted with ❤ by GitHub

Nuestra BaseUseCase:


abstract class BaseUseCase<Params, Result> {
abstract suspend fun doWork(params: Params): Result
}
view raw BaseUseCase.kt hosted with ❤ by GitHub

Implementación de GetPostsListUseCase:


class GetListOfPostsUseCase
: BaseUseCase<GetListOfPostsUseCase.Params, GetListOfPostsUseCase.Result>() {
override suspend fun doWork(params: Params): Result {
return Result(
PostsRepository()
.doWork(PostsRepository.Params())
.response
.posts
)
}
class Params
class Result(val posts: List<Post>?)
}

Aquí está el resultado:


fun doWork() {
val useCase = GetListOfPostsUseCase()
viewModelScope.launch {
withContext(Dispatchers.IO) {
val result = useCase.doWork(
GetListOfPostsUseCase.Params()
)
Log.d("coroutines example", "get list of posts = ${result.posts}")
}
}
}
fun cancelJob() {
viewModelJob.cancel()
}
view raw Example.kt hosted with ❤ by GitHub

Haciéndolo mejor


Soy una criatura perezosa y cada vez que no quiero arrastrar toda la hoja de código, hice los métodos necesarios en BaseViewModel:


abstract class BaseViewModel : ViewModel() {
private var viewModelJob = Job()
private val viewModelScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private var isActive = true
// Do work in IO
fun <P> doWork(doOnAsyncBlock: suspend CoroutineScope.() -> P) {
doCoroutineWork(doOnAsyncBlock, viewModelScope, IO)
}
// Do work in Main
// doWorkInMainThread {...}
fun <P> doWorkInMainThread(doOnAsyncBlock: suspend CoroutineScope.() -> P) {
doCoroutineWork(doOnAsyncBlock, viewModelScope, Main)
}
// Do work in IO repeately
// doRepeatWork(1000) {...}
// then we need to stop it calling stopRepeatWork()
fun <P> doRepeatWork(delay: Long, doOnAsyncBlock: suspend CoroutineScope.() -> P) {
isActive = true
viewModelScope.launch {
while (this@BaseViewModel.isActive) {
withContext(IO) {
doOnAsyncBlock.invoke(this)
}
if (this@BaseViewModel.isActive) {
delay(delay)
}
}
}
}
fun stopRepeatWork() {
isActive = false
}
override fun onCleared() {
super.onCleared()
isActive = false
viewModelJob.cancel()
}
private inline fun <P> doCoroutineWork(
crossinline doOnAsyncBlock: suspend CoroutineScope.() -> P,
coroutineScope: CoroutineScope,
context: CoroutineContext
) {
coroutineScope.launch {
withContext(context) {
doOnAsyncBlock.invoke(this)
}
}
}
}
view raw BaseViewModel.kt hosted with ❤ by GitHub

Ahora obtener la lista de publicaciones se ve así:


class PostViewModel : BaseViewModel() {
val lengthOfPostsList = MutableLiveData<String>()
fun getListOfPosts() {
doWork {
val result = GetListOfPostsUseCase()
.doWork(GetListOfPostsUseCase.Params())
Log.d("coroutines example", "get list of posts = ${result.posts}")
lengthOfPostsList.postValue(result.posts?.size.toString())
}
}
view raw PostViewModel.kt hosted with ❤ by GitHub

Conclusión


Usé corutinas en el producto y el código realmente resultó ser más limpio y más legible.


UPD:
Descripción de manejo de excepciones de modificación ver comentario

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


All Articles