Irbisa · Шпаргалка

Состояние в Compose

remember, mutableStateOf, derivedStateOf, поднятие состояния, ViewModel + StateFlow

Middle-разработчик · 20 вопросов

  1. 01

    Обработчик клика у этого счётчика срабатывает, но число на экране не меняется. Что не так?

    @Composablefun Counter() {    var count = 0    Button(onClick = { count++ }) {        Text("Clicked $count times")    }}

    count объявлен как обычная локальная переменная: при каждой рекомпозиции она снова становится 0, а запись в неё ничего не сообщает Compose.

  2. 02

    Что такое mutableStateOf и как его изменение запускает рекомпозицию?

    mutableStateOf(value) создаёт наблюдаемый MutableState<T>: когда composable читает .value, Compose запоминает это чтение, а когда кто-то записывает новое значение, все composable, которые его читали, ставятся в очередь на рекомпозицию.

    @Composablefun LikeButton() {    var liked by remember { mutableStateOf(false) }    var taps by remember { mutableIntStateOf(0) } // no boxing on writes     Row {…
  3. 03

    Чем remember отличается от rememberSaveable: что переживает рекомпозицию, поворот экрана и смерть процесса?

    remember сохраняет значение только между рекомпозициями; rememberSaveable дополнительно записывает его в saved instance state, поэтому оно переживает смену конфигурации и смерть процесса.

    @Composablefun SearchBar() {    var expanded by remember { mutableStateOf(false) }     // lost on rotation    var query by rememberSaveable { mutableStateOf("") }   // restored after rotation and process death    val normalized = remember(query) { query.trim().lowercase() } // recomputed when query changes…
  4. 04

    Что такое state hoisting, чем stateful-composable отличается от stateless и где должно жить состояние?

    State hoisting (подъём состояния) выносит стейт из composable в вызывающий код: composable становится stateless и получает текущее value и колбэк onValueChange, а stateful-composable сам владеет стейтом через remember.

    // stateful: owns its state, easy to drop in, impossible to control from outside@Composablefun NameFieldStateful() {    var name by rememberSaveable { mutableStateOf("") }    NameField(name = name, onNameChange = { name = it })}…
  5. 05

    Диалог держит remember { mutableStateOf(initialName) }, родитель передаёт новое имя, а поле показывает старое. Почему?

    @Composablefun NameDialog(initialName: String, onSave: (String) -> Unit) {    var name by remember { mutableStateOf(initialName) }     AlertDialog(        onDismissRequest = {},        confirmButton = { TextButton(onClick = { onSave(name) }) { Text("Save") } },        text = { TextField(value = name, onValueChange = { name = it }) },    )}

    remember выполняет свою лямбду только при первой композиции этого места вызова, а дальше возвращает сохранённое значение, поэтому параметр, изменившийся позже, никогда не попадёт в стейт, который он когда-то инициализировал.

  6. 06

    Как связать StateFlow из ViewModel с Compose: collectAsStateWithLifecycle, collectAsState или produceState?

    ViewModel отдаёт StateFlow<UiState>, а экран читает его через collectAsStateWithLifecycle(), который собирает значения, только пока жизненный цикл не ниже STARTED.

    data class ProfileUiState(val name: String = "", val isLoading: Boolean = true) class ProfileViewModel(repo: ProfileRepository) : ViewModel() {    val uiState: StateFlow<ProfileUiState> = repo.observeProfile()        .map { ProfileUiState(name = it.name, isLoading = false) }        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), ProfileUiState())…
  7. 07

    Почему добавление элемента в MutableList внутри mutableStateOf не обновляет экран и что меняют mutableStateListOf или новый список?

    Snapshot-стейт отслеживает ссылку, которую хранит, а не то, что происходит внутри объекта: list.add(x) оставляет тот же экземпляр, поэтому записи в стейт не происходит и ни один читатель не инвалидируется.

    class TodoHolder {    // wrong: the reference never changes, so readers are never invalidated    val broken = mutableStateOf(mutableListOf<String>())    fun addBroken(item: String) {        broken.value.add(item)    }…
  8. 08

    Что такое derivedStateOf и какую проблему он решает?

    `derivedStateOf { ...

    @Composablefun ArticleList(articles: List<Article>) {    val listState = rememberLazyListState()    val scope = rememberCoroutineScope()     // the index changes on every scroll frame; the Boolean flips rarely…
  9. 09

    Найдите баг: поле поиска фильтрует список правильно, но после обновления данных родителем список остаётся прежним.

    @Composablefun ProductList(products: List<Product>, maxPrice: Int) {    var query by rememberSaveable { mutableStateOf("") }    val visible by remember {        derivedStateOf {            products.filter { it.price <= maxPrice && it.name.contains(query, ignoreCase = true) }        }    }     Column {        TextField(value = query, onValueChange = { query = it })        LazyColumn {            items(visible, key = { it.id }) { ProductRow(it) }        }    }}

    remember без ключей создаёт derivedStateOf один раз, и его лямбда хранит products и maxPrice из первой композиции.

  10. 10

    Что такое side effects в Compose и когда использовать LaunchedEffect, DisposableEffect, SideEffect и produceState?

    Под side effect (побочным эффектом) понимают всё, что меняет мир за пределами composable; так как тело composable может перезапуститься в любой момент, эффекты идут через effect-API, которые запускаются, перезапускаются и убираются синхронно с композицией.

    @Composablefun UserScreen(userId: String, repo: UserRepository, analytics: Analytics) {    // restarts when userId changes, cancelled when the screen leaves    val user by produceState<User?>(initialValue = null, userId) {        value = repo.load(userId)    }…
  11. 11

    Эта кнопка должна показывать снекбар по нажатию, но код не компилируется. Почему и что выбрать: LaunchedEffect или rememberCoroutineScope?

    @Composablefun SaveButton(snackbarHostState: SnackbarHostState) {    Button(onClick = {        LaunchedEffect(Unit) {            snackbarHostState.showSnackbar("Saved")        }    }) {        Text("Save")    }}

    LaunchedEffect сам является composable, а onClick остаётся обычной лямбдой, поэтому компилятор выдаёт @Composable invocations can only happen from the context of a @Composable function.

  12. 12

    Родитель передаёт новый onTimeout, пока сплэш-экран ждёт, но вызывается старая лямбда. Почему и что исправляет rememberUpdatedState?

    @Composablefun SplashScreen(onTimeout: () -> Unit) {    LaunchedEffect(Unit) {        delay(3_000)        onTimeout()    }    Logo()}

    LaunchedEffect(Unit) запускается один раз, и его корутина захватывает onTimeout из первой композиции; следующие рекомпозиции передают новую лямбду, но запущенная корутина держит старую.

  13. 13

    Что такое snapshotFlow и когда нужно превращать стейт Compose во Flow?

    snapshotFlow { } превращает чтение стейта Compose в холодный Flow: выполняет блок, запоминает, какой стейт был прочитан, и эмитит новый результат, когда один из этих стейтов меняется.

    @Composablefun FeedAnalytics(listState: LazyListState, analytics: Analytics) {    LaunchedEffect(listState) {        snapshotFlow { listState.firstVisibleItemIndex }            .filter { it > 0 }            .collect { index -> analytics.logScrolledTo(index) }…
  14. 14

    Когда CompositionLocal служит правильным способом передать значение вниз по дереву, а когда это злоупотребление?

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

    data class Spacing(val small: Dp = 4.dp, val medium: Dp = 8.dp, val large: Dp = 16.dp) val LocalSpacing = staticCompositionLocalOf { Spacing() } // no sensible default: fail loudly instead of rendering with a fakeval LocalImageLoader = staticCompositionLocalOf<ImageLoader> {…
  15. 15

    Когда вы пишете обычный класс-state holder с фабрикой rememberXState() вместо того, чтобы держать стейт во ViewModel?

    Обычный state holder нужен для UI-логики, то есть стейта, который важен только UI и может умереть вместе с composable; ViewModel служит для бизнес-логики и данных экрана, которые должны пережить смену конфигурации и обращаются к слою данных.

    class CheckoutFormState(initialEmail: String) {    var email by mutableStateOf(initialEmail)        private set    var showErrors by mutableStateOf(false)        private set…
  16. 16

    Нажатие на звезду заполняет её, потом она на мгновение пустеет и снова заполняется. Что не так с этим кодом?

    @Composablefun FavouriteRow(item: Item, onToggle: (Long) -> Unit) {    var fav by remember(item) { mutableStateOf(item.isFavorite) }     IconToggleButton(        checked = fav,        onCheckedChange = {            fav = it            onToggle(item.id) // ViewModel sends the request        },    ) {        Icon(if (fav) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, "Favourite")    }}

    У строки два источника истины: локальная копия, которая переключается при нажатии, и значение из ViewModel, которое снова применяется со старым false до завершения записи, поэтому звезда откатывается, пока не придёт ответ сервера.

  17. 17

    ViewModel отдаёт StateFlow<UiState>, а коллега хочет вместо этого var uiState by mutableStateOf(...). В чём реальная разница?

    StateFlow оставляет ViewModel свободной от Compose и даёт операторы Flow; mutableStateOf проще читать в Compose и он перекомпонует по каждому объекту стейта, но привязывает ViewModel к рантайму Compose.

  18. 18

    При быстром наборе в TextField, значение которого возвращается из ViewModel, символы теряются или меняются местами. Почему и как этого избегает TextFieldState?

    Цикл value/onValueChange должен обновляться синхронно; любой асинхронный шаг позволяет полю показать текст старше набранного.

    val DigitsOnly = InputTransformation {    if (!asCharSequence().all { it.isDigit() }) revertAllChanges()} class PhoneViewModel : ViewModel() {    // the field edits this directly; no value/onValueChange round trip…
  19. 19

    Для формы оформления заказа: что кладут в rememberSaveable, что в SavedStateHandle, что перезагружают и как протестировать восстановление?

    Сохраняйте то, что пользователь ввёл или выбрал: поля только для UI кладите в rememberSaveable, то, что нужно ViewModel, кладите в SavedStateHandle; всё, что загружено из слоя данных, перезагружается по id.

    class CheckoutViewModel(private val handle: SavedStateHandle) : ViewModel() {    // small user input: survives process death    val note: StateFlow<String> = handle.getStateFlow("note", "")    fun onNoteChange(value: String) {        handle["note"] = value    }…
  20. 20

    Как устроена система снапшотов под стейтом Compose и что она гарантирует?

    Стейт Compose живёт в снапшотах (изолированных версионированных представлениях всех объектов стейта), которые дают каждому читателю согласованную картину и делают группу записей похожей на транзакцию.