Coroutines рдФрд░ Retrofit2 рдХреЗ рд╕рд╛рде рдЕрдиреБрднрд╡ рдХрд░реЗрдВ

рдпрд╣ рдХреНрдпрд╛ рд╣реИ


рдХрд┐рд╕рдиреЗ рдЕрднреА рддрдХ рдкреНрд░рд▓реЗрдЦрди рдирд╣реАрдВ рдкрдврд╝рд╛ рд╣реИ - рдореИрдВ рдЖрдкрдХреЛ рдЦреБрдж рдХреЛ рдкрд░рд┐рдЪрд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдЕрддреНрдпрдзрд┐рдХ рд╕рд▓рд╛рд╣ рджреЗрддрд╛ рд╣реВрдВред


рдЬреЗрдЯрдмреНрд░реЗрди рдХреНрдпрд╛ рд▓рд┐рдЦрддрд╛ рд╣реИ:


рд▓рд╛рдЗрдмреНрд░реЗрд░реА рдХреЗ рдЕрдВрджрд░ рд╕рднреА рдЬрдЯрд┐рд▓рддрд╛рдУрдВ рдХреЛ рдЫреЛрдбрд╝рддреЗ рд╣реБрдП, рдХреЛрд░рд╛рдЙрдЯреАрди рдЕрддреБрд▓реНрдпрдХрд╛рд▓рд┐рдХ рдкреНрд░реЛрдЧреНрд░рд╛рдорд┐рдВрдЧ рдХреЛ рд╕рд░рд▓ рдмрдирд╛рддреЗ рд╣реИрдВред рдХрд╛рд░реНрдпрдХреНрд░рдо рдХреЗ рддрд░реНрдХ рдХреЛ рдХреЛрд░рдЯрд╛рдЗрдиреНрд╕ рдореЗрдВ рдХреНрд░рдорд┐рдХ рд░реВрдк рд╕реЗ рд╡реНрдпрдХреНрдд рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ, рдФрд░ рдЖрдзрд╛рд░ рдкреБрд╕реНрддрдХрд╛рд▓рдп рдЗрд╕реЗ рд╣рдорд╛рд░реЗ рд▓рд┐рдП рдЕрддреБрд▓реНрдпрдХрд╛рд▓рд┐рдХ рд░реВрдк рд╕реЗ рд▓рд╛рдЧреВ рдХрд░реЗрдЧрд╛ред рдкреБрд╕реНрддрдХрд╛рд▓рдп рд╕рдВрдмрдВрдзрд┐рдд рдШрдЯрдирд╛рдУрдВ рдХреА рд╕рджрд╕реНрдпрддрд╛ рд▓реЗрдиреЗ рд╡рд╛рд▓реЗ рдХреЙрд▓рдмреИрдХ рдореЗрдВ рдЙрдкрдпреЛрдЧрдХрд░реНрддрд╛ рдХреЛрдб рдХреЗ рд╕рдВрдмрдВрдзрд┐рдд рднрд╛рдЧреЛрдВ рдХреЛ рд▓рдкреЗрдЯ рд╕рдХрддрд╛ рд╣реИ, рдФрд░ рдЕрд▓рдЧ-рдЕрд▓рдЧ рдереНрд░реЗрдб (рдпрд╛ рдпрд╣рд╛рдВ рддрдХ тАЛтАЛрдХрд┐ рдЕрд▓рдЧ-рдЕрд▓рдЧ рдорд╢реАрдиреЛрдВ рдХреЗ рд▓рд┐рдП рдирд┐рд╖реНрдкрд╛рджрди рдХреЛ рднреА рдкреНрд░реЗрд╖рдг) рдХрд░ рд╕рдХрддрд╛ рд╣реИред рдХреЛрдб рдЙрддрдирд╛ рд╣реА рд╕рд░рд▓ рд░рд╣реЗрдЧрд╛ рдЬрд┐рддрдирд╛ рдХрд┐ рд╕рдЦреНрддреА рд╕реЗ рдХреНрд░рдо рд╕реЗ рдирд┐рд╖реНрдкрд╛рджрд┐рдд рдХрд┐рдпрд╛ рдЬрд╛рдПрдЧрд╛ред

рд╕рд░рд▓ рд╢рдмреНрджреЛрдВ рдореЗрдВ, рдпрд╣ рд╕рд┐рдВрдХреНрд░реЛрдирд╕ / рдПрд╕рд┐рдВрдХреНрд░реЛрдирд╕ рдХреЛрдб рдирд┐рд╖реНрдкрд╛рджрди рдХреЗ рд▓рд┐рдП рдПрдХ рдкреБрд╕реНрддрдХрд╛рд▓рдп рд╣реИред


рдХреНрдпреЛрдВ?


рдХреНрдпреЛрдВрдХрд┐ RxJava рдЕрдм рдлреИрд╢рди рдореЗрдВ рдирд╣реАрдВ рд╣реИ (рд╕рд┐рд░реНрдл рдордЬрд╛рдХ рдХрд░ рд░рд╣рд╛ рд╣реИ)ред


рд╕рдмрд╕реЗ рдкрд╣рд▓реЗ, рдореИрдВ рдХреБрдЫ рдирдпрд╛ рдХрд░рдиреЗ рдХреА рдХреЛрд╢рд┐рд╢ рдХрд░рдирд╛ рдЪрд╛рд╣рддрд╛ рдерд╛, рдФрд░ рджреВрд╕рд░реА рдмрд╛рдд, рдореИрдВ рдПрдХ рд▓реЗрдЦ - рдХреЛрд░реБрдЯрд┐рди рдХреА рдЧрддрд┐ рдФрд░ рдЕрдиреНрдп рддрд░реАрдХреЛрдВ рдХреА рддреБрд▓рдирд╛ рдореЗрдВ рдЖрдпрд╛ рдерд╛ред


рдЙрджрд╛рд╣рд░рдг


рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, рдЖрдкрдХреЛ рдкреГрд╖реНрдарднреВрдорд┐ рдореЗрдВ рдПрдХ рдСрдкрд░реЗрд╢рди рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИред


рдЖрд░рдВрдн рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП - corutin рдкрд░ рд╣рдорд╛рд░реЗ 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

рд╣рдо рдЕрдкрдиреЗ рдХреЛрдб рдореЗрдВ рд╡рд┐рдзрд┐ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рддреЗ рд╣реИрдВ:


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

рдЬрд╣рд╛рдВ рд╕рдВрджрд░реНрдн рдореЗрдВ - рд╣рдо рдЙрд╕ рдереНрд░реЗрдб рдкреВрд▓ рдХреЛ рдирд┐рд░реНрджрд┐рд╖реНрдЯ рдХрд░рддреЗ рд╣реИрдВ рдЬрд┐рд╕рдХреА рд╣рдореЗрдВ рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ - рд╕рд░рд▓ рдорд╛рдорд▓реЛрдВ рдореЗрдВ, рдпреЗ рдЖрдИрдУ, рдореБрдЦреНрдп рдФрд░ рдбрд┐рдлрд╝реЙрд▓реНрдЯ рд╣реИрдВ

IO - рдПрдкреАрдЖрдИ рдХреЗ рд╕рд╛рде рд╕рд░рд▓ рд╕рдВрдЪрд╛рд▓рди рдХреЗ рд▓рд┐рдП, рдбреЗрдЯрд╛рдмреЗрд╕ рдХреЗ рд╕рд╛рде рд╕рдВрдЪрд╛рд▓рди, рд╕рд╛рдЭрд╛ рдкреНрд░рд╛рдердорд┐рдХрддрд╛рдПрдВ рдЖрджрд┐ред
рдореБрдЦреНрдп - рдпреВрдЖрдИ рдзрд╛рдЧрд╛, рдЬрд╣рд╛рдВ рд╕реЗ рд╣рдо рджреГрд╢реНрдп рддрдХ рдкрд╣реБрдВрдЪ рд╕рдХрддреЗ рд╣реИрдВ
рдбрд┐рдлрд╝реЙрд▓реНрдЯ - рдЙрдЪреНрдЪ рд╕реАрдкреАрдпреВ рд▓реЛрдб рдХреЗ рд╕рд╛рде рднрд╛рд░реА рд╕рдВрдЪрд╛рд▓рди рдХреЗ рд▓рд┐рдП
(рдЗрд╕ рд▓реЗрдЦ рдореЗрдВ рдФрд░ рдЕрдзрд┐рдХ)


рдмреНрд▓реЙрдХ - рд▓рд╛рдВрдмрд╛ рдЬрд┐рд╕реЗ рд╣рдо рдирд┐рд╖реНрдкрд╛рджрд┐рдд рдХрд░рдирд╛ рдЪрд╛рд╣рддреЗ рд╣реИрдВ


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

рд╕рд┐рджреНрдзрд╛рдВрдд рд░реВрдк рдореЗрдВ, рдпрд╣ рд╕рдм, рд╣рдореЗрдВ 1 рд╕реЗ 1000 рддрдХ рд╡рд░реНрдЧреЛрдВ рдХреЗ рдпреЛрдЧ рдХрд╛ рдкрд░рд┐рдгрд╛рдо рдорд┐рд▓рддрд╛ рд╣реИ рдФрд░ рд╕рд╛рде рд╣реА рд╣рдо рдореБрдЦреНрдп рдзрд╛рдЧреЗ рдХреЛ рдмреНрд▓реЙрдХ рдирд╣реАрдВ рдХрд░рддреЗ рд╣реИрдВ, рдЬрд┐рд╕рдХрд╛ рдЕрд░реНрде рд╣реИ рдХрд┐ рдХреЛрдИ ANR рдирд╣реАрдВ


рд╣рд╛рд▓рд╛рдВрдХрд┐, рдЕрдЧрд░ рд╣рдорд╛рд░реА рдХреЛрд░рдЯрд╛рдЗрди рдХреЛ 20 рд╕реЗрдХрдВрдб рдХреЗ рд▓рд┐рдП рдирд┐рд╖реНрдкрд╛рджрд┐рдд рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ рдФрд░ рдЗрд╕ рд╕рдордп рдХреЗ рджреМрд░рд╛рди рд╣рдордиреЗ рдбрд┐рд╡рд╛рдЗрд╕ рдХреЗ 2 рдореЛрдбрд╝ рдмрдирд╛рдП рд╣реИрдВ, рддреЛ рд╣рдорд╛рд░реЗ рдкрд╛рд╕ 3 рдПрдХ рд╕рд╛рде рдЪрд▓рдиреЗ рд╡рд╛рд▓реЗ рдмреНрд▓реЙрдХ рд╣реЛрдВрдЧреЗред рдЙрдлрд╝ред


рдФрд░ рдЕрдЧрд░ рд╣рдордиреЗ рдмреНрд▓реЙрдХ рдореЗрдВ рдЧрддрд┐рд╡рд┐рдзрд┐ рдХреЗ рд▓рд┐рдП рдПрдХ рд▓рд┐рдВрдХ рдкрд╛рд░рд┐рдд рдХрд┐рдпрд╛ - рдПрдХ рд░рд┐рд╕рд╛рд╡ рдФрд░ рдкреБрд░рд╛рдиреЗ рдмреНрд▓реЙрдХреЛрдВ рдореЗрдВ рджреГрд╢реНрдп рдХреЗ рд╕рд╛рде рд╕рдВрдЪрд╛рд▓рди рдХрд░рдиреЗ рдХреА рдХреНрд╖рдорддрд╛ рдХреА рдХрдореАред рджреЛ рдмрд╛рд░ рдЙрдлрд╝ред


рддреЛ рдХреНрдпрд╛ рдХрд░реЗрдВ?


рдмреЗрд╣рддрд░ рдХрд░ рд░рд╣реЗ рд╣реИрдВ


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

рдЗрд╕ рдкреНрд░рдХрд╛рд░, рд╣рдо рд╕реНрдЯреНрд░реАрдо рдореЗрдВ рдЕрдкрдиреЗ рдХреЛрдб рдХреЗ рдирд┐рд╖реНрдкрд╛рджрди рдХреЛ рд░реЛрдХрдиреЗ рдореЗрдВ рд╕рдХреНрд╖рдо рдереЗ, рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, рдЬрдм рд╕реНрдХреНрд░реАрди рдХреЛ рдШреБрдорд╛рдпрд╛ рдЬрд╛рддрд╛ рд╣реИред


CoroutineScope рдиреЗ рд╕рднреА рдиреЗрд╕реНрдЯреЗрдб рдХреЙрд░рдЖрдЙрдЯреНрд╕ рдХреЗ рджрд╛рдпрд░реЗ рдХреЛ рд╕рдВрдпреЛрдЬрд┐рдд рдХрд░рдирд╛ рд╕рдВрднрд╡ рдмрдирд╛рдпрд╛ рдФрд░, рдЬрдм job.cancel () рдХреЛ рдХреЙрд▓ рдХрд┐рдпрд╛, рддреЛ рдЙрдирдХреЗ рдирд┐рд╖реНрдкрд╛рджрди рдХреЛ рд░реЛрдХ рджрд┐рдпрд╛ред


