Русский
Русский
English
Статистика
Реклама

Livedata

Перевод Миграция с LiveData на Kotlins Flow

08.06.2021 16:18:29 | Автор: admin

LiveData была нужна нам еще в 2017 году. Паттерн наблюдателя облегчил нам жизнь, но такие опции, как RxJava, в то время были слишком сложными для новичков. Команда Architecture Components создала LiveData: очень авторитетный класс наблюдаемых хранилищ данных, разработанный для Android. Он был простым, чтобы облегчить начало работы, а для более сложных случаев реактивных потоков рекомендовалось использовать RxJava, используя преимущества интеграции между ними.

DeadData?

LiveData по-прежнему остается нашим решением для Java-разработчиков, новичков и простых ситуаций. В остальном, хорошим вариантом является переход на Kotlin Flows. Flows (потоки) все еще имеют крутую кривую обучения, но они являются частью языка Kotlin, поддерживаемого Jetbrains; кроме того, на подходе Compose, который хорошо сочетается с реактивной моделью.

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

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

Flow: простые вещи труднее, а сложные легче

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

Давайте рассмотрим некоторые паттерны LiveData и их эквиваленты Flow:

#1: Показ результата однократной операции с модифицированным держателем данных

Это классический паттерн, в котором вы мутируете держатель состояния с результатом выполнения корутины:

Показ результата однократной операции с модифицированным (Mutable) держателем данных (LiveData)Показ результата однократной операции с модифицированным (Mutable) держателем данных (LiveData)
<!-- Copyright 2020 Google LLC.   SPDX-License-Identifier: Apache-2.0 -->class MyViewModel {    private val _myUiState = MutableLiveData<Result<UiState>>(Result.Loading)    val myUiState: LiveData<Result<UiState>> = _myUiState    // Load data from a suspend fun and mutate state    init {        viewModelScope.launch {             val result = ...            _myUiState.value = result        }    }}

Чтобы сделать то же самое с потоками, мы используем модифицированный StateFlow:

Показ результата однократной операции с модифицированным держателем данных (StateFlow)Показ результата однократной операции с модифицированным держателем данных (StateFlow)
class MyViewModel {    private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading)    val myUiState: StateFlow<Result<UiState>> = _myUiState    // Load data from a suspend fun and mutate state    init {        viewModelScope.launch {             val result = ...            _myUiState.value = result        }    }}

StateFlow это особый вид SharedFlow (который является особым типом Flow), наиболее близкий к LiveData:

  • У него всегда есть значение.

  • У него только одно значение.

  • Он поддерживает несколько наблюдателей (поэтому поток является общим).

  • Он всегда воспроизводит последнее значение при подписке, независимо от количества активных наблюдателей.

При отображении состояния пользовательского интерфейса для представления используйте StateFlow. Это безопасный и эффективный наблюдатель, предназначенный для хранения состояния пользовательского интерфейса.

#2: Показ результата однократной операции

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

В LiveData мы использовали для этого конструктор корутин liveData:

Показ результата однократной операции (LiveData)Показ результата однократной операции (LiveData)
class MyViewModel(...) : ViewModel() {    val result: LiveData<Result<UiState>> = liveData {        emit(Result.Loading)        emit(repository.fetchItem())    }}

Поскольку держатели состояния всегда имеют значение, хорошей идеей будет обернуть наше UI-состояние в какой-нибудь класс Result, который поддерживает такие состояния, как Loading, Success и Error.

Эквивалент Flow немного сложнее, потому что вам придется выполнить некоторую настройку:

Показ результата однократной операции (StateFlow)Показ результата однократной операции (StateFlow)
class MyViewModel(...) : ViewModel() {    val result: StateFlow<Result<UiState>> = flow {        emit(repository.fetchItem())    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000), // Or Lazily because it's a one-shot        initialValue = Result.Loading    )}

stateIn это оператор Flow, который преобразует его в StateFlow. Давайте пока доверимся этим параметрам, так как позже нам понадобится более сложная информация для правильного объяснения.

#3: Однократная загрузка данных с параметрами

Допустим, вы хотите загрузить некоторые данные, которые зависят от ID пользователя, и вы получаете эту информацию от AuthManager, который показывает Flow:

Однократная загрузка данных с параметрами (LiveData)Однократная загрузка данных с параметрами (LiveData)

С помощью LiveData можно сделать примерно следующее:

class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: LiveData<String?> =         authManager.observeUser().map { user -> user.id }.asLiveData()    val result: LiveData<Result<Item>> = userId.switchMap { newUserId ->        liveData { emit(repository.fetchItem(newUserId)) }    }}

switchMap это преобразование, тело которого выполняется, а результат подписывается при изменении userId.

Если нет причин для того, чтобы userId был LiveData, лучшей альтернативой этому является объединение потоков с Flow и окончательное преобразование полученного результата в LiveData.

class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }    val result: LiveData<Result<Item>> = userId.mapLatest { newUserId ->       repository.fetchItem(newUserId)    }.asLiveData()}

