[Программирование, Разработка под Android, Kotlin] Более безопасный способ сбора потоков данных из пользовательских интерфейсов Android (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В приложении для Android потоки Kotlin обычно собираются из пользовательского интерфейса для отображения обновлений данных на экране. Однако, собирая эти потоки (flows) данных, следует убедиться, что не приходится выполнять больше работы, чем необходимо, тратить ресурсы (как процессора, так и памяти) или допускать утечку данных, когда представление переходит в фоновый режим.В этой статье вы узнаете, как API Lifecycle.repeatOnLifecycle и Flow.flowWithLifecycle защищают вас от пустой траты ресурсов и почему их лучше использовать по умолчанию для сбора потоков данных из пользовательского интерфейса.. . .Неэффективное использование ресурсовРекомендуется предоставлять API Flow<T> с нижних уровней иерархии вашего приложения, независимо от деталей имплементации производителя потока данных. При этом следует также безопасно собирать их.Холодный поток, поддерживаемый каналом или использующий операторы с буферами, такие как buffer, conflate, flowOn или shareIn, небезопасно собирать с помощью некоторых из существующих API, таких как CoroutineScope.launch, Flow<T>.launchIn или LifecycleCoroutineScope.launchWhenX, за исключением случаев, если вы вручную отменяете Job, запустивший корутину, когда активность переходит в фон. Эти API сохранят производителя стандартного потока активным, пока он будет эмитировать элементы в буфер в фоновом режиме, таким образом будут расходоваться ресурсы.
Примечание: Холодный поток — это тип потока, который по требованию выполняет блок кода производителя, когда необходимо собрать данные для нового подписчика.
Например, рассмотрим этот поток, который выдает обновления местоположения с помощью callbackFlow:
// Implementation of a cold flow backed by a Channel that sends Location updates
fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
try { offer(result.lastLocation) } catch(e: Exception) {}
}
}
requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
.addOnFailureListener { e ->
close(e) // in case of exception, close the Flow
}
// clean up when Flow collection ends
awaitClose {
removeLocationUpdates(callback)
}
}
Сбор этого потока из пользовательского интерфейса с помощью любого из вышеупомянутых API обеспечивает передачу местоположений, даже если представление не отображает их в пользовательском интерфейсе! См. пример ниже:
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Collects from the flow when the View is at least STARTED and
// SUSPENDS the collection when the lifecycle is STOPPED.
// Collecting the flow cancels when the View is DESTROYED.
lifecycleScope.launchWhenStarted {
locationProvider.locationFlow().collect {
// New location! Update the map
}
}
// Same issue with:
// - lifecycleScope.launch { /* Collect from locationFlow() here */ }
// - locationProvider.locationFlow().onEach { /* ... */ }.launchIn(lifecycleScope)
}
}
lifecycleScope.launchWhenStarted приостанавливает выполнение корутины. Новые местоположения не обрабатываются, но производитель callbackFlow тем не менее продолжает отправлять местоположения. Использование API lifecycleScope.launch или launchIn еще более опасно, поскольку представление продолжает использовать местоположения, даже если оно находится в фоновом режиме! Что потенциально может привести к отказу вашего приложения.Чтобы решить эту проблему с этими API, вам нужно будет вручную отменить сбор данных, когда представление перейдет в фоновый режим, чтобы отменить callbackFlow и избежать такого, когда провайдер местоположений будет эмитировать элементы и тратить ресурсы. Например, вы можете сделать что-то вроде следующего:
class LocationActivity : AppCompatActivity() {
// Coroutine listening for Locations
private var locationUpdatesJob: Job? = null
override fun onStart() {
super.onStart()
locationUpdatesJob = lifecycleScope.launch {
locationProvider.locationFlow().collect {
// New location! Update the map
}
}
}
override fun onStop() {
// Stop collecting when the View goes to the background
locationUpdatesJob?.cancel()
super.onStop()
}
}
Это хорошее решение, но это шаблонный код, друзья! И если существует всеобщая истина о разработчиках Android, то она такова, что мы совершенно не любим писать шаблонный код. Одно из самых больших преимуществ отказа от написания шаблонного кода заключается в том, что при небольшом количестве кода меньше шансов совершить ошибку!Lifecycle.repeatOnLifecycleТеперь, когда мы пришли к единому мнению и знаем, где кроется проблема, настало время придумать решение. Решение должно быть 1) простым, 2) дружественным или легким для запоминания/понимания, и, что более важно, 3) безопасным! Оно должно работать для всех случаев использования, независимо от деталей имплементации потока.Без лишних слов, API, который вы должны использовать, это Lifecycle.repeatOnLifecycle, доступный в библиотеке lifecycle-runtime-ktx.
Примечание: Эти API доступны в библиотеке lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 или более поздней версии.
Взгляните на следующий код:
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create a new coroutine since repeatOnLifecycle is a suspend function
lifecycleScope.launch {
// The block passed to repeatOnLifecycle is executed when the lifecycle
// is at least STARTED and is cancelled when the lifecycle is STOPPED.
// It automatically restarts the block when the lifecycle is STARTED again.
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Safely collect from locationFlow when the lifecycle is STARTED
// and stops collection when the lifecycle is STOPPED
locationProvider.locationFlow().collect {
// New location! Update the map
}
}
}
}
}
repeatOnLifecycle — это функция приостановки, принимающая Lifecycle.State в качестве параметра, который используется для автоматического создания и запуска новой корутины с переданным ей блоком, когда жизненный цикл достигает этого state, и отмены текущей корутины, выполняющей этот блок, когда жизненный цикл падает ниже state.Это позволяет обойтись без использования шаблонного кода, поскольку код, для отмены корутины, когда она больше не нужна, автоматически выполняется функцией repeatOnLifecycle. Как вы могли догадаться, рекомендуется вызывать этот API в методах onCreate активности или onViewCreated фрагмента, чтобы избежать неожиданного поведения. Смотрите пример ниже с использованием фрагментов:
class LocationFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// ...
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
locationProvider.locationFlow().collect {
// New location! Update the map
}
}
}
}
}
Важно: Фрагменты всегда должны использовать viewLifecycleOwner для запуска обновлений пользовательского интерфейса. Однако это не относится к DialogFragments, у которых иногда может не быть View. Для DialogFragments можно использовать lifecycleOwner.
Примечание: Эти API доступны в библиотеке lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 или более поздней версии.
Копнем глубже!repeatOnLifecycle приостанавливает вызывающую корутину, повторно запускает блок, когда жизненный цикл переходит в таргет state и из него в новую корутину, и возобновляет вызывающую корутину, когда Lifecycle уничтожается. Последний пункт очень важен: вызывающая программа, которая вызывает repeatOnLifecycle, не возобновит выполнение до тех пор, пока жизненный цикл не будет уничтожен.
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create a coroutine
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
// Repeat when the lifecycle is RESUMED, cancel when PAUSED
}
// `lifecycle` is DESTROYED when the coroutine resumes. repeatOnLifecycle
// suspends the execution of the coroutine until the lifecycle is DESTROYED.
}
}
}
Визуальная диаграммаВозвращаясь к началу, сбор locationFlow непосредственно из корутины, запущенной с помощью lifecycleScope.launch, был опасен, поскольку он продолжался, даже когда представление находилось в фоновом режиме.repeatOnLifecycle предотвращает трату ресурсов и сбои приложения, поскольку останавливает и перезапускает сбор потока, когда жизненный цикл переходит в таргет-состояние и обратно.
Разница между использованием и неиспользованием API repeatOnLifecycleFlow.flowWithLifecycleВы также можете использовать оператор Flow.flowWithLifecycle, когда у вас есть только один поток для сбора. Этот API использует repeatOnLifecycle, эмитирует элементы и отменяет стандартного производителя, когда Lifecycle переходит в таргет-состояние и выходит из него.
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Listen to one flow in a lifecycle-aware manner using flowWithLifecycle
lifecycleScope.launch {
locationProvider.locationFlow()
.flowWithLifecycle(this, Lifecycle.State.STARTED)
.collect {
// New location! Update the map
}
}
// Listen to multiple flows
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// As collect is a suspend function, if you want to collect
// multiple flows in parallel, you need to do so in
// different coroutines
launch {
flow1.collect { /* Do something */ }
}
launch {
flow2.collect { /* Do something */ }
}
}
}
}
}
Примечание: Это 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<T> для обновления пользовательского интерфейса (UI) Compose. Даже если Compose не перекомпоновывает UI, когда активность хоста или фрагмента находится в фоновом режиме, производитель потоков все еще активен и может тратить ресурсы. Compose может испытывать аналогичную проблему, что и система представления (View).При сборе потоков в Compose используйте оператор Flow.flowWithLifecycle следующим образом:
@Composable
fun LocationScreen(locationFlow: Flow<Flow>) {
val lifecycleOwner = LocalLifecycleOwner.current
val locationFlowLifecycleAware = remember(locationFlow, lifecycleOwner) {
locationFlow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
}
val location by locationFlowLifecycleAware.collectAsState()
// Current location, do something with it
}
Обратите внимание, что вам требуется поток 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".
===========
Источник:
habr.com
===========
===========
Автор оригинала: Manuel Vivo
===========Похожие новости:
- [Информационная безопасность, Смартфоны, IT-компании] Google автоматически устанавливала на смартфоны жителей Массачусетса приложение для уведомлений о коронавирусе
- [JavaScript, Программирование] Полное руководство по созданию классических приложений на JavaScript (перевод)
- [Python, Программирование] Что вернёт эта функции в Python?
- [Программирование, Обработка изображений, Big Data, Машинное обучение] Помогите прочитать, что здесь написано? (OCR)
- [Программирование, Алгоритмы] Найти подстроку в строке
- [Программирование, Управление разработкой, Управление продажами, Карьера в IT-индустрии, Читальный зал] Дай таблетку, программист. Как в прошлый раз
- [JavaScript, Программирование, Node.JS] Как управлять несколькими потоками в Node JS (перевод)
- [Программирование, Венчурные инвестиции, Развитие стартапа, Образование за рубежом, Бизнес-модели] Перевод Курса по стартапам и бизнесу от Стэнфордского Университета. Лекция №3. Подготовка к созданию стартапа (перевод)
- [JavaScript, Программирование, Тестирование веб-сервисов, Машинное обучение] В закладки: репозитории с книгами, шпаргалками, ресурсами по дизайну и не только (перевод)
- [Программирование, Java] Создание самодостаточных исполняемых JAR (перевод)
Теги для поиска: #_programmirovanie (Программирование), #_razrabotka_pod_android (Разработка под Android), #_kotlin, #_android, #_kotlin, #_kotlin_coroutines, #_android_development, #_blog_kompanii_otus (
Блог компании OTUS
), #_programmirovanie (
Программирование
), #_razrabotka_pod_android (
Разработка под Android
), #_kotlin
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 08:41
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В приложении для Android потоки Kotlin обычно собираются из пользовательского интерфейса для отображения обновлений данных на экране. Однако, собирая эти потоки (flows) данных, следует убедиться, что не приходится выполнять больше работы, чем необходимо, тратить ресурсы (как процессора, так и памяти) или допускать утечку данных, когда представление переходит в фоновый режим.В этой статье вы узнаете, как API Lifecycle.repeatOnLifecycle и Flow.flowWithLifecycle защищают вас от пустой траты ресурсов и почему их лучше использовать по умолчанию для сбора потоков данных из пользовательского интерфейса.. . .Неэффективное использование ресурсовРекомендуется предоставлять API Flow<T> с нижних уровней иерархии вашего приложения, независимо от деталей имплементации производителя потока данных. При этом следует также безопасно собирать их.Холодный поток, поддерживаемый каналом или использующий операторы с буферами, такие как buffer, conflate, flowOn или shareIn, небезопасно собирать с помощью некоторых из существующих API, таких как CoroutineScope.launch, Flow<T>.launchIn или LifecycleCoroutineScope.launchWhenX, за исключением случаев, если вы вручную отменяете Job, запустивший корутину, когда активность переходит в фон. Эти API сохранят производителя стандартного потока активным, пока он будет эмитировать элементы в буфер в фоновом режиме, таким образом будут расходоваться ресурсы. Примечание: Холодный поток — это тип потока, который по требованию выполняет блок кода производителя, когда необходимо собрать данные для нового подписчика.
// Implementation of a cold flow backed by a Channel that sends Location updates
fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> { val callback = object : LocationCallback() { override fun onLocationResult(result: LocationResult?) { result ?: return try { offer(result.lastLocation) } catch(e: Exception) {} } } requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper()) .addOnFailureListener { e -> close(e) // in case of exception, close the Flow } // clean up when Flow collection ends awaitClose { removeLocationUpdates(callback) } } class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Collects from the flow when the View is at least STARTED and // SUSPENDS the collection when the lifecycle is STOPPED. // Collecting the flow cancels when the View is DESTROYED. lifecycleScope.launchWhenStarted { locationProvider.locationFlow().collect { // New location! Update the map } } // Same issue with: // - lifecycleScope.launch { /* Collect from locationFlow() here */ } // - locationProvider.locationFlow().onEach { /* ... */ }.launchIn(lifecycleScope) } } class LocationActivity : AppCompatActivity() {
// Coroutine listening for Locations private var locationUpdatesJob: Job? = null override fun onStart() { super.onStart() locationUpdatesJob = lifecycleScope.launch { locationProvider.locationFlow().collect { // New location! Update the map } } } override fun onStop() { // Stop collecting when the View goes to the background locationUpdatesJob?.cancel() super.onStop() } } Примечание: Эти API доступны в библиотеке lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 или более поздней версии.
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Create a new coroutine since repeatOnLifecycle is a suspend function lifecycleScope.launch { // The block passed to repeatOnLifecycle is executed when the lifecycle // is at least STARTED and is cancelled when the lifecycle is STOPPED. // It automatically restarts the block when the lifecycle is STARTED again. lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { // Safely collect from locationFlow when the lifecycle is STARTED // and stops collection when the lifecycle is STOPPED locationProvider.locationFlow().collect { // New location! Update the map } } } } } class LocationFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { // ... viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { locationProvider.locationFlow().collect { // New location! Update the map } } } } } Примечание: Эти API доступны в библиотеке lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 или более поздней версии.
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Create a coroutine lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { // Repeat when the lifecycle is RESUMED, cancel when PAUSED } // `lifecycle` is DESTROYED when the coroutine resumes. repeatOnLifecycle // suspends the execution of the coroutine until the lifecycle is DESTROYED. } } } Разница между использованием и неиспользованием API repeatOnLifecycleFlow.flowWithLifecycleВы также можете использовать оператор Flow.flowWithLifecycle, когда у вас есть только один поток для сбора. Этот API использует repeatOnLifecycle, эмитирует элементы и отменяет стандартного производителя, когда Lifecycle переходит в таргет-состояние и выходит из него. class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Listen to one flow in a lifecycle-aware manner using flowWithLifecycle lifecycleScope.launch { locationProvider.locationFlow() .flowWithLifecycle(this, Lifecycle.State.STARTED) .collect { // New location! Update the map } } // Listen to multiple flows lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { // As collect is a suspend function, if you want to collect // multiple flows in parallel, you need to do so in // different coroutines launch { flow1.collect { /* Do something */ } } launch { flow2.collect { /* Do something */ } } } } } } Примечание: Это API использует оператор Flow.flowOn(CoroutineContext) в качестве прецедента, поскольку Flow.flowWithLifecycle изменяет CoroutineContext, используемый для сбора восходящего потока, оставляя при этом нисходящий поток незатронутым. Также, подобно flowOn, Flow.flowWithLifecycle добавляет буфер на случай, если потребитель не успевает за производителем. Это связано с тем, что его имплементация использует callbackFlow.
Примечание: API, показанные в этой статье, являются хорошим вариантом по умолчанию для сбора потоков из пользовательского интерфейса и должны использоваться независимо от деталей реализации потока. Эти API делают то, что должны: прекращают сбор, если пользовательский интерфейс не виден на экране. Это зависит от имплементации потока, должен ли он быть всегда активным или нет.
@Composable
fun LocationScreen(locationFlow: Flow<Flow>) { val lifecycleOwner = LocalLifecycleOwner.current val locationFlowLifecycleAware = remember(locationFlow, lifecycleOwner) { locationFlow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED) } val location by locationFlowLifecycleAware.collectAsState() // Current location, do something with it } Перевод материала подготовлен в преддверии старта курса "Android Developer. Basic".
=========== Источник: habr.com =========== =========== Автор оригинала: Manuel Vivo ===========Похожие новости:
Блог компании OTUS ), #_programmirovanie ( Программирование ), #_razrabotka_pod_android ( Разработка под Android ), #_kotlin |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 08:41
Часовой пояс: UTC + 5