Connect with us

Программирование

Шаблоны корутин в Android и как они работают

Шесть паттернов, использующих корутины, и один антипаттерн — и самое главное, почему они работают и что за ними стоит.

Фото аватара

Опубликовано

/

     
     

Я знаю многих разработчиков Android, которые изучают корутины на примерах, и этого обычно достаточно, чтобы справиться с задачей. Но при этом упускается вся красота, которая скрывается за ними — и тот факт, что, по сути, все очень просто. Так что же заставляет эти паттерны работать?

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

И, конечно, если вы новичок в этом деле, добро пожаловать! Вот хороший набор паттернов, которые действительно стоит изучить разработчику Android.

Шаблон 1: Приостанавливающаяся (suspending) функция

Вот как вы делаете тосты. Возможно, вы уже знаете это:

  1. Положить хлеб в тостер
  2. Подождать
  3. Вынуть хлеб — теперь уже тост — из тостера

Вот версия этого на языке Kotlin:

suspend fun makeToast() {
    println("Putting bread in toaster")
    delay(2000)
    println("Bread is now toasted")
}

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

Чем же вы занимаетесь в ожидании? Да чем угодно. Вы можете отметить еще один пункт в своем списке дел. Если вы вернетесь вовремя, чтобы заняться своим поджаренным хлебом, когда он будет готов, то все будет в порядке.

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

Таким образом — и это ключевой момент — когда вы вызываете эту функцию приостановки, основной поток не блокируется. Библиотека корутин эффективно использует задержку, и поток переходит к работе.

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

Шаблон 2: Вызов приостанавливающейся функции из главного потока

Вот почему часто безопасно вызывать приостанавливающуюся функцию из главного потока/потока пользовательского интерфейса. Учитывая, что она не блокирует вызывающий поток, вызывающий поток может продолжать заниматься делами пользовательского интерфейса.

Вот пример этого паттерна. При нажатии на кнопку мы раскрываем PIN-код на 10 секунд, а затем снова его скрываем.

Главная активити:

@Composable
fun PlanetsScreen(...) {
    val revealPIN by viewModel.isShowingPin.collectAsStateWithLifecycle()
    val scope = rememberCoroutineScope()

    Column {
        Button(
            onClick = {
                scope.launch {
                    // Here we call a function which takes at least 10 seconds to run,
                    // directly from the main thread. Safe because the thread isn't blocked.
                    viewModel.revealPinBriefly()
                }
            }
        ) {
            Text("Reveal PIN")
        }

        if (revealPIN) {
            Text(text = "Your PIN is 1234")
        }
    }
}

Модель:

val isShowingPin = MutableStateFlow(false)

// This function suspends the coroutine for a long time, but
// doesn't block the calling thread. So it can be called from
// the main/UI thread safely.
suspend fun revealPinBriefly() {
    isShowingPin.value = true
    delay(10_000)
    isShowingPin.value = false
}

Это совершенно безопасно, поскольку не блокирует поток пользовательского интерфейса. UI будет продолжать отвечать на запросы в течение 10-секундной задержки.

Шаблон 3: Переключение контекстов

Многие функции с приостановками проводят большую часть своего времени в, очевидно, приостановленном состоянии. Хорошим примером является получение данных из Интернета: установка соединения не требует особых усилий, но ожидание загрузки данных занимает большую часть времени.

Так безопасно ли выполнять приостановленные сетевые задачи в потоке пользовательского интерфейса? Нет! Вовсе нет.

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

Сетевые задачи включают в себя всевозможную работу вне ожидания: настройку, шифрование, разбор ответов и т.д. Они могут занимать всего миллисекунды — но это миллисекунды времени, в течение которого поток пользовательского интерфейса блокируется.

По соображениям производительности вам нужно, чтобы поток UI постоянно обновлял пользовательский интерфейс. Не прерывайте его, иначе производительность вашего приложения пострадает.

Вот почему у нас есть паттерн «переключение контекстов»:

suspend fun saveNote(note: Note) {
    withContext(Dispatchers.IO) {
        notesRemoteDataSource.saveNote(note)
    }
}

Вышеуказанный параметр withContext гарантирует, что эта функция приостановки будет запущена в пуле потоков ввода-вывода. Благодаря этому функция saveNote может быть безопасно вызвана из потока пользовательского интерфейса.

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

Шаблон 4: Запуск корутинов и область действия (scope)

Это не совсем паттерн, так как всем корутинам нужен контекст, в котором они будут выполняться.

Но возьмем пример ниже. Что на самом деле означает такой код?

viewModelScope.launch {
  // Do something
}

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

Поэтому, говоря viewModelScope.launch, вы говорите: запустить корутину, время жизни которой ограничено viewModelScope.

