📜 ⬆️ ⬇️

Experience using Coroutines and Retrofit2

What is it?


Who has not read the documentation - I strongly recommend to read.


What does the Jetbrains write:


Coroutines simplify asynchronous programming, leaving all the complications inside libraries. The program logic can be expressed sequentially in coroutines, and the core library will implement it asynchronously for us. The library can wrap the relevant parts of the user code in callbacks subscribing to the relevant events, and dispatch execution to different threads (or even to different machines!). The code at the same time will remain as simple as if it was executed strictly consistently.

In simple terms, this is a library for synchronous \ asynchronous code execution.


What for?


Because RxJava is no longer in fashion (joke).


Firstly, I wanted to try something new, and secondly, I came across an article - comparing the speed of work of corutin and other methods.


Example


For example, you need to perform an operation in the background.


First, let's add a dependency on corutin to our build.gradle:


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

We use in our code method:


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

Where in context - we specify the thread pool we need - in simple cases it is IO, Main and Default

IO - for simple operations with the API, database operations, shared preferencies, and so on.
Main - UI thread from where we can access the view
Default - for heavy operations with high CPU load
(More in this article )


Block - lambda we want to perform


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

In principle, this is all, we get the result of the sum of squares from 1 to 1000 and at the same time do not block the main thread which means - no ANR


However, if our korutin is fulfilled for 20 seconds and during this time we made 2 turns of the device, then we will have 3 simultaneously executed blocks. Oops.


And if we passed a link to the activity to the block, there is a leak and the lack of the ability in old blocks to perform operations with view. Double oops.


Taki what to do?


Do better


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

Thus, we were able to stop the execution of our code in the stream, for example, when turning the screen.


CoroutineScope made it possible to combine the scope of all nested coruntines and, when calling job.cancel (), stop their execution


If you plan to reuse the scope after stopping the execution, you need to use job.cancelChildren () instead of job.cancel (), thanks Neikist for commenting


At the same time, we still have the ability to manage flows:


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

We connect retrofit2


Add dependencies in degrees:


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

Use the https://my-json-server.typicode.com/typicode/demo/posts handle for example


We describe the interface of a retrofit:


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

We describe the returned model Post:


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

Our BaseRepository:


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

Implementing 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>?)
}

Our BaseUseCase:


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

Implementing 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>?)
}

Here's what happened in the end:


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

Making it even better


I am a lazy creature and do not want every time to pull the entire sheet of code, so I rendered the necessary methods in 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

Now getting the list of Posts looks like this:


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


I used korutiny in the prode and the code really turned out cleaner and more readable.


UPD:
For a description of Retrofit exception handling, see comment .


')

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


All Articles