В этом уроке используем комбинацию remember + mutableStateOf

 

Чтобы понять, зачем нужна комбинация remember + mutableStateOf, необходимо обсудить один важный вопрос: где хранить State?

В примерах прошлых уроков мы хранили State снаружи Composable функций - в Activity. В реальном приложении у нас данные будут храниться во ViewModel. Но так как мы еще не знаем, как это делать, то пока продолжаем работать с Activity, а ViewModel держим в уме.

 

State снаружи Composable функции

Давайте снова рассмотрим пример с CheckBox. Допустим, этот чекбокс у нас используется для изменения какой-то важной настройки:

@Composable
fun HomeScreen(
   checked: State<Boolean>,
   onCheckedChange: (Boolean) -> Unit
) {
   val checkedValue = checked.value
   Row(verticalAlignment = CenterVertically) {
       Checkbox(checked = checkedValue, onCheckedChange = onCheckedChange)
       Text("Some important preference",  fontSize = 18.sp)
   }
}

Стандартная схема. Получаем State и лямбду и используем их в CheckBox.

 

State хранится в Activity:

class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       val checked = mutableStateOf(false)
       setContent {
           HomeScreen(
               checked = checked,
               onCheckedChange = { newCheckedValue ->
                   checked.value = newCheckedValue
               }
           )
       }
   }
}

 

Что нам дает хранение State в Activity (или в ViewModel), а не в самой Composable функции? Этот вопрос можно немного переформулировать. Что нам дает хранение State в коде, где реализована логика, а не в UI коде?

Основной смысл такого расположения State в том, что это дает нам постоянный доступ к значению чекбокса в коде с логикой. В момент нажатия на чекбокс (onCheckedChange) или по нажатию на какую-нибудь кнопку Save мы легко можем взять значение чекбокса и сохранить в SharedPreferences или отправить на сервер.

Еще один плюс в том, что мы можем захотеть программно включить/выключить чекбокс. Мы просто меняем значение State у себя в логике, а Composable функция сама перерисовывается.

Ну и еще плюс - это возможность покрыть тестами логику изменения State.

 

 

State внутри Composable функции

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

Рассмотрим простой пример:

@Composable
fun HomeScreen(
   checked: State<Boolean>,
   onCheckedChange: (Boolean) -> Unit
) {
   val checkedValue = checked.value
   Column {
       Row(verticalAlignment = CenterVertically) {
           Checkbox(checked = checkedValue, onCheckedChange = onCheckedChange)
           Text("More details", fontSize = 18.sp)
       }
       if (checkedValue) {
           Text(text = stringResource(id = R.string.details))
       }
   }
}

У нас есть чекбокс, по нажатию на который показывается дополнительный текст.

Т.е. это просто подсказка для пользователя, если он хочет получить больше информации о чем-либо на экране.

 

Сейчас State со значением этого чекбокса хранится в Activity. Но мы не собираемся никуда сохранять/отправлять это значение, или менять его программно. В нашей логике оно не нужно. Поэтому нам нет особого смысла хранить его в Activity. Переместим этот State в HomeScreen:

import androidx.compose.runtime.mutableStateOf

@Composable
fun HomeScreen() {
    val checked = mutableStateOf(false)
    val checkedValue = checked.value
    Column {
        Row(verticalAlignment = CenterVertically) {
            Checkbox(checked = checkedValue, onCheckedChange = { value -> checked.value = value })
            Text("More details", fontSize = 18.sp)
        }
        if (checkedValue) {
            Text(text = stringResource(id = R.string.details))
        }
    }
}

 

Студия подчеркивает mutableStateOf и ругается: "Creating a state object during composition without using remember"

Мы пока проигнорируем это. Нам самим надо понять, в чем именно тут проблема.

 

Код в Activity теперь выглядит так

class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           HomeScreen()
       }
   }
}

Только вызов Composable функции и никаких State.

 

Запускаем, проверяем

Чекбокс перестал работать. Давайте разбираться почему.

 

Мы в HomeScreen создаем State. HomeScreen читает этот State, а значит подписывается на изменение его значения.

Когда мы на экране нажимаем на чекбокс, мы меняем значение State (в onCheckedChange). Это приводит к тому, что функция HomeScreen перезапускается. При перезапуске она должна прочитать новое значение из State и отобразить его. 

Но вместо этого она каждый раз создает новый State со значением false и читает значение из него:

@Composable
fun HomeScreen() {
   val checked = mutableStateOf(false)
   val checkedValue = checked.value
   // ...
}

HomeScreen читает значение этого создаваемого State, всегда получает false и отображает выключенный чекбокс.

Т.е. проблема в том, что функция теряет прошлый State, который получил значение true при клике на чекбокс. Вместо него она сама же и создает новый State со значением false.

Когда мы State держали в Activity, такой проблемы не было. Потому что Activity создавало State и хранило его у себя. Он получал новые значения, но он не пересоздавался. Нам надо сделать так, чтобы Composable функция создавала State один раз и потом всегда его использовала даже в случае перезапусков. Для этого у нас есть функция remember из прошлого урока.

Оборачиваем в нее создание State:

val checked = remember { mutableStateOf(false) }

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

 

Запускаем

Все работает.

 

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

 

 

Делегат by

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

Вместо

val checked = remember { mutableStateOf(false) }

 

Мы можем написать

var checked by remember { mutableStateOf(false) }

 

Чтобы это сработало, необходимо добавить импорт

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

 

Теперь State можно использовать как обычную var переменную и для чтения значения и для записи:

var checked by remember { mutableStateOf(false) }

Checkbox(checked = checked, onCheckedChange = { value -> checked = value })

Обращаться к его полю value уже не нужно.

 

 

rememberSaveable

У remember есть версия, которая способна сохранить значение даже при повороте экрана и завершении процесса. Это rememberSaveable.

 

 

Если курс вас заинтересовал, то приобрести полную версию можно на странице курса.

 


Присоединяйтесь к нам в Telegram:

- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance 

- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня




Комментарии   

# rememberSaveableДанила 24.10.2023 13:52
Я правильно понимаю, что предпочтительнее использовать rememberSaveable вместо обыкновенного remember и не париться о сохранении состояния элемента?
# RE: rememberSaveableDmitry Vinogradov 09.11.2023 20:44
Не думаю. Все таки rememberSaveable работает чуть дольше, чем remember, т.к. он там парсит данные. Если он явно не нужен, то лучше обойтись обычным remember. А все критичные данные хранить в ViewModel

Language

Автор сайта

Дмитрий Виноградов

Подробнее можно посмотреть или почитать.

Никакие другие люди не имеют к этому сайту никакого отношения и просто занимаются плагиатом.

Социальные сети

 

В канале я публикую ссылки на интересные и полезные статьи по Android

В чате можно обсудить вопросы и проблемы, возникающие при разработке



Группа ВКонтакте



Поддержка проекта

Яндекс
410011180491924

WebMoney
R248743991365
Z551306702056

Paypal