Что это?


Кто еще не читал документацию — настоятельно рекомендую к ознакомлению.


Что пишет джетбрейнс:


Сопрограммы упрощают асинхронное программирование, оставив все осложнения внутри библиотек. Логика программы может быть выражена последовательно в сопрограммах, а базовая библиотека будет её реализовывать асинхронно для нас. Библиотека может обернуть соответствующие части кода пользователя в обратные вызовы (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)


  1. Valle
    25.03.2019 18:30

    А так как все равно все заканчивается обновлением MutableLiveData, то используя AsyncTask или даже просто viewModelJob = AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> liveData.postValue(download())); можно еще много кода выкинуть ;-)


  1. Neikist
    25.03.2019 22:03

    Есть один нюанс, вы предлагаете для отмены корутин использовать просто

    viewModelJob.cancel()
    но это подходит только если вы больше не планируете запускать на этом Job других корутин. Иначе, если планируете дальнейшее использование CoroutineScope, нужно использовать
    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)
    }


    1. left-sinii Автор
      26.03.2019 12:25

      но это подходит только если вы больше не планируете запускать на этом Job других корутин.

      В моем примере .cancel() вызывается в onDestroy() фрагмента. Но для общего случая, замечание верное, добавил информацию в статью.

       @Override
          protected void onDestroy() {
              super.onDestroy();
      
              if (mViewModelStore != null && !isChangingConfigurations()) {
                 mViewModelStore.clear();
              }
      
              mFragments.dispatchDestroy();
          }


  1. BreakPoints
    26.03.2019 10:40

    Спасибо за статью. Я столкнулся с проблемой, что если даже закэнселить джоб, то реквест, который ретрофит отправил не кэнселится. Тривиального решения данной проблемы я не нашел и отложил использования корутин до того, как это github.com/square/retrofit/pull/2886 не будет доступно в релизе.


  1. CoolMind
    28.03.2019 14:52

    Если соединение нестабильно или отсутствует, Retrofit может выбросить исключение по тайм-ауту и уронить приложение. Как вы обходите эту проблему?