В этом уроке используем комбинацию 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 

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




Language

Автор сайта

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

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

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

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

 

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

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



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



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

Яндекс
410011180491924

WebMoney
R248743991365
Z551306702056

Paypal