Compose


https://developer.android.com/compose
https://d.android.com/jetpack/compose/documentation
https://developer.android.com/jetpack/compose/mental-model
https://developer.android.com/jetpack/compose/phases
https://developer.android.com/jetpack/compose/layering
https://developer.android.com/jetpack/compose/performance
https://developer.android.com/jetpack/compose/semantics
https://developer.android.com/jetpack/compose/compositionlocal
https://d.android.com/jetpack/compose/performance/stability
08.08.2024https://habr.com/ru/companies/clevertec/articles/834052/
24.07.2024https://habr.com/ru/companies/alfa/articles/827510/
15.03.2024https://habr.com/ru/companies/otus/articles/800521/
28.02.2024https://habr.com/ru/articles/796437/
23.06.2023https://habr.com/ru/companies/ozontech/articles/742854/
25.01.2023https://youtu.be/qb0Ezy-WO_k
Compose

Cовременная, декларативная библиотека для создания пользовательских интерфейсов в Android. Compose значительно упрощает разработку UI, заменяя традиционную императивную модель (основанную на Views и XML) на декларативный подход.

Декларативный UI: В Compose UI описывается как функция. Вы определяете, как интерфейс должен выглядеть в зависимости от текущего состояния данных, а библиотека автоматически обновляет UI при изменении этих данных.

@Composable
fun Greeting(name: String) {
    Text(text = "Hello, $name!")
}

Состояние и управление: Compose использует состояние для обновления интерфейса. Если изменяется состояние, вызов функции обновляет только измененные части интерфейса — это называется рекомпозицией. Состояние в Compose управляется с помощью таких функций, как remember, mutableStateOf, rememberSaveable.

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Clicked $count times")
    }
}

Без XML: Compose полностью устраняет необходимость в XML-файлах для описания интерфейсов. Всё создается и управляется на уровне кода.

Модульность и многократное использование: Compose предлагает возможность создавать небольшие, независимые и переиспользуемые компоненты. Это упрощает создание и поддержку сложных интерфейсов.

Совместимость с View: Compose и традиционные Views могут взаимодействовать друг с другом. Вы можете встраивать Views в Compose или использовать Compose в существующих проектах с Views.

@Composable
fun CustomView() {
    AndroidView(factory = { context -> 
        TextView(context).apply {
            text = "This is a traditional Android View!"
        }
    })
}

Упрощенная анимация: В Compose встроена поддержка анимаций, которая позволяет легко добавлять динамичные эффекты в интерфейс.

@Composable
fun AnimatedBox() {
    var expanded by remember { mutableStateOf(false) }
    val size by animateDpAsState(if (expanded) 200.dp else 100.dp)

    Box(
        Modifier
            .size(size)
            .clickable { expanded = !expanded }
            .background(Color.Blue)
    )
}
Lifecycle

Жизненный цикл Compose отличается от традиционных View-систем в Android, так как он основан на декларативном подходе к построению интерфейсов. В Compose нет привычных методов жизненного цикла, таких как onCreate, onStart и onDestroy. Вместо этого, основной механизм работы с UI — это рекомпозиция и управление состоянием.

Этапы жизненного цикла в Compose

• Composition (Композиция)

• Recomposition (Рекомпозиция)

• Disposal (Уничтожение)

Composition

• Это процесс создания и отображения UI на основе данных (состояния).

• Когда функция с аннотацией @Composable вызывается, Compose создает дерево компонентов и отображает его на экране.

• Происходит единовременное построение интерфейса на основе начальных значений состояния.

Recomposition

• После начальной композиции, когда изменяется состояние (например, с помощью mutableStateOf), происходит рекомпозиция.

• Compose повторно вызывает функции @Composable, которые зависят от изменившихся данных, и обновляет только ту часть интерфейса, которая изменилась.

• Это позволяет эффективно управлять изменениями UI без полной перерисовки.

// При изменении значения count, Compose перерисует только часть интерфейса, связанную с этой переменной, вызывая рекомпозицию для компонента Text.

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text("Clicked $count times")
    }
}

Disposal

• Когда элемент UI больше не нужен (например, пользователь ушел с экрана или компонент был удален), Compose выполняет его уничтожение.

• Это освобождает ресурсы, такие как анимации или асинхронные задачи, связанные с данным UI.

Annotations

@Composable

Эта аннотация сообщает компилятору Compose, что эта функция предназначена для преобразования данных в пользовательский интерфейс. Функция ничего не возвращает.

@Preview

Позволяет наблюдать изменение виджета прямо в IDE без необходимости перезапускать приложение на устройстве.

@ReadOnlyComposable

Если composable-функция выполняет только операции чтения, то можно пометить её аннотацией @ReadOnlyComposable. Это даст небольшой прирост производительности. Основной сценарий использования - функция, которой аннотация @Composable нужна только для чтения CompositionLocal (например, чтение цвета из темы), а не для вызова других composable-функций.

@Stable

Указывает, что объект стабилен и его свойства не изменяются неожиданно. Это важно для оптимизации процессов компоновки (composition) и перерисовки (recomposition). Когда объект помечен как @Stable, Compose может не перерисовывать его повторно, если его свойства явно не изменились, что помогает избежать лишних перерисовок и улучшить производительность приложения. Изменения состояния могут происходить, но будут управляемыми.

// В этом примере класс User помечен как @Stable, что говорит Compose, что объект не изменяется неожиданно. 
// Даже если UserInfo будет вызываться несколько раз в процессе композиции, если свойства User не изменились, перерисовка не будет выполнена.

@Stable
class User(val name: String, val age: Int)

@Composable
fun UserInfo(user: User) {
    Text(text = "Name: ${user.name}, Age: ${user.age}")
}

@Immutable

Указывает, что объект неизменяемый (immutable), то есть все его поля являются неизменяемыми (final). Если объект помечен как @Immutable, это означает, что его состояние никогда не изменится после инициализации, что позволяет Compose не выполнять повторные перерисовки, когда он используется в UI.

@Immutable
data class Person(val name: String, val age: Int)

@Composable
fun PersonInfo(person: Person) {
    Text(text = "Name: ${person.name}, Age: ${person.age}")
}
State Functions

key

remember создает объект один раз и возвращает нам его все последующие вызовы. Но иногда нам необходимо пересоздать объект из remember. Если приводить аналогию с кэшем, то нам надо сбросить кэш и заново получить данные. Пока key не меняется, remember каждый раз возвращает нам ранее созданный объект. Но если при очередном вызове remember у нас сменился key, то remember снова выполняет код по созданию объекта. И теперь каждый раз будет возвращать нам этот новый объект, пока key снова не поменяется.

val exampleWithKey: Example = remember(paramKey) { Example() }

by

Сейчас для работы с значением State мы используем его поле value. Но это можно сделать немного проще с помощью специальных делегатов. Теперь State можно использовать как обычную var переменную и для чтения значения и для записи.

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

val checked: Boolean by remember { mutableStateOf(false) }
println(checked)

remember

С помощью этой функции мы создаем объект внутри Composable-функции только 1 раз при первом запуске. Получаем этот объект во всех последующих перезапусках. Работает как кэш. Если объекта еще нет, то она его создаст. А если он уже был создан, она его вернет нам. При каждом перезапуске мы получаем и используем один и тот же объект.

class Example

@Composable
fun ComposeFunc(
		paramKey: Boolean
) {
		val example: Example = remember { Example() }
}

rememberSaveable

Версия remember которая сохраняет значение при изменении конфигурации.

var checked: Boolean by rememberSaveable { mutableStateOf(false) }

rememberUpdateState

Используется когда мы хотим сохранить обновленную ссылку на переменную в длительном side effect без необходимости перезапуска при рекомпозиции.

@Composable
fun TwoButtonScreen() {
    var buttonColour by remember { mutableStateOf("Unknown") }
		Button(onClick = { buttonColour = "Red" })  
    Button(onClick = { buttonColour = "Black" })
		Timer(buttonColor = buttonColour)
}

@Composable
fun Timer(
    buttonColour: String
) {
    println("Composing timer with colour : $buttonColour")
    val buttonColorUpdated by rememberUpdatedState(newValue = buttonColour)
    LaunchedEffect(key1 = Unit) {
        startTimer(5000L) {
            println("Timer ended")
            println("[1] Last pressed button color is $buttonColour") // bad
            println("[2] Last pressed button color is $buttonColorUpdated") // good
        }
    })
}

suspend fun startTimer(time: Long, onTimerEnd: () -> Unit) {
    delay(timeMillis = time)
    onTimerEnd()
}
State Builders

mutableStateOf

В комбинации remember + mutableStateOf, функция mutableStateOf создает State, а функция remember делает, так, чтобы этот State не сбрасывался при каждом перезапуске функции.

val checked: MutableState<Boolean> = remember { mutableStateOf(false) }
println(checked.value)

mutableIntStateOf

Держатель значения для примитивного типа Int.

var page: Int by remember { mutableIntStateOf(0) }

mutableLongStateOf

Держатель значения для примитивного типа Long.

var time: Long by remember { mutableLongStateOf(0L) }

mutableFloatStateOf

Держатель значения для примитивного типа Float.

var value: Float by remember { mutableFloatStateOf(0F) }

mutableDoubleStateOf

Держатель значения для примитивного типа Double.

var value: Double by remember { mutableDoubleStateOf(0.0) }

mutableStateListOf

Cоздает мутируемый список, который поддерживает реактивное обновление пользовательского интерфейса при изменении элементов внутри этого списка. Этот список является аналогом стандартного MutableList, но с возможностью интеграции в реактивную систему Compose. Когда вы используете список, созданный с помощью mutableStateListOf, любые изменения в списке (добавление, удаление или изменение элементов) автоматически вызывают перерисовку тех частей интерфейса, которые зависят от этого списка. Это полезно для создания динамических интерфейсов, где состояние списка влияет на отображение UI-компонентов.

• Время доступа к элементам списка и выполнения операций, таких как добавление или удаление, аналогично обычному ArrayList, то есть время доступа к элементам — O(1), добавление в конец — O(1), удаление — O(n).

• Пространственная сложность зависит от количества элементов в списке, как и в обычном списке.

@Composable
fun DynamicListExample() {
    // Создаем состояние списка с помощью mutableStateListOf
    val items: SnapshotStateList<String> = remember { mutableStateListOf("Item 1", "Item 2", "Item 3") }

    Column(modifier = Modifier.padding(16.dp)) {
        // Отображаем каждый элемент списка
        items.forEach { item ->
            Text(text = item, modifier = Modifier.padding(4.dp))
        }

        // Добавляем кнопку для добавления нового элемента в список
        Button(onClick = { 
            items.add("Item ${items.size + 1}") 
        }) {
            Text("Add Item")
        }
    }
}
derivedStateOf

Использовать когда входные данные меняются чаще чем нужна рекомпозиция. Пример: изменение положения скролла. Действует анологично оператору distinctUntilChanged в Kotlin Flow. Подписывается на изменения состояний, которые были прочитаны за первый проход. Примеры:

• порог прокрутки (scrollPosition > 0).

• количество элементов больше порог(items > 0).

• валидация формы (username.isValid)

val listState = rememberLazyListState()
val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } }
Snapshot

Концепция, которая используется в Compose для управления состоянием и его отслеживания. Она обеспечивает механизм для наблюдения за изменениями данных и синхронизирует эти изменения с интерфейсом пользователя (UI). Основная цель механизма снапшотов — сделать работу с состоянием в Compose реактивной, то есть когда данные изменяются, UI автоматически обновляется. Снимки состояния позволяют изолировать и отслеживать изменения, сделанные в реактивных данных, и гарантируют, что эти изменения будут безопасно применяться, особенно в многопоточных приложениях.

// Compose позволяет вручную управлять снапшотами, используя методы вроде Snapshot.takeSnapshot, 
// чтобы работать с состоянием более тонко, когда это необходимо.

@Composable
fun ManualSnapshotExample() {
    val state = remember { mutableStateOf("Initial") }

    Column {
        Text(text = "Current state: ${state.value}")

        Button(onClick = {
            // Создаём новый снапшот
            Snapshot.takeSnapshot {
                state.value = "Changed in Snapshot"
            }
        }) {
            Text("Change State in Snapshot")
        }
    }
}

SnapshotStateList

Это специальный тип списка, созданный для работы с системой состояния Compose. По сути, mutableStateListOf возвращает SnapshotStateList, который отслеживает изменения и интегрируется с Compose. Обычные списки List или MutableList не будут автоматически обновлять пользовательский интерфейс при изменении данных. Например, если вы измените элементы внутри обычного List, вам нужно будет вручную обновить состояние, чтобы заставить Compose перерисовать элементы UI. SnapshotStateList поддерживает систему снимков (Snapshot) в Compose, чтобы отслеживать изменения состояния.

snapshotFlow

Специальная функция в Compose, которая позволяет наблюдать за изменениями состояния в системе снапшотов и преобразовывать эти изменения в поток данных (Flow). Это полезный механизм для интеграции состояния Compose с асинхронной моделью потоков, например, с Flow, когда вам нужно реагировать на изменения состояния и обрабатывать их в асинхронных операциях, таких как сетевые запросы, обновления UI и другие задачи. Когда вы используете реактивное состояние в Compose, например, через mutableStateOf, изменения этого состояния автоматически управляются системой снапшотов. С помощью snapshotFlow можно следить за этими изменениями и отправлять их как элементы Flow, что даёт вам больше гибкости для работы с состоянием в асинхронной среде.

@Composable
fun SnapshotFlowExample() {
    val count = remember { mutableStateOf(0) }
    val scope = rememberCoroutineScope()

    LaunchedEffect(Unit) {
        // Преобразуем изменения состояния в Flow с помощью snapshotFlow
        snapshotFlow { count.value }
            .collect { newValue ->
                // Обрабатываем новое значение из snapshotFlow
                println("Collected new value: $newValue")
            }
    }

    Column {
        Text(text = "Counter: ${count.value}")

        Button(onClick = {
            count.value += 1
        }) {
            Text("Increase Counter")
        }
    }
}
CompositionLocal

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

Компонент который хочет использовать значение CompositionLocal может использовать текущее свойство ключа CompositionLocal которое возвращает текущее значение CompositionLocal и подписывает компонент на его изменения.

val ActiveUser = compositionLocalOf<User> { error("No active user found!") }

@Composable
fun App(user: User) {
    CompositionLocalProvider(ActiveUser provides user) {
        SomeScreen()
    }
}

@Composable
fun SomeScreen() {
    UserPhoto()
}

@Composable
fun UserPhoto() {
    val user = ActiveUser.current
    ProfileIcon(src = user.profilePhotoUrl)
}

staticCompositionLocalOf

Чтение значения не отслеживается Compose. Изменение значения приводит к перекомпоновке всего content где предоставляется, а не только мест, где значение считывается в композиции. Если значение, предоставленное для CompositionLocal никогда не изменится используй staticCompositionLocalOf для лучшей производительности.

ViewCompositionStrategy

Определяет стратегию автоматического удаления композиции. Для ComposeView и AbstractComposeView. Нужен для интеропа Compose и View.

DisposeOnDetachedFromWindowOrReleasedFromPool. Используется по умолчанию. Композиция будет автоматически удалена при отсоединении View от Window если только она не является частью контейнера пула такого как RecyclerView.

DisposeOnDetachedFromWindow - композиция будет автоматически удалена при отсоединении View от Window.

DisposeOnLifecycleDestroyed(LifecycleOwner) - удаляет композицию когда возвращенный LifecycleOwner уничтожается.

DisposeOnViewTreeLifecycleDestroyed - удаляет композицию при переходе Lifecycle в состояние Destroyed.

BackHandler

Позволяет перехватывать и обрабатывать нажатие кнопки “Назад” внутри @Composable-функции.

@Composable
fun MyScreen() {
    // Обработка нажатия кнопки "Назад"
    BackHandler(enabled = isHandlerEnabled) {
        // Действия, которые должны выполниться при нажатии кнопки "Назад"
    }

    // Основное содержимое экрана
    Text(text = "Текст")
}
nestedScrollConnection

Сспособ реагировать на фазы цикла вложенной прокрутки.

• пример: дочерний экран со списком внутри Pager на родительском экране с AppBar. При скролле будет прокручиваться список дочернего экрана, когда перемотка будет завершена - будет прокручиваться AppBar родительского экрана.

val nestedScrollConnection = rememberNestedScrollInteropConnection()

Scaffold(
    modifier = Modifier.nestedScroll(nestedScrollConnection)
) {}
Compose. Вопросы на собесе
  1. Для чего нужен nestedScrollConnection?
  1. Отличия remember и rememberSaveable?
  1. Для чего нужен CompositionLocal?
  1. Как Compose понимает, что стейт поменялся?
  1. Как LocalContext работает под капотом?
  1. Разница между compositionLocalOf и staticCompositionLocalOf?
  1. Число лежит в mutableState без remember при клике увеличивается. Что произойдет через 3 нажатия?
  1. Для чего нужен mutableState?
  1. Как понять, что код на Compose оптимально написан?
  1. Что из себя представляет remember?

    Функция, которая сохраняет результат вычисления в памяти, чтобы избежать повторных вычислений при перерисовке компонента, сохраняя значение между составными вызовами.

  1. Как работают аннотации @Stable и @Immutable?

    @Immutable указывает, что объект полностью неизменен и его состояние не изменится после создания, что позволяет Compose оптимизировать рендеринг.

    @Stable обозначает, что объект может изменяться, но изменения управляются контролируемо, что позволяет Compose правильно отслеживать такие изменения и обновлять UI при необходимости.

  1. Как работает @Composable-функция?

    Функция с аннотацией @Composable описывает часть UI, которая добавляется в дерево композиций и может реагировать на изменения состояния. При каждом изменении состояния система вызывает функцию заново, чтобы обновить соответствующие элементы интерфейса.

  1. Как часто вызывается @Composable-функция?

    Функция @Composable вызывается каждый раз, когда изменяется её состояние или параметры. Если параметры функции изменяются, она будет вызвана снова для обновления пользовательского интерфейса, даже без изменения состояния.

  1. Как ускорить Compose?

    Заменить XML-иконки на ImageVector.

    Заменить Modifier.composed на Modifier.Node.

    Использовать специальные типы для MutableState.

    Использовать аннотации @Stable @Immutable @ReadOnlyComposable.

    Подключить линтеры.

  1. Чем Compose отличается от View?

    Декларативный подход, интеллектуальная перекомпановка, типобезопасность Kotlin.

  1. Что делает объект Composer под капотом у @Composable-функций?

    Управляет состоянием и перерисовкой @Composable-функций, отслеживая их изменения и обновляя только измененные части UI. Он строит и изменяет дерево композиций, оптимизируя обновление интерфейса для повышения производительности.