Таким образом, «viewModelScope» здесь — это как ведро, в котором хранятся корутины для модели представления, включая ту, что описана выше. Когда ведро опустошается — то есть, когда viewModelScope отменяется — его содержимое также будет отменено. С практической точки зрения это означает, что вы можете писать код, не беспокоясь о том, когда его нужно будет завершить.

Шаблон 5: Несколько операций в останавливающейся функции

Выше мы уже столкнулись с viewModelScope. Существует множество других, например:

  • rememberCoroutineScope() в Compose, которая обеспечивает область видимости, которая длится до тех пор, пока @Composable находится на экране (шаблон 1 выше — пример этого)
  • viewLifecycleOwner.lifecycleScope в Android View, который длится столько, сколько длится Activity/Fragment
  • GlobalScope, который длится вечно (и поэтому обычно, но не всегда, является плохой идеей™)

Или вы можете создать свой собственный, как в этом шаблоне:

suspend fun deleteAllNotes() = withContext(...) {
    // Create a scope. The suspend function will return when *all* the
    // scope's child coroutines finish.
    coroutineScope {
        launch { remoteDataSource.deleteAllNotes() }
        launch { localDataSource.deleteAllNotes() }
    }
}

Зачем вам это нужно? Ну, coroutineScope — это специальная функция, которая создает новую область видимости корутины и приостанавливает ее выполнение до тех пор, пока все дочерние корутины в ней не завершатся.

Таким образом, паттерн выше означает «делайте эти вещи параллельно, а затем возвращайтесь, когда все они будут выполнены».

Это полезно, например, в классах репозиториев с локальными и удаленными источниками данных, потому что часто требуется сделать что-то с обоими источниками данных одновременно. Операция считается завершенной только тогда, когда оба действия завершены.

Шаблон 6: Бесконечные циклы (почти)

Теперь, когда мы поняли область действия корутин, можно увидеть, как паттерн, подобный этому, на самом деле работает:

fun flashTheLights() {
    viewModelScope.launch {
        // This seems like an unsafe infinite loop, but in fact
        // it'll shut down when the viewModelScope is cancelled.
        while(true) {
            delay(1_000)
            lightState = !lightState
        }
    }
}

Цикл while(true), который еще 5 лет назад был бы огромным тревожным сигналом, теперь на самом деле совершенно безопасен. Как только viewModelScope будет отменен, будет отменена и запущенная корутина, и таким образом «бесконечный» цикл остановится.

Но причина, по которой он останавливается, довольно интересна…

Вызов delay() передает поток диспетчеру корутины. Это означает, что он позволяет диспетчеру корутин проверить, не нужно ли сделать еще что-нибудь, и он может пойти и сделать это.

Но это также означает, что диспетчер корутины проверяет, не была ли отменена корутина, и если да, то выбрасывает CancellationException. Вам не нужно обрабатывать это исключение, но в результате стек разматывается, а while(true) отбрасывается.

Антишаблон: Останавливающаяся функция, которая не останавливается

Поэтому уступать диспетчеру корутинов просто необходимо. Совершенно безопасно использовать такие библиотеки, как Room, Retrofit и Coil, потому что они при необходимости отступают перед диспетчером.

Но вот почему вы никогда не должны писать корутину, которая делает это:

// !!!!! DON'T DO THIS !!!!!
suspend fun countToAHundredBillion_unsafe() {
    var count = 0L
    
    // This suspend fun won't be cancelled if the coroutine
    // that's running it gets cancelled, because it doesn't
    // ever yield.
    while(count < 100_000_000_000) {
        count++
    }
}

Для запуска требуется значительное время. И после запуска это нельзя остановить.

Безопасная для корутинов версия вышеописанной функции может использовать функцию yield(). Эта yield() немного похожа на выполнение delay() без фактической задержки: она уступает диспетчеру и получает CancellationException, если ей нужно остановиться.

Вот безопасная версия вышеприведенной функции:

suspend fun countToAHundredBillion() {
    var count = 0L
    
    while(count < 100_000_000_000) {
        count++
        
        // Every 10,000 we yield to the coroutine
        // dispatcher, allowing this loop to be
        // cancelled if needed.
        if (count % 10_000 == 0) {
            yield()
        }
    }
}

Ну вот и все. Шесть паттернов, использующих корутины, и один антипаттерн — и самое главное, почему они работают и что за ними стоит.

В одной из следующих статей я более подробно расскажу, например, о разнице между областью видимости и контекстом корутинов, о том, что такое Job и что происходит при использовании запуска.

Источник

Еще про корутины

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Advertisement

Наши партнеры:

LEGALBET

Мобильные приложения для ставок на спорт
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: