- Kotlin Flow против Android LiveData
- Migrating from LiveData to Kotlin’s Flow
- DeadData?
- Flow: Simple things are harder and complex things are easier
- #1: Expose the result of a one-shot operation with a Mutable data holder
- #2: Expose the result of a one-shot operation
- #3: One-shot data load with parameters
- #4: Observing a stream of data with parameters
- #5 Combining multiple sources: MediatorLiveData -> Flow.combine
- Configuring the exposed StateFlow (stateIn operator)
- The WhileSubscribed strategy
- Stop timeout
- Replay expiration
- Observing StateFlow from the view
- LaunchWhenStarted, launchWhenResumed…
- lifecycle.repeatOnLifecycle to the rescue
- Summary
- Более безопасный способ сбора потоков данных из пользовательских интерфейсов Android
- Неэффективное использование ресурсов
- Lifecycle.repeatOnLifecycle
- Визуальная диаграмма
- Flow.flowWithLifecycle
- Настройка производителя стандартного потока
- Безопасный сбор Flow в Jetpack Compose
- Сравнение с LiveData
- Поддержка StateFlow в связывании данных
Kotlin Flow против Android LiveData
У меня есть несколько вопросов о Kotlin Flow
- Я могу наблюдать LiveData из нескольких фрагментов. Могу ли я сделать это с Flow? Если да, то как?
- Мы можем иметь несколько LiveData из одной LiveData, используя map & switchMap . Есть ли способ иметь несколько потоков из одного источника потока?
- Используя MutableLiveData я могу обновить данные из любой точки мира, используя ссылку на переменную. Есть ли способ сделать то же самое с Flow?
У меня есть такой пример использования: я буду наблюдать за SharedPreferences использованием, callbackFlow <. >которое даст мне единственный источник потока. Из этого потока я хочу создать несколько потоков для каждой пары ключ-значение.
Это может звучать глупо. Я новичок в мире Rx и Flow.
Я могу наблюдать LiveData из нескольких фрагментов. Могу ли я сделать это с Flow? Если да, то как?
Да. Вы можете сделать это с emit и collect . Мысли emit похожи на живые данные postValue и collect похожи на observe . Давайте приведем пример.
вместилище
ViewModel
Фрагмент
Мы можем иметь несколько LiveData из одной LiveData, используя map & switchMap. Есть ли способ иметь несколько потоков из одного источника потока?
Поток очень удобен. Вы можете просто создать поток внутри потока. Допустим, вы хотите добавить знак градуса к каждому из данных прогноза погоды.
ViewModel
Затем соберите данные во фрагменте так же, как # 1. Здесь происходит то, что модель представления собирает данные из хранилища, а фрагмент собирает данные из модели представления.
Используя MutableLiveData, я могу обновлять данные из любой точки мира, используя ссылку на переменную. Есть ли способ сделать то же самое с Flow?
Вы не можете излучать значение вне потока. Блок кода внутри потока выполняется только при наличии какого-либо коллектора. Но вы можете преобразовать поток в живые данные, используя расширение asLiveData из LiveData.
ViewModel
В вашем случае вы можете сделать это
редактировать
Спасибо @mark за его комментарий. Создание нового потока в модели представления для getWeatherForecast функции фактически не требуется . Это может быть переписано как
Источник
Migrating from LiveData to Kotlin’s Flow
LiveData was something we needed back in 2017. The observer pattern made our lives easier, but options such as RxJava were too complex for beginners at the time. The Architecture Components team created LiveData: a very opinionated observable data holder class, designed for Android. It was kept simple to make it easy to get started and the recommendation was to use RxJava for more complex reactive streams cases, taking advantage of the integration between the two.
DeadData?
LiveData is still our solution for Java developers, beginners, and simple situations. For the rest, a good option is moving to Kotlin Flows. Flows still have a steep learning curve but they are part of the Kotlin language, supported by Jetbrains; and Compose is coming, which fits nicely with the reactive model.
We’ve been talking about using Flows for a while to connect the different parts of your app except for the view and ViewModel. Now that we have a safer way to collect flows from Android UIs, we can create a complete migration guide.
In this post you’ll learn how to expose Flows to a view, how to collect them, and how to fine-tune it to fit specific needs.
Flow: Simple things are harder and complex things are easier
LiveData did one thing and it did it well: it exposed data while caching the latest value and understanding Android’s lifecycles. Later we learned that it could also start coroutines and create complex transformations, but this was a bit more involved.
Let’s look at some LiveData patterns and their Flow equivalents:
#1: Expose the result of a one-shot operation with a Mutable data holder
This is the classic pattern, where you mutate a state holder with the result of a coroutine:
To do the same with Flows, we use (Mutable)StateFlow:
StateFlow is a special kind of SharedFlow (which is a special type of Flow), closest to LiveData:
- It always has a value.
- It only has one value.
- It supports multiple observers (so the flow is shared).
- It always replays the latest value on subscription, independently of the number of active observers.
When exposing UI state to a view, use StateFlow. It’s a safe and efficient observer designed to hold UI state.
#2: Expose the result of a one-shot operation
This is the equivalent to the previous snippet, exposing the result of a coroutine call without a mutable backing property.
With LiveData we used the liveData coroutine builder for this:
Since the state holders always have a value, it’s a good idea to wrap our UI state in some kind of Result class that supports states such as Loading , Success , and Error .
The Flow equivalent is a bit more involved because you have to do some configuration:
stateIn is a Flow operator that converts a Flow to StateFlow. Let’s trust these parameters for now, as we need more complexity to explain it properly later.
#3: One-shot data load with parameters
Let’s say you want to load some data that depends on the user’s ID and you get this information from an AuthManager that exposes a Flow:
With LiveData you would do something similar to this:
switchMap is a transformation whose body is executed and the result subscribed to when userId changes.
If there’s no reason for userId to be a LiveData, a better alternative to this is to combine streams with Flow and finally convert the exposed result to LiveData.
Doing this with Flows looks very similar:
Note that if you need more flexibility you can also use transformLatest and emit items explicitly:
#4: Observing a stream of data with parameters
Now let’s make the example more reactive. The data is not fetched, but observed, so we propagate changes in the source of data automatically to the UI.
Continuing with our example: instead of calling fetchItem on the data source, we use a hypothetical observeItem operator that returns a Flow.
With LiveData you can convert the flow to LiveData and emitSource all the updates:
Or, preferably, combine both flows using flatMapLatest and convert only the output to LiveData:
The Flow implementation is similar but it doesn’t have LiveData conversions:
The exposed StateFlow will receive updates whenever the user changes or the user’s data in the repository is changed.
#5 Combining multiple sources: MediatorLiveData -> Flow.combine
MediatorLiveData lets you observe one or more sources of updates (LiveData observables) and do something when they get new data. Usually, you update the value of the MediatorLiveData:
The Flow equivalent is much more straightforward:
You can also use the combineTransform function, or zip.
Configuring the exposed StateFlow (stateIn operator)
We previously used stateIn to convert a regular flow to a StateFlow, but it requires some configuration. If you don’t want to go into detail right now and just need to copy-paste, this combination is what I recommend:
However, if you’re not sure about that seemingly random 5-second started parameter, read on.
stateIn has 3 parameters (from docs):
started can take 3 values:
- Lazily : start when the first subscriber appears and stop when scope is cancelled.
- Eagerly : start immediately and stop when scope is cancelled
- WhileSubscribed : It’s complicated.
For one-shot operations you can use Lazily or Eagerly . However, if you’re observing other flows, you should use WhileSubscribed to do small but important optimizations as explained below.
The WhileSubscribed strategy
WhileSubscribed cancels the upstream flow when there are no collectors. The StateFlow created using stateIn exposes data to the View, but it’s also observing flows coming from other layers or the app (upstream). Keeping these flows active might lead to wasting resources, for example, if they continue reading data from other sources such as a database connection, hardware sensors, etc. When your app goes to the background, you should be a good citizen and stop these coroutines.
WhileSubscribed takes two parameters:
Stop timeout
From its documentation:
stopTimeoutMillis configures a delay (in milliseconds) between the disappearance of the last subscriber and the stopping of the upstream flow. It defaults to zero (stop immediately).
This is useful because you don’t want to cancel the upstream flows if the view stopped listening for a fraction of a second. This happens all the time — for example, when the user rotates the device and the view is destroyed and recreated in quick succession.
The solution in the liveData coroutine builder was to add a delay of 5 seconds after which the coroutine would be stopped if no subscribers are present. WhileSubscribed(5000) does exactly that:
This approach checks all the boxes:
- When the user sends your app to the background, updates coming from other layers will stop after five seconds, saving battery.
- The latest value will still be cached so that when the user comes back to it, the view will have some data immediately.
- Subscriptions are restarted and new values will come in, refreshing the screen when available.
Replay expiration
If you don’t want the user to see stale data when they’ve gone away for too long and you prefer to display a loading screen, check out the replayExpirationMillis parameter in WhileSubscribed . It’s very handy in this situation and it also saves some memory, as the cached value is restored to the initial value defined in stateIn . Coming back to the app won’t be as snappy, but you won’t show old data.
replayExpirationMillis — configures a delay (in milliseconds) between the stopping of the sharing coroutine and the resetting of the replay cache (which makes the cache empty for the shareIn operator and resets the cached value to the original initialValue for the stateIn operator). It defaults to Long.MAX_VALUE (keep replay cache forever, never reset buffer). Use zero value to expire the cache immediately.
Observing StateFlow from the view
As we’ve seen so far, it’s very important for the view to let the StateFlows in the ViewModel know that they’re no longer listening. However, as with everything related to lifecycles, it’s not that simple.
In order to collect a flow, you need a coroutine. Activities and fragments offer a bunch of coroutine builders:
- Activity.lifecycleScope.launch : starts the coroutine immediately and cancels it when the activity is destroyed.
- Fragment.lifecycleScope.launch : starts the coroutine immediately and cancels it when the fragment is destroyed.
- Fragment.viewLifecycleOwner.lifecycleScope.launch : starts the coroutine immediately and cancels it when the fragment’s view lifecycle is destroyed. You should use the view lifecycle if you’re modifying UI.
LaunchWhenStarted, launchWhenResumed…
Specialized versions of launch called launchWhenX will wait until the lifecycleOwner is in the X state and suspend the coroutine when the lifecycleOwner falls below the X state. It’s important to note that they don’t cancel the coroutine until their lifecycle owner is destroyed.
Receiving updates while the app is in the background could lead to crashes, which is solved by suspending the collection in the View. However, upstream flows are kept active while the app is in the background, possibly wasting resources.
This means that everything we’ve done so far to configure StateFlow would be quite useless; however, there’s a new API in town.
lifecycle.repeatOnLifecycle to the rescue
This new coroutine builder (available from lifecycle-runtime-ktx 2.4.0-alpha01) does exactly what we need: it starts coroutines at a particular state and it stops them when the lifecycle owner falls below it.
For example, in a Fragment:
This will start collecting when the view of the Fragment is STARTED , will continue through RESUMED , and will stop when it goes back to STOPPED . Read all about it in A safer way to collect flows from Android UIs.
Mixing the repeatOnLifecycle API with the StateFlow guidance above will get you the best performance while making a good use of the device’s resources.
Warning: The StateFlow support recently added to Data Binding uses launchWhenCreated to collect updates, and it will start using repeatOnLifecycle `instead when it reaches stable.
For Data Binding, you should use Flows everywhere and simply add asLiveData() to expose them to the view. Data Binding will be updated when lifecycle-runtime-ktx 2.4.0 goes stable.
Summary
The best way to expose data from a ViewModel and collect it from a view is:
- ✔️ Expose a StateFlow , using the WhileSubscribed strategy, with a timeout. [example]
- ✔️ Collect with repeatOnLifecycle . [example]
Any other combination will keep the upstream Flows active, wasting resources:
- ❌ Expose using WhileSubscribed and collect inside lifecycleScope.launch / launchWhenX
- ❌ Expose using Lazily / Eagerly and collect with repeatOnLifecycle
Of course, if you don’t need the full power of Flow… just use LiveData. 🙂
Источник
Более безопасный способ сбора потоков данных из пользовательских интерфейсов Android
В приложении для Android потоки Kotlin обычно собираются из пользовательского интерфейса для отображения обновлений данных на экране. Однако, собирая эти потоки (flows) данных, следует убедиться, что не приходится выполнять больше работы, чем необходимо, тратить ресурсы (как процессора, так и памяти) или допускать утечку данных, когда представление переходит в фоновый режим.
В этой статье вы узнаете, как API Lifecycle.repeatOnLifecycle и Flow.flowWithLifecycle защищают вас от пустой траты ресурсов и почему их лучше использовать по умолчанию для сбора потоков данных из пользовательского интерфейса.
Неэффективное использование ресурсов
Рекомендуется предоставлять API Flow с нижних уровней иерархии вашего приложения, независимо от деталей имплементации производителя потока данных. При этом следует также безопасно собирать их.
Холодный поток, поддерживаемый каналом или использующий операторы с буферами, такие как buffer, conflate, flowOn или shareIn, небезопасно собирать с помощью некоторых из существующих API, таких как CoroutineScope.launch, Flow .launchIn или LifecycleCoroutineScope.launchWhenX, за исключением случаев, если вы вручную отменяете Job, запустивший корутину, когда активность переходит в фон. Эти API сохранят производителя стандартного потока активным, пока он будет эмитировать элементы в буфер в фоновом режиме, таким образом будут расходоваться ресурсы.
Примечание: Холодный поток — это тип потока, который по требованию выполняет блок кода производителя, когда необходимо собрать данные для нового подписчика.
Например, рассмотрим этот поток, который выдает обновления местоположения с помощью callbackFlow:
Примечание: Внутри callbackFlow использует канал, который концептуально очень похож на очередь блокировки и имеет емкость по умолчанию 64 элемента.
Сбор этого потока из пользовательского интерфейса с помощью любого из вышеупомянутых API обеспечивает передачу местоположений, даже если представление не отображает их в пользовательском интерфейсе! См. пример ниже:
lifecycleScope.launchWhenStarted приостанавливает выполнение корутины. Новые местоположения не обрабатываются, но производитель callbackFlow тем не менее продолжает отправлять местоположения. Использование API lifecycleScope.launch или launchIn еще более опасно, поскольку представление продолжает использовать местоположения, даже если оно находится в фоновом режиме! Что потенциально может привести к отказу вашего приложения.
Чтобы решить эту проблему с этими API, вам нужно будет вручную отменить сбор данных, когда представление перейдет в фоновый режим, чтобы отменить callbackFlow и избежать такого, когда провайдер местоположений будет эмитировать элементы и тратить ресурсы. Например, вы можете сделать что-то вроде следующего:
Это хорошее решение, но это шаблонный код, друзья! И если существует всеобщая истина о разработчиках Android, то она такова, что мы совершенно не любим писать шаблонный код. Одно из самых больших преимуществ отказа от написания шаблонного кода заключается в том, что при небольшом количестве кода меньше шансов совершить ошибку!
Lifecycle.repeatOnLifecycle
Теперь, когда мы пришли к единому мнению и знаем, где кроется проблема, настало время придумать решение. Решение должно быть 1) простым, 2) дружественным или легким для запоминания/понимания, и, что более важно, 3) безопасным! Оно должно работать для всех случаев использования, независимо от деталей имплементации потока.
Без лишних слов, API, который вы должны использовать, это Lifecycle.repeatOnLifecycle , доступный в библиотеке lifecycle-runtime-ktx.
Примечание: Эти API доступны в библиотеке lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 или более поздней версии.
Взгляните на следующий код:
repeatOnLifecycle — это функция приостановки, принимающая Lifecycle.State в качестве параметра, который используется для автоматического создания и запуска новой корутины с переданным ей блоком, когда жизненный цикл достигает этого state , и отмены текущей корутины, выполняющей этот блок, когда жизненный цикл падает ниже state .
Это позволяет обойтись без использования шаблонного кода, поскольку код, для отмены корутины, когда она больше не нужна, автоматически выполняется функцией repeatOnLifecycle . Как вы могли догадаться, рекомендуется вызывать этот API в методах onCreate активности или onViewCreated фрагмента, чтобы избежать неожиданного поведения. Смотрите пример ниже с использованием фрагментов:
Важно: Фрагменты всегда должны использовать viewLifecycleOwner для запуска обновлений пользовательского интерфейса. Однако это не относится к DialogFragments , у которых иногда может не быть View. Для DialogFragments можно использовать lifecycleOwner .
Примечание: Эти API доступны в библиотеке lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 или более поздней версии.
Копнем глубже!
repeatOnLifecycle приостанавливает вызывающую корутину, повторно запускает блок, когда жизненный цикл переходит в таргет state и из него в новую корутину, и возобновляет вызывающую корутину, когда Lifecycle уничтожается. Последний пункт очень важен: вызывающая программа, которая вызывает repeatOnLifecycle , не возобновит выполнение до тех пор, пока жизненный цикл не будет уничтожен.
Визуальная диаграмма
Возвращаясь к началу, сбор locationFlow непосредственно из корутины, запущенной с помощью lifecycleScope.launch , был опасен, поскольку он продолжался, даже когда представление находилось в фоновом режиме.
repeatOnLifecycle предотвращает трату ресурсов и сбои приложения, поскольку останавливает и перезапускает сбор потока, когда жизненный цикл переходит в таргет-состояние и обратно.
Разница между использованием и неиспользованием API repeatOnLifecycle
Flow.flowWithLifecycle
Вы также можете использовать оператор Flow.flowWithLifecycle , когда у вас есть только один поток для сбора. Этот API использует repeatOnLifecycle , эмитирует элементы и отменяет стандартного производителя, когда Lifecycle переходит в таргет-состояние и выходит из него.
Примечание: Это API использует оператор Flow.flowOn(CoroutineContext) в качестве прецедента, поскольку Flow.flowWithLifecycle изменяет CoroutineContext , используемый для сбора восходящего потока, оставляя при этом нисходящий поток незатронутым. Также, подобно flowOn , Flow.flowWithLifecycle добавляет буфер на случай, если потребитель не успевает за производителем. Это связано с тем, что его имплементация использует callbackFlow .
Настройка производителя стандартного потока
Если вы используете эти API, остерегайтесь горячих потоков, которые могут тратить ресурсы, хотя данные потоки никто не собирает! Имейте ввиду, что для них есть несколько подходящих случаев использования, и если это необходимо, документируйте. Наличие активного производителя стандартного потока в фоновом режиме, даже если он тратит ресурсы, может быть полезно для некоторых сценариев использования: вы мгновенно получаете свежие данные, а не наверстываете упущенное и временно показываете несвежие данные. В зависимости от сценария использования, решите, должен ли производитель быть всегда активным или нет.
API MutableStateFlow и MutableSharedFlow предоставляют поле subscriptionCount , которое можно использовать для остановки производителя стандартного потока, когда subscriptionCount равно нулю. По умолчанию они будут поддерживать производителя активным до тех пор, пока объект, содержащий экземпляр потока, находится в памяти. Однако для этого есть несколько подходящих случаев использования, например, UiState , передаваемый из ViewModel в UI с помощью StateFlow . Это нормально! Этот случай использования требует, чтобы ViewModel всегда предоставляла последнее состояние пользовательского интерфейса представлению.
Точно так же операторы Flow.stateIn и Flow.shareIn могут быть сконфигурированы для этого с правилами запуска общего доступа. WhileSubscribed() остановит производителя стандартного потока, если нет активных наблюдателей! Напротив, Eagerly или Lazily будут поддерживать базового производителя активным до тех пор, пока активен CoroutineScope, который они используют.
Примечание: API, показанные в этой статье, являются хорошим вариантом по умолчанию для сбора потоков из пользовательского интерфейса и должны использоваться независимо от деталей реализации потока. Эти API делают то, что должны: прекращают сбор, если пользовательский интерфейс не виден на экране. Это зависит от имплементации потока, должен ли он быть всегда активным или нет.
Безопасный сбор Flow в Jetpack Compose
Функция Flow.collectAsState используется в Compose для сбора потоков из компонуемых объектов и представления значений в виде State для обновления пользовательского интерфейса (UI) Compose . Даже если Compose не перекомпоновывает UI, когда активность хоста или фрагмента находится в фоновом режиме, производитель потоков все еще активен и может тратить ресурсы. Compose может испытывать аналогичную проблему, что и система представления (View).
При сборе потоков в Compose используйте оператор Flow.flowWithLifecycle следующим образом:
Обратите внимание, что вам требуется поток remember, который знает о жизненном цикле, с locationFlow и lifecycleOwner в качестве ключей, чтобы всегда использовать один и тот же поток, если только один из ключей не изменится.
В Compose побочные эффекты должны выполняться в контролируемой среде. Для этого используйте LaunchedEffect, чтобы создать корутину, которая следует за жизненным циклом составного компонента. В ее блоке вы можете вызвать приостановку Lifecycle.repeatOnLifecycle , если вам нужно, чтобы она повторно запустила блок кода, когда жизненный цикл хоста находится в определенном State .
Сравнение с LiveData
Вы могли заметить, что этот API ведет себя аналогично LiveData, и это действительно так! LiveData знает о Lifecycle , и возможность перезапуска делает его идеальным для наблюдения за потоками данных из пользовательского интерфейса. Это также справедливо для API Lifecycle.repeatOnLifecycle и Flow.flowWithLifecycle !
Сбор потоков с помощью этих API является естественной заменой LiveData в приложениях, работающих только на Kotlin. Если вы используете эти API для сбора потоков, LiveData не имеет никаких преимуществ перед корутинами и потоками. Более того, потоки более гибкие, так как их можно собирать из любого Dispatcher , и они могут работать со всеми его операторами. В отличие от LiveData , который имеет ограниченное количество доступных операторов и значения которых всегда наблюдаются из потока UI.
Поддержка StateFlow в связывании данных
С другой стороны, одна из причин, по которой вы, возможно, используете LiveData, заключается в том, что он поддерживается в привязке данных. Так вот, StateFlow также имеет такую поддержку! Для получения дополнительной информации о поддержке StateFlow в привязке данных ознакомьтесь с официальной документацией.
Используйте API Lifecycle.repeatOnLifecycle или Flow.flowWithLifecycle для безопасного сбора потоков данных из пользовательского интерфейса в Android.
Перевод материала подготовлен в преддверии старта курса «Android Developer. Basic».
Источник