Выполнение этого действия с помощью Flows выглядит очень похоже:

Однократная загрузка данных с параметрами (StateFlow)Однократная загрузка данных с параметрами (StateFlow)
class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }    val result: StateFlow<Result<Item>> = userId.mapLatest { newUserId ->        repository.fetchItem(newUserId)    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.Loading    )}

Обратите внимание, что если вам нужна большая гибкость, вы также можете использовать transformLatest и emit элементы в явном виде:

val result = userId.transformLatest { newUserId ->        emit(Result.LoadingData)        emit(repository.fetchItem(newUserId))    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.LoadingUser // Note the different Loading states    )

#4: Наблюдение за потоком данных с параметрами

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

Продолжая наш пример: вместо вызова fetchItem на источнике данных мы используем гипотетический оператор observeItem, который возвращает Flow.

С помощью LiveData вы можете преобразовать поток в LiveData и все обновления emitSource:

Наблюдение за потоком с параметрами (LiveData)Наблюдение за потоком с параметрами (LiveData)
class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: LiveData<String?> =         authManager.observeUser().map { user -> user.id }.asLiveData()    val result = userId.switchMap { newUserId ->        repository.observeItem(newUserId).asLiveData()    }}

Или, лучше всего, объединить оба потока с помощью flatMapLatest и преобразовать только выход в LiveData:

class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: Flow<String?> =         authManager.observeUser().map { user -> user?.id }    val result: LiveData<Result<Item>> = userId.flatMapLatest { newUserId ->        repository.observeItem(newUserId)    }.asLiveData()}

Имплементация Flow похожа, но в ней нет преобразований LiveData:

Наблюдение за потоком с параметрами (StateFlow)Наблюдение за потоком с параметрами (StateFlow)
class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: Flow<String?> =         authManager.observeUser().map { user -> user?.id }    val result: StateFlow<Result<Item>> = userId.flatMapLatest { newUserId ->        repository.observeItem(newUserId)    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.LoadingUser    )}

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

#5 Объединение нескольких источников: MediatorLiveData -> Flow.combine

MediatorLiveData позволяет вам наблюдать за одним или несколькими источниками обновлений (наблюдаемыми LiveData) и что-то делать, когда они получают новые данные. Обычно вы обновляете значение MediatorLiveData:

val liveData1: LiveData<Int> = ...val liveData2: LiveData<Int> = ...val result = MediatorLiveData<Int>()result.addSource(liveData1) { value ->    result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))}result.addSource(liveData2) { value ->    result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))}

Эквивалент Flow намного проще:

val flow1: Flow<Int> = ...val flow2: Flow<Int> = ...val result = combine(flow1, flow2) { a, b -> a + b }

Можно также использовать функцию combineTransform или zip.

Настройка открытого StateFlow (оператор stateIn)

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

val result: StateFlow<Result<UiState>> = someFlow    .stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.Loading    )

Однако если вы не уверены в этом, казалось бы, случайном 5-секундном параметре started, читайте дальше.

StateIn имеет 3 параметра (из документации):

@param scope the coroutine scope in which sharing is started.@param started the strategy that controls when sharing is started and stopped.@param initialValue the initial value of the state flow.This value is also used when the state flow is reset using the [SharingStarted.WhileSubscribed] strategy with the `replayExpirationMillis` parameter.

started может принимать 3 значения:

  • Lazily: начать, когда появится первый подписчик, и остановить, когда scope будет отменен.

  • Eagerly: начать немедленно и остановить, когда scope будет отменен.

  • WhileSubscribed: Это сложно.

Для одноразовых операций вы можете использовать Lazily или Eagerly. Однако, если вы наблюдаете за другими потоками, вам следует использовать WhileSubscribed для выполнения небольших, но важных оптимизаций, как описано ниже.

Стратегия WhileSubscribed

WhileSubscribed отменяет восходящий поток, когда нет коллекторов. StateFlow, созданный с помощью stateIn, передает данные в View, но он также наблюдает за потоками, поступающими из других слоев или приложения (восходящий поток). Поддержание этих потоков активными может привести к напрасной трате ресурсов, например, если они продолжают считывать данные из других источников, таких как подключение к базе данных, аппаратные датчики и т.д. Когда ваше приложение переходит в фоновый режим, будет хорошо, если вы остановите эти корутины.

WhileSubscribed принимает два параметра:

public fun WhileSubscribed(    stopTimeoutMillis: Long = 0,    replayExpirationMillis: Long = Long.MAX_VALUE)

Таймаут остановки

Из документации:

stopTimeoutMillis настраивает задержку (в миллисекундах) между исчезновением последнего абонента и остановкой восходящего потока. По умолчанию она равна нулю (остановка происходит немедленно).

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

Решение в конструкторе корутины liveData заключалось в добавлении задержки в 5 секунд, после которой корутина будет остановлена, если нет подписчиков. WhileSubscribed(5000) делает именно это:

class MyViewModel(...) : ViewModel() {    val result = userId.mapLatest { newUserId ->        repository.observeItem(newUserId)    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.Loading    )}

Этот подход отвечает всем требованиям:

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

  • Последнее значение все еще будет кэшироваться, чтобы, когда пользователь вернется к нему, представление сразу же получило данные.

  • Подписки перезапускаются, и новые значения будут поступать, обновляя экран, когда они доступны.

Истечение срока воспроизведения

replayExpirationMillis настраивает задержку (в миллисекундах) между завершением работы программы совместного доступа и сбросом кэша воспроизведения (что делает кэш пустым для оператора shareIn и возвращает кэшированное значение к исходному initialValue для stateIn). По умолчанию он равен Long.MAX_VALUE (кэш воспроизведения сохраняется постоянно, буфер никогда не сбрасывается). Используйте нулевое значение для немедленного истечения срока действия кэша.

Наблюдение StateFlow из представления

Как мы уже видели, для представления очень важно сообщить StateFlows во ViewModel, что они больше не прослушиваются. Однако, как и во всем, что связано с жизненными циклами, все не так просто.

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

  • Activity.lifecycleScope.launch: запускает корутину немедленно и отменяет ее при завершении активности.

  • Fragment.lifecycleScope.launch: немедленно запускает корутину и отменяет ее при завершении фрагмента.

LaunchWhenStarted, launchWhenResumed...

Специализированные версии launch, называемые launchWhenX, будут ждать, пока lifecycleOwner находится в состоянии X, и приостановят выполнение корутины, когда lifecycleOwner упадет ниже состояния X. Важно отметить, что они не отменяют выполнение программы до тех пор, пока жизненный цикл не будет закончен.

Сбор потоков с помощью launch/launchWhenX небезопасенСбор потоков с помощью launch/launchWhenX небезопасен

Получение обновлений, когда приложение находится в фоновом режиме, может привести к сбоям, что решается приостановкой сбора в View. Однако восходящие потоки остаются активными, пока приложение находится в фоновом режиме, что может привести к трате ресурсов.

Это означает, что все, что мы делали до сих пор для настройки StateFlow, было бы совершенно бесполезно; однако в нашем распоряжении есть новый API.

lifecycle.repeatOnLifecycle на помощь

Этот новый конструктор корутин (доступный в lifecycle-runtime-ktx 2.4.0-alpha01) делает именно то, что нам нужно: он запускает корутины в определенном состоянии и останавливает их, когда уровень жизненного цикла опускается ниже этого состояния.

Различные методы сбора потокаРазличные методы сбора потока

Например, во Фрагменте:

onCreateView(...) {    viewLifecycleOwner.lifecycleScope.launch {        viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {            myViewModel.myUiState.collect { ... }        }    }}

Сбор начнется, когда представление фрагмента будет STARTED, продолжится до RESUMED и остановится, когда оно вернется к STOPPED. Читайте об этом в статье Более безопасный способ сбора потоков из пользовательских интерфейсов Android.

Сочетание API repeatOnLifecycle с приведенным выше руководством по StateFlow обеспечит вам наилучшую производительность при рациональном использовании ресурсов устройства.

StateFlow выставляется с помощью WhileSubscribed(5000) и собирается с помощью repeatOnLifecycle(STARTED)StateFlow выставляется с помощью WhileSubscribed(5000) и собирается с помощью repeatOnLifecycle(STARTED)

Предупреждение: Поддержка StateFlow, недавно добавленная в Data Binding, использует launchWhenCreated для сбора обновлений, и она начнет использовать repeatOnLifecycle` вместо этого, когда достигнет стабильности.

Для Data Binding вы должны использовать Flows везде и просто добавить asLiveData(), чтобы отобразить их в представлении. Привязка данных будет обновлена, когда lifecycle-runtime-ktx 2.4.0 станет стабильным.

Резюме

Лучшим способом предоставления данных из ViewModel и сбора их из представления является:

  • Выставить StateFlow, используя стратегию WhileSubscribed, с таймаутом. [пример]

  • Собирать с помощью repeatOnLifecycle. [пример].

Любая другая комбинация будет поддерживать восходящие потоки активными, расходуя ресурсы:


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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru