Се­год­ня в выпус­ке: раз­бира­емся с проб­лемами, воз­ника­ющи­ми при исполь­зовании корутин в Kotlin, прев­раща­ем кол­бэки в suspend-фун­кции, пишем при­ложе­ние с исполь­зовани­ем Kotlin Flow, а так­же раз­бира­емся, почему нель­зя про­водить тес­ты про­изво­дитель­нос­ти на отла­доч­ной сбор­ке, и учим­ся уста­нав­ливать на смар­тфон сра­зу отла­доч­ную и про­дак­шен‑вер­сии при­ложе­ния. Ну и как обыч­но, под­борка све­жих биб­лиотек.

Разработчику

Проблемы использования корутин в Kotlin

7 Gotchas When Explore Kotlin Coroutine — статья о проб­лемах, с которы­ми мож­но стол­кнуть­ся при исполь­зовании корутин в Kotlin. Вот наибо­лее инте­рес­ные тезисы.

1. runBlocking может под­весить при­ложе­ние

Рас­смот­рим сле­дующий код:

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
runBlocking(Dispatchers.Main) {
Log.d("Track", "${Thread.currentThread()}")
Log.d("Track", "$coroutineContext")
}
}

Выг­лядит безобид­но, но он при­ведет к фри­зу при­ложе­ния. Почему так про­исхо­дит, под­робно опи­сано в статье How runBlocking May Surprise You. Если крат­ко, то проб­лема воз­ника­ет из‑за самого прин­ципа работы runBlocking. Он запус­кает новую корути­ну, а затем бло­киру­ет текущий поток. Но если запус­тить runBlocking с дис­петче­ром Main из основно­го потока, то порож­денная корути­на ока­жет­ся в том же потоке и в ито­ге не смо­жет получить управле­ние, так как текущий поток будет заб­локиро­ван.

По‑хороше­му эта проб­лема реша­ется отка­зом от исполь­зования runBlocking. Это инс­тру­мент для юнит‑тес­тов, а не для про­дак­шена. Но если очень хочет­ся, мож­но убрать имя дис­петче­ра из вызова фун­кции:

runBlocking {
Log.d("Track", "${Thread.currentThread()}")
Log.d("Track", "$coroutineContext")
}
2. Корути­ну нель­зя завер­шить в любой момент

Не­опыт­ные раз­работ­чики счи­тают, что если выз­вать метод cancel() корути­ны, то она будет завер­шена сра­зу. На самом деле это не так и в ряде слу­чаев корути­на может успеть пол­ностью отра­ботать, перед тем как обра­бота­ет сиг­нал завер­шения.

Про­исхо­дит так потому, что корути­ны реали­зуют модель коопе­ратив­ной мно­гоза­дач­ности. Ког­да одна корути­на посыла­ет сиг­нал завер­шения дру­гой корути­не, пос­ледняя может либо обра­ботать этот сиг­нал, либо про­игно­риро­вать его. Хорошая новость сос­тоит в том, что все стан­дар­тные suspend-фун­кции (yield(), delay(), withContext() и дру­гие) уме­ют самос­тоятель­но обра­баты­вать этот сиг­нал и завер­шать корути­ну. Пло­хая новость — бла­года­ря такой невиди­мой авто­мати­зации раз­работ­чики быва­ют удив­лены, что одни корути­ны в их коде завер­шают­ся поч­ти мгно­вен­но в ответ на cancel(), а дру­гие про­дол­жают работать.

Проб­лему реша­ем так: про­веря­ем зна­чение свой­ства isActive меж­ду вычис­лени­ями и завер­шаем корути­ну, если получи­ли зна­чение false.

3. Отме­нен­ный coroutine scope нель­зя исполь­зовать пов­торно

Взгля­ни на сле­дующий код:

@Test
fun testingLaunch() {
val scope = MainScope()
runBlocking {
scope.cancel()
scope.launch {
try {
println("Start Launch 2")
delay(200)
println("End Launch 2")
} catch (e: CancellationException) {
println("Cancellation Exception")
}
}.join()
println("Finished")
}
}

Дан­ный код не будет работать. Если выз­вать cancel() на coroutine scope, он ста­новит­ся неп­ригод­ным для даль­нейше­го исполь­зования. Выхода из этой ситу­ации два: соз­дать новый scope на мес­те ста­рого и завер­шать не сам scope, а все при­над­лежащие ему корути­ны:

resultsScope.coroutineContext.cancelChildren()

Используем колбэки в последовательном коде

Suspending over Views — хорошая статья о том, как прев­ратить кол­бэки в suspend-фун­кции с помощью suspendCancellableCoroutine().

В Android кол­бэки пов­сюду, и UI фрей­мворк не исклю­чение. Кол­бэки исполь­зуют­ся для все­го под­ряд:

  • AnimatorListener — что­бы запус­тить код по окон­чании ани­мации;
  • RecyclerView.OnScrollListener — что­бы выпол­нить код сра­зу пос­ле про­мот­ки RecyclerView;
  • View.OnLayoutChangeListener — что­бы узнать, ког­да View был перери­сован пос­ле изме­нения.

В целом кол­беки не очень удоб­ны и могут прев­ратить код в труд­нопере­вари­ваемую кашу (callback hell). Фун­кции‑рас­ширения из биб­лиоте­ки android-ktx час­тично реша­ют эту проб­лему: пре­обра­зуют кол­бэки на осно­ве клас­сов в лям­бды (doOnLayout() вмес­то OnLayoutChangeListener()), но всем нам хотелось бы писать код в пос­ледова­тель­ном сти­ле, как и пред­лага­ет язык Kotlin и suspend-фун­кции. Но мож­но ли это­го дос­тичь, имея дело с фрей­мвор­ком Android, написан­ным с помощью кол­бэков?

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Оставить мнение