Jetpack Compose значительно упростил разработку пользовательских интерфейсов в Android, но все же важно понимать, как правильно управлять эффектами для повышения производительности. В этой статье мы рассмотрим три важные функции Composable, которые помогают нам эффективно управлять эффектами пользовательского интерфейса: SideEffect
, LaunchedEffect
и DisposableEffect
.
Зачем нужен побочные эффекты?
Цель побочных эффектов в Jetpack Compose — обеспечить возможность выполнения операций, не связанных с пользовательским интерфейсом, которые изменяют состояние приложения вне Composable-функции контролируемым и предсказуемым образом.
Побочные эффекты, такие как обновление базы данных или сетевой вызов, должны быть отделены от логики рендеринга пользовательского интерфейса для повышения производительности и удобства сопровождения кода.
Jetpack Compose предоставляет несколько функций Composable, таких как SideEffect
, LaunchedEffect
и DisposableEffect
. Основными преимуществами использования побочных эффектов в Jetpack Compose являются:
- Повышение производительности: Выполнение операций, не связанных с пользовательским интерфейсом, за пределами функций Composable позволяет логике рендеринга пользовательского интерфейса оставаться отзывчивой и производительной.
- Улучшение организации кода: Отделение операций, не связанных с пользовательским интерфейсом, от логики рендеринга, упрощает понимание и сопровождение кодовой базы.
- Улучшение отладки: Побочные эффекты могут использоваться для протоколирования и аналитики, что помогает разработчикам лучше понять поведение своих приложений и выявить проблемы.
SideEffect
SideEffect — это функция Composable, которая позволяет нам выполнять побочный эффект при перекомпоновке родительской Composable. Побочный эффект — это операция, которая не влияет непосредственно на пользовательский интерфейс, например, ведение лога, аналитика или обновление внешнего состояния. Эта функция полезна для выполнения операций, которые не зависят от состояния или свойств Composable.
При перекомпоновке Composable весь код внутри функции Composable выполняется заново, включая все побочные эффекты. Однако пользовательский интерфейс будет обновлен только теми изменениями, которые были внесены в состояние или реквизиты Composable.
Как использовать SideEffect?
Чтобы использовать SideEffect, необходимо вызвать его внутри функции Composable и передать лямбду, содержащую побочный эффект, который мы хотим выполнить. Приведем пример:
@Composable fun Counter() { // Define a state variable for the count val count = remember { mutableStateOf(0) } // Use SideEffect to log the current value of count SideEffect { // Called on every recomposition log("Count is ${count.value}") } Column { Button(onClick = { count.value++ }) { Text("Increase Count") } // With every state update, text is changed and recomposition is triggered Text("Counter ${count.value}") } }
В данном примере функция SideEffect логирует текущее значение переменной состояния count
при каждой перекомпоновке функции Counter
. Это полезно для отладки и мониторинга поведения Composable.
Следует помнить, что побочный эффект срабатывает только при рекомпозиции текущей Composable-функции, но не для всех вложенных Composable-функций. Это означает, что если у вас есть Composable-функция, которая вызывает другую Composable-функцию, то SideEffect во внешней Composable-функции не будет срабатывать при рекомпозиции внутренней Composable-функции. Чтобы понять это, изменим код следующим образом:
@Composable fun Counter() { // Define a state variable for the count val count = remember { mutableStateOf(0) } // Use SideEffect to log the current value of count SideEffect { // Called on every recomposition log("Count is ${count.value}") } Column { Button(onClick = { count.value++ }) { // This recomposition doesn't trigger the outer side effect // every time button has been tapped Text("Increase Count ${count.value}") } } }
В приведенном выше коде при первом запуске приложения составляется функция Counter
, а SideEffect записывает в консоль начальное значение count
. При нажатии на Button
, композабл Text
перекомпонуется с новым значением count
, но это не вызывает повторного срабатывания побочного эффекта.
Теперь добавим внутренний побочный эффект, чтобы посмотреть, как это работает:
@Composable fun Counter() { // Define a state variable for the count val count = remember { mutableStateOf(0) } // Use SideEffect to log the current value of count SideEffect { // Called on every recomposition log("Outer Count is ${count.value}") } Column { Button(onClick = { count.value++ }) { // Use SideEffect to log the current value of count SideEffect { // Called on every recomposition log("Inner Count is ${count.value}") } // This recomposition doesn't trigger the outer side effect // every time button has been tapped Text("Increase Count ${count.value}") } } }
В приведенном выше коде при нажатии на кнопку вывод будет выглядеть следующим образом:
Outer Count is 0 Inner Count is 0 Inner Count is 1 Inner Count is 2 Inner Count is 3
Уверен, теперь вы понимаете логику работы.
LaunchedEffect
LaunchedEffect — это Composable-функция, выполняющая побочный эффект в отдельном скоупе корутины. Эта функция полезна для выполнения операций, которые могут занимать длительное время, например, сетевых вызовов или анимации, без блокировки потока пользовательского интерфейса.
Приведем пример использования LaunchedEffect:
@Composable fun MyComposable() { val isLoading = remember { mutableStateOf(false) } val data = remember { mutableStateOf(listOf<String>()) } // Define a LaunchedEffect to perform a long-running operation asynchronously // `LaunchedEffect` will cancel and re-launch if // `isLoading.value` changes LaunchedEffect(isLoading.value) { if (isLoading.value) { // Perform a long-running operation, such as fetching data from a network val newData = fetchData() // Update the state with the new data data.value = newData isLoading.value = false } } Column { Button(onClick = { isLoading.value = true }) { Text("Fetch Data") } if (isLoading.value) { // Show a loading indicator CircularProgressIndicator() } else { // Show the data LazyColumn { items(data.value.size) { index -> Text(text = data.value[index]) } } } } } // Simulate a network call by suspending the coroutine for 2 seconds private suspend fun fetchData(): List<String> { // Simulate a network delay delay(2000) return listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5",) }
В данном примере функция LaunchedEffect
выполняет сетевой вызов для получения данных из API, когда переменная состояния isLoading
установлена в true
. Функция выполняется в отдельной области действия корутины, что позволяет пользовательскому интерфейсу оставаться отзывчивым во время выполнения операции.
Функция LaunchedEffect
принимает два параметра: key
, который устанавливается в значение isLoading.value
, и block
, который представляет собой лямбду, определяющую побочный эффект, который должен быть выполнен. В данном случае лямбда block
вызывает функцию fetchData()
, которая имитирует сетевой вызов, приостанавливая выполнение корутины на 2 секунды. После получения данных происходит обновление переменной состояния data
и установка значения isLoading
в false
, что позволяет скрыть индикатор загрузки и отобразить полученные данные.
Какова логика работы ключевого параметра?
Параметр key
в LaunchedEffect
используется для идентификации экземпляра LaunchedEffect
и предотвращения его ненужной перекомпоновки.
Когда Composable перекомпонуется, Jetpack Compose определяет, нужно ли его перерисовывать. Если состояние или реквизиты Composable изменились, или если Composable вызвал команду invalidate
, Jetpack Compose перерисует Composable. Перерисовка Composable может быть дорогостоящей операцией, особенно если Composable содержит долго выполняющиеся операции или побочные эффекты, которые не нужно выполнять заново при каждой перекомпоновке Composable.
Предоставив параметр key для LaunchedEffect, мы можем указать значение, которое однозначно идентифицирует экземпляр LaunchedEffect. Если значение ключевого параметра изменится, Jetpack Compose будет рассматривать экземпляр LaunchedEffect как новый экземпляр и выполнит побочный эффект заново. Если значение ключевого параметра не изменится, то Jetpack Compose пропустит выполнение побочного эффекта и повторно использует предыдущий результат, предотвращая ненужные перекомпоновки.
Вы также можете использовать несколько ключей для LaunchedEffect
:
// Use a random UUID as the key for LaunchedEffect val key = remember { UUID.randomUUID().toString() } LaunchedEffect(key, isLoading.value) { .... }
DisposableEffect
DisposableEffect
— это функция Composable, выполняющая побочный эффект при первом рендеринге родительского Composable и утилизирующая эффект при удалении Composable из иерархии пользовательского интерфейса. Эта функция полезна для управления ресурсами, которые должны быть очищены, когда Composable больше не используется, например, слушателями событий или анимациями.
Вот пример использования DisposableEffect
:
@Composable fun TimerScreen() { val elapsedTime = remember { mutableStateOf(0) } DisposableEffect(Unit) { val scope = CoroutineScope(Dispatchers.Default) val job = scope.launch { while (true) { delay(1000) elapsedTime.value += 1 log("Timer is still working ${elapsedTime.value}") } } onDispose { job.cancel() } } Text( text = "Elapsed Time: ${elapsedTime.value}", modifier = Modifier.padding(16.dp), fontSize = 24.sp ) }
В этом коде мы используем DisposableEffect
для запуска корутины, которая каждую секунду увеличивает значение состояния elapsedTime
. Мы также используем DisposableEffect
для обеспечения отмены этой программы и очистки ресурсов, использованных ею, когда Composable больше не используется.
В функции очистки DisposableEffect
мы отменяем работу программы с помощью метода cancel()
экземпляра Job
, хранящегося в job
.
Функция onDispose
вызывается, когда Composable удаляется из иерархии пользовательского интерфейса, и предоставляет возможность очистить все ресурсы, использованные Composable. В данном случае мы используем функцию onDispose
для отмены работы корутины и обеспечения очистки всех ресурсов, использованных этой программой.
Чтобы проверить, как работает этот DisposableEffect
, выполним следующий код и посмотрим на результат:
@Composable fun RunTimerScreen() { val isVisible = remember { mutableStateOf(true) } Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Bottom ) { Spacer(modifier = Modifier.height(10.dp)) if (isVisible.value) TimerScreen() Button(onClick = { isVisible.value = false }) { Text("Hide the timer") } } }
Я добавил новый компонент RunTimerScreen
, который позволяет пользователю переключать видимость экрана таймера. Когда пользователь нажимает кнопку «Скрыть таймер», TimerScreen
Composable удаляется из иерархии пользовательского интерфейса, а короутин отменяется и очищается.
Если убрать вызов job.cancel()
из функции onDispose
, то короутин продолжит выполняться даже тогда, когда TimerScreen
Composable больше не используется, что может привести к утечкам памяти и другим проблемам с производительностью.
Используя DisposableEffect
и CoroutineScope
таким образом, мы гарантируем, что запущенная CoroutineScope
программа будет отменена, а ресурсы очищены, когда TimerScreen
Composable перестанет использоваться. Это позволяет избежать утечек и других проблем с производительностью, а также повысить производительность и стабильность работы нашего приложения.
Когда использовать каждый из эффектов
Примеры использования DisposableEffect
- Добавление и удаление слушателей событий
- Запуск и остановка анимации
- Привязка и отвязка ресурсов сенсоров, таких как Camera, LocationManager и т.д.
- Управление соединениями с базой данных
Примеры использования LaunchedEffect
- Получение данных из сети
- Выполнение обработки изображений
- Обновление базы данных
Примеры использования SideEffect
- Ведение логов и аналитика
- Выполнение однократной инициализации, например, установка соединения с Bluetooth-устройством, загрузка данных из файла или инициализация библиотеки.
Приведем пример использования SideEffect для однократной инициализации:
@Composable fun MyComposable() { val isInitialized = remember { mutableStateOf(false) } SideEffect { if (!isInitialized.value) { // Execute one-time initialization tasks here initializeBluetooth() loadDataFromFile() initializeLibrary() isInitialized.value = true } } // UI code here }
Резюме
Вот краткое описание различий между SideEffect, DisposableEffect и LaunchedEffect:
SideEffect
выполняется, когда родительский Composable перекомпонован, и полезен для выполнения операций, не зависящих от состояния или реквизитов Composable.DisposableEffect
выполняется при первом рендеринге родительского Composable и полезен для управления ресурсами, которые необходимо очистить, когда Composable больше не используется. Он срабатывает при первой композиции или смене ключа и вызывает методonDispose
при завершении.LaunchedEffect
выполняет побочный эффект в отдельном скоупе корутины и удобен для выполнения длительных операций без блокировки потока пользовательского интерфейса. Он срабатывает при первой композиции или смене ключа.
И последний совет: композиты в идеале должны быть свободны от побочных эффектов.