В этом уроке рассматриваем функцию remember и его параметр key.

 

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

Рассмотрим этот механизм на примере счетчика кликов:

@Composable
fun ClickCounter(
   counterValue: Int,
   onCounterClick: () -> Unit
) {
   Text(
       text = "Clicks: $counterValue",
       modifier = Modifier.clickable(onClick = onCounterClick)
   )
}

 

Мы хотим расширить пример. Рядом с числом кликов будем показывать: четное оно или нечетное.

Для этого мы создаем класс, который будет это определять

class EvenOdd() {
   fun check(value: Int): String {
       return if (value % 2 == 0) "even" else "odd"
   }
}

Метод на вход берет число и возвращает строку с результатом.

 

Будем создавать объект EvenOdd прямо в Composable функции:

@Composable
fun ClickCounter(
   counterValue: Int,
   onCounterClick: () -> Unit
) {
   val evenOdd = EvenOdd()
   Text(
       text = "Clicks: $counterValue ${evenOdd.check(counterValue)}",
       modifier = Modifier.clickable(onClick = onCounterClick)
   )
}

Создаем EvenOdd и используем для определения четности.

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

Результат:



Функция ClickCounter перезапускается каждый раз, когда мы делаем клик. Об этом мы подробно говорили в прошлом уроке.

 

Но каждый такой перезапуск приводит к тому, что выполняется код:

val evenOdd = EvenOdd()

и создается новый EvenOdd объект.

Чтобы убедиться в этом добавим логирование:

@Composable
fun ClickCounter(
   counterValue: Int,
   onCounterClick: () -> Unit
) {
   val evenOdd = EvenOdd()
   Text(
       text = "Clicks: $counterValue ${evenOdd.check(counterValue)}",
       modifier = Modifier.clickable(onClick = onCounterClick)
   )
   Log.d(TAG, "ClickCounter $counterValue ${evenOdd.hashCode().toHexString()}")
}

В лог выводим hashcode объекта EvenOdd.

 

Запускаем и кликаем:

ClickCounter 0 e1f3113
ClickCounter 1 422b4f3
ClickCounter 2 b178bae
ClickCounter 3 aa70cdc
ClickCounter 4 3fdcf6b

Каждый клик у нас разный hashCode. Значит, каждый вызов функции действительно создает новый объект. Это конечно не memory leak, потому что старые объекты удаляются, но все равно неприятно. Хотелось бы создавать объект только один раз.

В этом нам поможет remember. Используем его при создании объекта EvenOdd:

import androidx.compose.runtime.remember

@Composable
fun ClickCounter(
    counterValue: Int,
    onCounterClick: () -> Unit
) {
    val evenOdd = remember { EvenOdd() }
    Text(
        text = "Clicks: $counterValue ${evenOdd.check(counterValue)}",
        modifier = Modifier.clickable(onClick = onCounterClick)
    )
    Log.d(TAG, "ClickCounter $counterValue ${evenOdd.hashCode().toHexString()}")
}

Функция remember работает как кэш. Если объекта еще нет, то она его создаст. А если он уже был создан, она его вернет нам.

Т.е. когда функция ClickCounter вызывается первый раз, remember выполнит код внутри себя, чтобы создать объект EvenOdd, и вернет нам этот объект в переменную evenOdd. А все последующие запуски ClickCounter мы будем от remember получать этот ранее созданный EvenOdd.


Запускаем, кликаем и смотрим логи:

ClickCounter 0 e1f3113
ClickCounter 1 e1f3113
ClickCounter 2 e1f3113
ClickCounter 3 e1f3113
ClickCounter 4 e1f3113

При каждом перезапуске мы получаем и используем один и тот же объект EvenOdd.

 

 

key

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

Для таких случаев у remember есть параметр key:

remember(key) { ... }

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

 

Давайте расширим наш пример, чтобы увидеть, как работает key в remember.

В класс EvenOdd добавим новый функционал - uppercase:

class EvenOdd(private val uppercase: Boolean) {
   fun check(value: Int): String {
       var result = if (value % 2 == 0) "even" else "odd"
       if (uppercase) result = result.uppercase()
       return result
   }

   override fun toString(): String {
       return "EvenOdd(uppercase = $uppercase, hashcode = ${hashCode().toHexString()})"
   }
}

Конструктор EvenOdd теперь принимает Boolean параметр uppercase. Этим параметром мы настраиваем, надо ли методу check возвращать текст в верхнем регистре.

Также добавим в этот класс метод toString, чтобы его проще было логировать. В этом методе выводим значение uppercase и hashcode

 

Перепишем ClickCounter:

@Composable
fun ClickCounter(
   uppercase: Boolean,
   counterValue: Int,
   onCounterClick: () -> Unit
) {
   val evenOdd = remember { EvenOdd(uppercase) }
   Text(
       text = "Clicks: $counterValue ${evenOdd.check(counterValue)}",
       modifier = Modifier.clickable(onClick = onCounterClick)
   )
   Log.d(TAG, "ClickCounter(counter = $counterValue, uppercase = $uppercase), $evenOdd")
}

Функция ClickCounter принимает снаружи значение uppercase. Не будем подробно выяснять откуда именно оно приходит. Для нашего примера это не важно. Где-то там снаружи, в соседней Composable функции есть CheckBox. И от его состояния зависит, какое значение uppercase мы получаем в ClickCounter. Не буду приводить весь код, чтобы не усложнять пример.

Мы передаем uppercase в конструктор EvenOdd в remember. Давайте пока используем remember без key, чтобы посмотреть, что получится.

 

Запускаем приложение, кликаем счетчик, включаем uppercase и снова кликаем счетчик:


Разбираем логи.

Сначала мы кликаем счетчик:

ClickCounter(counter = 0, uppercase = false), EvenOdd(uppercase = false, hashcode = beddf50)
ClickCounter(counter = 1, uppercase = false), EvenOdd(uppercase = false, hashcode = beddf50)
ClickCounter(counter = 2, uppercase = false), EvenOdd(uppercase = false, hashcode = beddf50)
ClickCounter(counter = 3, uppercase = false), EvenOdd(uppercase = false, hashcode = beddf50)

Значение counter меняется. Значение uppercase пока что всегда false, т.к. CheckBox выключен. Функция remember каждый раз возвращает нам один и тот же EvenOdd.

 

Далее включаем CheckBox, который отвечает за uppercase. Это приводит к тому, что значение upperCase меняется на true, и функция ClickCounter перезапускается:

ClickCounter(counter = 3, uppercase = true), EvenOdd(uppercase = false, hashcode = beddf50)

Нам в ClickCounter приходит новое значение uppercase. И вот сейчас надо бы создать новый EvenOdd и передать ему это значение. Но по логу мы видим, что remember вернул нам все тот же объект EvenOdd, что и раньше, с uppercase = false.

 

Дальше кликаем счетчик:

ClickCounter(counter = 4, uppercase = true), EvenOdd(uppercase = false, hashcode = beddf50)
ClickCounter(counter = 5, uppercase = true), EvenOdd(uppercase = false, hashcode = beddf50)
ClickCounter(counter = 6, uppercase = true), EvenOdd(uppercase = false, hashcode = beddf50)

remember так и возвращает нам старый EvenOdd

 

Используем key в remember, чтобы решить эту проблему:

val evenOdd = remember(uppercase) { EvenOdd(uppercase) }

В качестве key указываем uppercase. Теперь remember будет пересоздавать EvenOdd, если значение uppercase изменится.

 

Запускаем приложение, кликаем счетчик, включаем uppercase и снова кликаем счетчик:


Сначала мы кликаем счетчик:

ClickCounter counter = 0, uppercase = false, EvenOdd(uppercase = false, hashcode = beddf50)
ClickCounter counter = 1, uppercase = false, EvenOdd(uppercase = false, hashcode = beddf50)
ClickCounter counter = 2, uppercase = false, EvenOdd(uppercase = false, hashcode = beddf50)
ClickCounter counter = 3, uppercase = false, EvenOdd(uppercase = false, hashcode = beddf50)

Пока uppercase не меняется, remember возвращает нам один и тот же объект EvenOdd.

 

Далее включаем CheckBox, который меняет значение uppercase на true

ClickCounter counter = 3, uppercase = true, EvenOdd(uppercase = true, hashcode = 40aff32)

remember определил, что значение uppercase поменялось и пересоздал нам EvenOdd. В этом новом объекте uppercase = true.

 

Дальше кликаем счетчик:

ClickCounter counter = 4, uppercase = true, EvenOdd(uppercase = true, hashcode = 40aff32)
ClickCounter counter = 5, uppercase = true, EvenOdd(uppercase = true, hashcode = 40aff32)
ClickCounter counter = 6, uppercase = true, EvenOdd(uppercase = true, hashcode = 40aff32)

uppercase не меняется, а значит remeber продолжает возвращать нам последний созданный EvenOdd

 

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

 

 

remember + mutableStateOf

Чаще всего в примерах, да и в реальном коде вы встретите комбинацию remember + mutableStateOf. Это пожалуй, одна из самых сложных вещей для понимания, когда начинаешь изучать Compose. Поэтому мы рассмотрели две этих функции по отдельности, а в следующем уроке разберем, зачем нужна эта комбинация.


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

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

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

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




Комментарии   

# RE: Урок 9. rememberИлья 01.01.2023 20:31
Хэш в тексте 8f1b1e8 не соответствует тому, тому, что на картинке.
# RE: Урок 9. rememberDmitry Vinogradov 18.01.2023 14:16
Спасибо, пофиксил!
# RE: Урок 9. rememberЛют 15.02.2023 16:44
по сути можно сказать что remember - полусинглтон? Или Синглтон по запросу? :lol:
# RE: Урок 9. rememberDmitry Vinogradov 20.02.2023 22:23
Ну да, что-то типа даггеровского синглтона, у которого scope - это Composable функция.
# Код примераVladimir 29.03.2023 13:30
Не буду приводить весь код, чтобы не усложнять пример. - Код примера был был не лишним, учитывая что там как раз всё по теме. Можно просто засунуть под кат. Кому надо тот бы посмотрел :-?
# RE: Код примераVladimir 29.03.2023 13:32
Ниже увидел, что это доп. задание. Ну ок)

Language

Автор сайта

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

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

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

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

 

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

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



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



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

Яндекс
410011180491924

WebMoney
R248743991365
Z551306702056

Paypal