рдпрджрд┐ рдЖрдк рдирд┐рд╖реНрдкрд╛рджрди рдХреЛ рд░реЛрдХрдиреЗ рдХреЗ рдмрд╛рдж рдЧреБрдВрдЬрд╛рдЗрд╢ рдХрд╛ рдкреБрди: рдЙрдкрдпреЛрдЧ рдХрд░рдиреЗ рдХреА рдпреЛрдЬрдирд╛ рдмрдирд╛рддреЗ рд╣реИрдВ, рддреЛ рдЖрдкрдХреЛ job.cancelChildren () рдХреЗ рдмрдЬрд╛рдп job.cancel () рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИред рдЯрд┐рдкреНрдкрдгреА рдХреЗ рд▓рд┐рдП рдзрдиреНрдпрд╡рд╛рдж Neikist


рдЙрд╕реА рд╕рдордп, рд╣рдорд╛рд░реЗ рдкрд╛рд╕ рдЕрднреА рднреА рдкреНрд░рд╡рд╛рд╣ рдХреЛ рдирд┐рдпрдВрддреНрд░рд┐рдд рдХрд░рдиреЗ рдХрд╛ рдЕрд╡рд╕рд░ рд╣реИ:


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

рд╣рдо рд░реЗрдЯреНрд░реЛрдлрд╝рд┐рдЯ 2 рдХрдиреЗрдХреНрдЯ рдХрд░рддреЗ рд╣реИрдВ


рдУрд▓реЛрдВ рдХреЗ рд▓рд┐рдП рдирд┐рд░реНрднрд░рддрд╛ рдЬреЛрдбрд╝реЗрдВ:


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

рд╣рдо рдПрдХ рдЙрджрд╛рд╣рд░рдг рдХреЗ рд░реВрдк рдореЗрдВ рдХрд▓рдо рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рддреЗ рд╣реИрдВ https://my-json-server.typicode.com/typicode/demo/posts


рд╣рдо рд░реЗрдЯреНрд░реЛрдлрд┐рдЯ рдЗрдВрдЯрд░рдлрд╝реЗрд╕ рдХрд╛ рд╡рд░реНрдгрди рдХрд░рддреЗ рд╣реИрдВ:


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

рд▓реМрдЯреЗ рдкреЛрд╕реНрдЯ рдореЙрдбрд▓ рдХрд╛ рд╡рд░реНрдгрди рдХрд░реЗрдВ:


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

рд╣рдорд╛рд░рд╛ рдмреЗрд╕рд░реЗрдкреЛрд╕рд┐рдЯрд░реА:


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

PostRepository рдХрд╛ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди:


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

рд╣рдорд╛рд░рд╛ рдмреЗрд╕рдЙрд╕реЗрдХреЗрд╕:


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

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

рдпрд╣рд╛рдБ рдкрд░рд┐рдгрд╛рдо рд╣реИ:


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

рдЗрд╕реЗ рдмреЗрд╣рддрд░ рдмрдирд╛рдирд╛


рдореИрдВ рдПрдХ рдЖрд▓рд╕реА рдкреНрд░рд╛рдгреА рд╣реВрдВ рдФрд░ рд╣рд░ рдмрд╛рд░ рдЬрдм рдореИрдВ рдХреЛрдб рдХреА рдкреВрд░реА рд╢реАрдЯ рдХреЛ рдмрд╛рд╣рд░ рдирд╣реАрдВ рдирд┐рдХрд╛рд▓рдирд╛ рдЪрд╛рд╣рддрд╛, рддреЛ рдореИрдВрдиреЗ 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

рдЕрдм рдбрд╛рдХ рд╕реВрдЪреА рдкреНрд░рд╛рдкреНрдд рдХрд░рдирд╛ рдЗрд╕ рдкреНрд░рдХрд╛рд░ рд╣реИ:


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

рдирд┐рд╖реНрдХрд░реНрд╖


рдореИрдВрдиреЗ рдЙрддреНрдкрд╛рджреЛрдВ рдореЗрдВ рдХреЛрд░рд╛рдЙрдЯрд╛рдЗрди рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд┐рдпрд╛ рдФрд░ рдХреЛрдб рд╡рд╛рд╕реНрддрд╡ рдореЗрдВ рдХреНрд▓реАрдирд░ рдФрд░ рдЕрдзрд┐рдХ рдкрдардиреАрдп рдирд┐рдХрд▓рд╛ред


рдпреБрдкреАрдбреА:
рд░реЗрдЯреНрд░реЛрдлрд╝рд┐рдЯ рдЕрдкрд╡рд╛рдж рд╣реИрдВрдбрд▓рд┐рдВрдЧ рд╡рд┐рд╡рд░рдг рдЯрд┐рдкреНрдкрдгреА рджреЗрдЦреЗрдВ

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


All Articles