Что это?
Кто еще не читал документацию — настоятельно рекомендую к ознакомлению.
Что пишет джетбрейнс:
Сопрограммы упрощают асинхронное программирование, оставив все осложнения внутри библиотек. Логика программы может быть выражена последовательно в сопрограммах, а базовая библиотека будет её реализовывать асинхронно для нас. Библиотека может обернуть соответствующие части кода пользователя в обратные вызовы (callbacks), подписывающиеся на соответствующие события, и диспетчировать исполнение на различные потоки (или даже на разные машины!). Код при этом останется столь же простой, как если бы исполнялся строго последовательно.
Если говорить простыми словами — это библиотека для синхронного \ асинхронного выполнения кода.
Зачем?
Потому что RxJava уже не в моде (шутка).
Во-первых, хотелось попробовать что-то новое, во-вторых, я наткнулся на статью — сравнение скорости работы корутин и других способов.
Пример
Например, нужно выполнить операцию в фоне.
Для начала — добавим в наш build.gradle зависимость от корутин:
Используем в нашем коде метод:
Где в context — мы указываем необходимый нам thread pool — в простых случаях это IO, Main и Default
IO — для простых операций с API, операциями с БД, shared preferencies и тд.
Main — UI тред, откуда мы можем получать доступ к вью
Default — для тяжелых операций с высокой нагрузкой на CPU
(Подробнее в этой статье )
Block — лямбда которую мы хотим выполнить
В принципе на этом все, мы получаем результат cуммы квадратов от 1 до 1000 и при этом не блокируем main thread что означает — никаких ANR
Однако, если наша корутина выполняется 20 секунд и за это время мы совершили 2 поворота девайса то мы будем иметь 3 одновременно выполняющихся block. Упс.
А если мы передали в block ссылку на activity — утечка и отсутствие возможности в старых блоках выполнить операции с view. Дважды упс.
Таки что делать?
Делаем лучше
Таким образом мы получили возможность останавливать выполнение нашего кода в потоке, например, при повороте экрана.
CoroutineScope сделал возможным объединить scope всех вложенных корутин и при вызове job.cancel() — останавливать их выполнение
Если планируется повторное использование scope после остановки выполнения — нужно использовать job.cancelChildren() вместо job.cancel() спасибо Neikist за комментарий
При этом у нас остается возможность управлять потоками:
Подключаем retrofit2
Добавляем зависимости в градл:
Используем для примера ручку https://my-json-server.typicode.com/typicode/demo/posts
Описываем интерфейс ретрофита:
Опишем возвращаемую модель Post:
Наш BaseRepository:
Реализация PostsRepository:
Наш BaseUseCase:
Реализация GetPostsListUseCase:
Вот что получилось в итоге:
Делаем еще лучше
Я ленивое существо и не хочу каждый раз тянуть всю простыню кода, поэтому вынес нужные методы в BaseViewModel:
Теперь получения списка Posts выглядит так:
Вывод
Я использовал корутины в проде и код действительно получился чище и читабельнее.
Комментарии (5)
Neikist
25.03.2019 22:03Есть один нюанс, вы предлагаете для отмены корутин использовать просто
но это подходит только если вы больше не планируете запускать на этом Job других корутин. Иначе, если планируете дальнейшее использование CoroutineScope, нужно использоватьviewModelJob.cancel()
viewModelJob.cancelChildren()
Можете провести
небольшой экспериментimport kotlinx.coroutines.* class CoroutinesCancelationClass { private val job = Job() private val scope = CoroutineScope(Dispatchers.IO + job) fun doWork(tag: String) { scope.launch{ println("start $tag") delay(1000) println("end $tag") } } fun cancel(){ job.cancel() } fun cancelChildren() { job.cancelChildren() } } fun testCancelChildren() { val testObj = CoroutinesCancelationClass() testObj.doWork("testCancelChildren 1") testObj.cancelChildren() testObj.doWork("testCancelChildren 2") } fun testCancel() { val testObj = CoroutinesCancelationClass() testObj.doWork("testCancel 1") testObj.cancel() testObj.doWork("testCancel 2") } fun main(args: Array<String>) = runBlocking { testCancel() delay(2000) testCancelChildren() delay(2000) }
left-sinii Автор
26.03.2019 12:25но это подходит только если вы больше не планируете запускать на этом Job других корутин.
В моем примере .cancel() вызывается в onDestroy() фрагмента. Но для общего случая, замечание верное, добавил информацию в статью.
@Override protected void onDestroy() { super.onDestroy(); if (mViewModelStore != null && !isChangingConfigurations()) { mViewModelStore.clear(); } mFragments.dispatchDestroy(); }
BreakPoints
26.03.2019 10:40Спасибо за статью. Я столкнулся с проблемой, что если даже закэнселить джоб, то реквест, который ретрофит отправил не кэнселится. Тривиального решения данной проблемы я не нашел и отложил использования корутин до того, как это github.com/square/retrofit/pull/2886 не будет доступно в релизе.
CoolMind
28.03.2019 14:52Если соединение нестабильно или отсутствует, Retrofit может выбросить исключение по тайм-ауту и уронить приложение. Как вы обходите эту проблему?
Valle
А так как все равно все заканчивается обновлением MutableLiveData, то используя AsyncTask или даже просто viewModelJob = AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> liveData.postValue(download())); можно еще много кода выкинуть ;-)