Expérience avec Coroutines et Retrofit2

Qu'est ce que c'est


Qui n'a pas encore lu la documentation - je vous recommande fortement de vous familiariser.


Ce que Jetbrain écrit:


Les coroutines simplifient la programmation asynchrone, laissant toutes les complications Ă  l'intĂ©rieur des bibliothĂšques. La logique du programme peut ĂȘtre exprimĂ©e sĂ©quentiellement en coroutines, et la bibliothĂšque de base l'implĂ©mentera de maniĂšre asynchrone pour nous. La bibliothĂšque peut encapsuler les parties correspondantes du code utilisateur dans des rappels souscrivant aux Ă©vĂ©nements correspondants et envoyer l'exĂ©cution Ă  diffĂ©rents threads (ou mĂȘme Ă  diffĂ©rentes machines!). Le code restera aussi simple que s'il Ă©tait exĂ©cutĂ© strictement sĂ©quentiellement.

En termes simples, il s'agit d'une bibliothÚque pour l'exécution de code synchrone / asynchrone.


Pourquoi?


Parce que RxJava n'est plus Ă  la mode (je plaisante).


PremiÚrement, je voulais essayer quelque chose de nouveau, et deuxiÚmement, je suis tombé sur un article - une comparaison de la vitesse de la corutine et d'autres méthodes.


Exemple


Par exemple, vous devez effectuer une opération en arriÚre-plan.


Pour commencer - ajoutez à notre dépendance build.gradle à la corutine:


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

Nous utilisons la méthode dans notre code:


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

OĂč dans le contexte - nous spĂ©cifions le pool de threads dont nous avons besoin - dans les cas simples, ce sont IO, Main et Default

IO - pour les opérations simples avec l'API, les opérations avec la base de données, les préférences partagées, etc.
Principal - Fil d'interface utilisateur, d'oĂč nous pouvons accĂ©der Ă  la vue
Par défaut - pour les opérations lourdes avec une charge CPU élevée
(Plus dans cet article )


Block - le lambda que nous voulons exécuter


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 principe, c'est tout, nous obtenons le rĂ©sultat de la somme des carrĂ©s de 1 Ă  1000 et en mĂȘme temps nous ne bloquons pas le thread principal, ce qui signifie pas d'ANR


Cependant, si notre coroutine est exécutée pendant 20 secondes et pendant ce temps, nous avons effectué 2 tours de l'appareil, nous aurons alors 3 blocs en cours d'exécution. Oups


Et si nous passions un lien vers l'activité vers le bloc - une fuite et le manque de capacité à effectuer des opérations avec vue dans les anciens blocs. Oups deux fois.


Alors que faire?


Faire mieux


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

Ainsi, nous avons pu arrĂȘter l'exĂ©cution de notre code dans le flux, par exemple, lorsque l'Ă©cran est tournĂ©.


CoroutineScope a permis de combiner la portĂ©e de toutes les coroutines imbriquĂ©es et, lors de l'appel de job.cancel (), a arrĂȘtĂ© leur exĂ©cution


Si vous prĂ©voyez de rĂ©utiliser la portĂ©e aprĂšs l'arrĂȘt de l'exĂ©cution, vous devez utiliser job.cancelChildren () au lieu de job.cancel () merci Neikist pour le commentaire


Dans le mĂȘme temps, nous avons encore la possibilitĂ© de contrĂŽler les flux:


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

Nous connectons retrofit2


Ajoutez des dĂ©pendances Ă  la grĂȘle:


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

Nous utilisons le stylet https://my-json-server.typicode.com/typicode/demo/posts comme exemple


Nous décrivons l'interface de retrofit:


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

Décrivez le modÚle de message retourné:


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

Notre référentiel de base:


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

Implémentation 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

Notre BaseUseCase:


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

Implémentation 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>?)
}

Voici le résultat:


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

Le rendre meilleur


Je suis une créature paresseuse et chaque fois que je ne veux pas faire glisser toute la feuille de code, j'ai donc fait les méthodes nécessaires dans 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

Maintenant, la liste des messages ressemble Ă  ceci:


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

Conclusion


J'ai utilisé des coroutines dans la prod et le code s'est avéré vraiment plus propre et plus lisible.


UPD:
Retrofit exception handling description see comment

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


All Articles