Pull to refresh

В каких случаях использовать функцию derivedStateOf из Jetpack Compose

Level of difficultyMedium
Reading time4 min
Views5.5K

derivedStateOf { ... }

derivedStateOf - это функция, которая из исходных объектов State формирует производный State. Основной смысл применения функции derivedStateOf - понизить частоту изменения исходного State, тем самым избавиться от ненужных рекомпозиций.

Лямбда-выражение, которое передается в derivedStateOf, выполняется каждый раз, когда изменяется любой из входных объектов State, и результат используется для обновления значения производного State. Важно понимать, что лямбда-выражение будет вызываться повторно только если изменится свойство value объекта State и чтение этого свойства происходит в этой лямбде. Изменение захваченных переменных другого типа не приведет к повторному вызову лямбда-выражения.

У объекта State может читаться свойство value в одной или нескольких compose-функциях. Через объект MutableState можно менять свойство value в callback'ах compose-функций или где-то еще за пределами compose, например во ViewModel'и. Если свойство изменилось, то в процессе построения следующего кадра произойдет рекомпозиция (повторный вызов) тех compose-функций, где это свойство читается. Таким образом частота изменения объекта MutableState определяет частоту рекомпозиций, но не чаще чем системный Frame rate, это примерно 60Hz, то есть 60 раз в секунду. Когда изменится State, Compose обновит UI при построении следующего кадра.

Когда частота изменения State слишком высокая, или другими словами есть некоторая последовательность изменений value State'а, при которой не нужно обновлять UI пускай даже значения разные, а следовательно не нужно совершать рекомпозицию, в этих случаях нужно использовать derivedStateOf. derivedStateOf определяет производный объект State от одного или нескольких других объектов State. Производный State должен обобщать исходные объекты State, то есть исходное множество значений State должно делиться на подмножества, и каждое такое подмножество будет соответствовать значению из производного множества. Изменения исходного State не всегда будут приводить к изменению производного State, таким образом понижается частота рекомпозиций.

Вот некоторые кейсы в которых понадобится derivedStateOf:

  • если пользователь доскроллил список до конца, то нужно разблокировать кнопку "далее"

@Composable
fun ListScreen(data: List<String>, onNextClick: () -> Unit) {
    val scrollState = rememberLazyListState()

    val isNextEnabled by remember {
        derivedStateOf {
            val lastVisibleIndex = scrollState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
            lastVisibleIndex == scrollState.layoutInfo.totalItemsCount - 1
        }
    }

    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        LazyColumn(
            state = scrollState,
            modifier = Modifier.fillMaxWidth().weight(1f)
        ) {
            items(data) { Item(it) }
        }
        Button(onClick = onNextClick, enabled = isNextEnabled) {
            Text(text = "Далее")
        }
    }
}
  • если пользователь начал проскролливать список, то нужно скрыть кнопку

@Composable
fun ListScreen(data: List<String>, onNextClick: () -> Unit) {
    val scrollState = rememberLazyListState()

    val isVisible by remember {
        derivedStateOf {
            scrollState.firstVisibleItemIndex == 0
        }
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        LazyColumn(
            state = scrollState,
            modifier = Modifier
                .fillMaxWidth()
                .weight(1f)
        ) {
            items(data) { Item(it) }
        }
        AnimatedVisibility(visible = isVisible) {
            Button(onClick = onNextClick) {
                Text(text = "Далее")
            }
        }
    }
}
  • на экране отображается два списка. Данные второго списка является подмножеством данных первого, таким образом при изменении первого списка не всегда будет изменяться второй

@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {

    val todoTasks = remember { mutableStateListOf<String>() }

    val highPriorityTasks by remember(highPriorityKeywords) {
        derivedStateOf {
            todoTasks.filter { task ->
                highPriorityKeywords.any { keyword ->
                    task.contains(keyword)
                }
            }
        }
    }

    Box(Modifier.fillMaxSize()) {
        LazyColumn {
            items(highPriorityTasks) { /* ... */ }
            items(todoTasks) { /* ... */ }
        }
    }
}
  • пользователь вводит данные в текстовое поле, и в зависимости от валидности введенных данных, кнопка будет или не будет заблокированной

@Composable
fun Screen() {
    var text by remember { mutableStateOf("") }
    val isButtonEnabled by remember { derivedStateOf { isValid(text) } }

    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        TextField(value = text, onValueChange = { text = it })
        Button(enabled = isButtonEnabled, onClick = { /* onClick() */ }) {
            Text(text = "Кнопка")
        }
    }
}
  • пользователь передвигает палец по экрану и нужно обобщить множество событий касания, например определить направление движения

@Composable
fun Gesture() {
    var dx by remember { mutableStateOf(0f) }
    var dy by remember { mutableStateOf(0f) }
    val vertical by remember {
        derivedStateOf {
            when {
                dy > 0f -> "движение вверх по вертикали"
                dy < 0f -> "движение вниз по вертикали"
                else -> "нет движение по вертикали"
            }
        }
    }
    val horizontal by remember {
        derivedStateOf {
            when {
                dx > 0f -> "движение влево по горизонтали"
                dx < 0f -> "движение вправо по горизонтали"
                else -> "нет движение по горизонтали"
            }
        }
    }
    Column(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                coroutineScope {
                    while (true) {
                        awaitPointerEventScope {
                            while (true) {
                                val pointerEvent = awaitPointerEvent()
                                val change = pointerEvent.changes.firstOrNull()
                                dx = (change?.previousPosition?.x ?: 0f) - (change?.position?.x ?: 0f)
                                dy = (change?.previousPosition?.y ?: 0f) - (change?.position?.y ?: 0f)
                            }
                        }
                    }
                }
            }
    ) {
        Text(text = vertical)
        Text(text = horizontal)
    }
}

Tags:
Hubs:
Total votes 4: ↑3 and ↓1+3
Comments1

Articles