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.
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.
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" | |
.... | |
} |
dependencies { | |
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1" | |
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1" | |
.... | |
} |
We use in our code method:
suspend fun <T> withContext( | |
context: CoroutineContext, | |
block: suspend CoroutineScope.() -> T | |
) |
suspend fun <T> withContext( | |
context: CoroutineContext, | |
block: suspend CoroutineScope.() -> T | |
) |
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") |
var result = 1.0 | |
withContext(IO) { | |
for (i in 1..1000) { | |
result += i * i | |
} | |
} | |
Log.d("coroutines example", "result = $result") |
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?
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() | |
} |
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() | |
} |
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() | |
} |
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() | |
} |
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" | |
... | |
} |
dependencies { | |
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" | |
implementation "com.squareup.retrofit2:converter-moshi:$converterMoshiVersion" | |
implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$retrofitCoroutinesVersion" | |
... | |
} |
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>>> | |
} |
interface RetrofitPosts { | |
@GET("posts") | |
fun getPosts(): Deferred<Response<List<Post>>> | |
} |
We describe the returned model Post:
data class Post(val id: Int, val title: String) |
Our BaseRepository:
abstract class BaseRepository<Params, Result> { | |
abstract suspend fun doWork(params: Params): Result | |
} |
abstract class BaseRepository<Params, Result> { | |
abstract suspend fun doWork(params: Params): Result | |
} |
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>?) | |
} |
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 | |
} |
abstract class BaseUseCase<Params, Result> { | |
abstract suspend fun doWork(params: Params): Result | |
} |
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>?) | |
} |
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() | |
} |
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() | |
} |
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) | |
} | |
} | |
} | |
} |
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) | |
} | |
} | |
} | |
} |
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()) | |
} | |
} |
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()) | |
} | |
} |
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/