Site icon AppTractor

Побочные эффекты Jetpack Compose в подробностях

Jetpack Compose значительно упростил разработку пользовательских интерфейсов в Android, но все же важно понимать, как правильно управлять эффектами для повышения производительности. В этой статье мы рассмотрим три важные функции Composable, которые помогают нам эффективно управлять эффектами пользовательского интерфейса: SideEffect, LaunchedEffect и DisposableEffect.

Зачем нужен побочные эффекты?

Цель побочных эффектов в Jetpack Compose — обеспечить возможность выполнения операций, не связанных с пользовательским интерфейсом, которые изменяют состояние приложения вне Composable-функции контролируемым и предсказуемым образом.

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

Jetpack Compose предоставляет несколько функций Composable, таких как SideEffect, LaunchedEffect и DisposableEffect. Основными преимуществами использования побочных эффектов в Jetpack Compose являются:

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

Примеры использования LaunchedEffect

Примеры использования SideEffect

Приведем пример использования 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:

И последний совет: композиты в идеале должны быть свободны от побочных эффектов.

Источник

Exit mobile version