Я знаю, что вам нравится RxJava, но ...

TL; DR: вам никогда не понадобится вызов типа withContext(...) { suspendingFunction() }.

Рассмотрим следующую хорошо знакомую задачу на Android: вы должны сделать сетевой запрос и обновить пользовательский интерфейс полученными данными. Ветераны RxJava без колебаний напишут следующее:

(Для простоты я предположил, что и успех, и ошибка будут заключены в один и тот же объект и отправлены в основной канал.) Обратите внимание на оператор subscribeOn, который помещает вызов восходящего потока в фоновый поток ввода-вывода, и оператор observeOn, который помещает нисходящий вызов updateUiWithResult в основном потоке. Эта комбинация настолько распространена, что большинство пользователей RxJava, пишущих на Kotlin, вероятно, будут иметь собственное вспомогательное расширение для одновременного применения обоих:

Однако вопрос, который я здесь хочу обсудить: каков лучший способ представить такую ​​цепочку вызовов с помощью сопрограмм Kotlin? Нетрудно встретить такие примеры, как:

or

или даже

Все они компилируются и работают правильно, но я считаю, что их следует рассматривать как неправильные (или, по крайней мере, неидиоматические и излишне избыточные). Все это проистекает из того, что вызывающий несет ответственность за обеспечение потоковой передачи фоновых операций для фоновых операций, и я утверждаю, что это мышление в стиле RxJava, которое не должно переноситься на сопрограммы ... и я не только один.

Правильный путь

Так как же это выглядит, если обеспечение потоковой передачи не несет ответственность вызывающая сторона? Учтите следующее:

Здесь следует отметить два очень важных момента:

  • viewModelScope (и эквиваленты в Activity или Fragment, такие как lifecycleScope) по умолчанию использует Dispatchers.Main.
  • Для getNetworkResult вызывающая сторона не применяет ручную потоковую передачу, используя что-то вроде withContext(Dispatchers.IO).

Этот первый пункт означает, что любые вызовы функций (приостановленные или другие), которые должны выполняться в основном потоке, могут выполняться без какой-либо дополнительной обработки в этой области. Кроме того, поскольку вам всегда нужен CoroutineScope для вызова функции приостановки в первую очередь, на вопрос «нисходящей потоковой передачи», естественно, уже дан ответ еще до того, как вы даже напишете сами вызовы приостановки.

Однако последний момент здесь действительно ключевой. Чтобы это был безопасный вызов, который не приводит к NetworkOnMainThreadException или иным образом блокирует основной поток, мы должны предположить, что getNetworkResult теперь имеет потоки, «предварительно примененные» в некотором смысле. Это может идти вразрез с инстинктами многих пользователей RxJava, но на самом деле это желаемая цель как JetBrains (который разрабатывает Kotlin и сопрограммы), так и Google (который продолжает чтобы использовать подход, ориентированный на Kotlin и, казалось бы, ориентированный на сопрограммы для новых функций Android).

Что говорит JetBrains

Во-первых, рассмотрим статью Романа Елизарова о взаимосвязи между блокирующими вызовами и сопрограммами. Он очень ясно дает понять, что сообщество Kotlin должно придерживаться шаблона, в котором функции приостановки не блокируют поток вызывающего абонента.

При написании функции приостановки для обертывания блокирующего вызова простое применение ключевого слова suspend ничего не делает для создания хорошо работающей функции приостановки, и Intellij IDE / Android Studio даже предупредит вас об этом. (Я бы даже сказал, что это должно быть ошибкой компиляции, а не простым предупреждением). Это явное применение withContext, которое наделяет функцию правильным поведением:

Неспособность сделать это просто приводит к тому, что функция должна вызываться в области сопрограммы, но по-прежнему действует как обычный блокирующий вызов внутри нее. Если бы мы проявили щедрость, это можно было бы рассматривать как «намек» на то, что звонок блокируется. Однако, поскольку наиболее распространенные области сопрограмм, с которыми будет иметь дело разработчик Android, - это те, которые все еще работают в основном потоке (например, viewModelScope), в некотором смысле это просто переносит ту же проблему в другое место и требует большего количества оборудования для решения Это. С правильным Dispatcher, уже примененным в теле функции, эту новую функцию можно безопасно вызывать из любой области сопрограммы и работать правильно. Это также означает, что вам никогда не нужно писать что-то вроде withContext(...) { suspendingFunction() }.

Что говорит Google

Это подводит нас к общей идее основной безопасности. Это способ Google пометить приостанавливающие функции, которые могут быть вызваны из области сопрограммы, которая использует Dispatchers.Main без какой-либо дополнительной обработки:

Функции приостановки должны быть безопасными для основного потока, то есть их можно безопасно вызывать из основного потока. Если класс выполняет длительные операции блокировки в сопрограмме, он отвечает за перенос выполнения из основного потока с помощью withContext. Это применимо ко всем классам в вашем приложении, независимо от части архитектуры, в которой находится класс.

На самом деле это просто специфический для Android способ заявить ключевой момент Романа Елизарова о правильном построении приостановки вызовов.

Сторонние библиотеки?

А как насчет сторонних библиотек, в которых есть функция приостановки вызовов функций? Любая обычная, хорошо поддерживаемая библиотека, используемая сообществом Android (например, Retrofit), также будет придерживаться тех же принципов. Фактически, хотя всегда можно было предварительно применить потоки к типам RxJava, предоставляемым Retrofit, это единственный способ получить от него функцию приостановки. Обычно это означает, что единственный код, в котором вам действительно нужно беспокоиться о том, чтобы приостановить функции main-safe, - это ваш собственный.

На самом деле все сводится к вопросу об ответственности: кто отвечает за обеспечение потоковой передачи в фоновом режиме для вызовов, которые в них нуждаются? Это потребитель метода или писатель метода? Исходя из RxJava - где это было более распространено, чтобы оставить это потребителю - можно легко попробовать перенести этот стиль мышления на сопрограммы. (Я знаю, что именно так я подошел к этому, когда сам впервые узнал о них). Однако, как сообщество, на этот раз у нас есть шанс установить лучшую, более сильную модель. У нас есть шанс перестать думать о размещении функции приостановки в фоновом потоке и начать требовать, чтобы они уже были в любом потоке, который им требуется. У нас есть возможность поговорить в один голос и сказать…

Это способ.

Брайан работает в Livefront, где * возможно * все это время выступал за неоптимальный шаблон потоковой передачи RxJava…