В этом уроке используем комбинацию 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
- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня