Опыт применения Coroutines и Retrofit2

Что это?


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


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


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



Вывод


Я использовал корутины в проде и код действительно получился чище и читабельнее.


UPD:
Описание обработки исключений Retrofit см. комментарий

  • +14
  • 4,9k
  • 9
Поделиться публикацией

Комментарии 9

    0
    А так как все равно все заканчивается обновлением MutableLiveData, то используя AsyncTask или даже просто viewModelJob = AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> liveData.postValue(download())); можно еще много кода выкинуть ;-)
      +1
      Есть один нюанс, вы предлагаете для отмены корутин использовать просто
      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)
      }


      Вывод будет следующий:
      start testCancel 1
      start testCancelChildren 1
      start testCancelChildren 2
      end testCancelChildren 2

      Т.е. в случае вызова просто cancel вторая корутина даже не запустилась, в случае вызова cancelChildren вторая корутина запустилась и успешно выполнилась.
        0
        но это подходит только если вы больше не планируете запускать на этом Job других корутин.

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

         @Override
            protected void onDestroy() {
                super.onDestroy();
        
                if (mViewModelStore != null && !isChangingConfigurations()) {
                   mViewModelStore.clear();
                }
        
                mFragments.dispatchDestroy();
            }
        0
        Спасибо за статью. Я столкнулся с проблемой, что если даже закэнселить джоб, то реквест, который ретрофит отправил не кэнселится. Тривиального решения данной проблемы я не нашел и отложил использования корутин до того, как это github.com/square/retrofit/pull/2886 не будет доступно в релизе.
          0
          Если соединение нестабильно или отсутствует, Retrofit может выбросить исключение по тайм-ауту и уронить приложение. Как вы обходите эту проблему?
            0
            В текущей реализации был использован вариант с обработкой этого исключения на уровне repository и дальнейшего преобразования в общий вид ошибки API.
            Однако после вашего комментария я задумался как бы почистить код и перенес отлов exception в BaseViewModel, а так же добавил дефолтное отображение ошибки через SingleLiveEvent(могу рассказать подробнее, если нужно)

            Примерный вид:
              private var viewModelJob = SupervisorJob()
            
              private inline fun <P> doCoroutineWork(
                    crossinline doOnAsyncBlock: suspend CoroutineScope.() -> P,
                    coroutineScope: CoroutineScope,
                    context: CoroutineContext
                ) {
                    coroutineScope.launch {
                        withContext(context) {
                            try {
                                doOnAsyncBlock.invoke(this)
                            } catch (e: UnknownHostException) {
                                e.printStackTrace()
                                Log.d(TAG, "Server is unreachable")
                            } catch (e: SocketTimeoutException) {
                                e.printStackTrace()
                                Log.d(TAG, "No internet connection")
                            }
                        }
                    }
                }
              0
              Спасибо! Я тоже использую try-catch, но сначала в методе onOnIntercept() интерфейса okhttp3.Interceptor для обработки ошибочных JSON, а затем в каждом запросе для обработки прочих ошибок (ошибок соединения). Но ваш опыт тоже интересен.
                +1
                Думаю, следует добавить ещё одно исключение: SSLHandshakeException. Это когда у сервера нет сертификата.
              0
              Столкнулся со следующей проблемой. Краш-логи в релизном приложении очень сложно понять. Строки ссылаются не на те, что в коде, декомпилировать в Java нормально нельзя. Также порядок вызова методов очень странный. Найти ошибку становится почти нереально. Это не корутины вносят сумятицу?

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое