О зацикливании рекомпозиции в Jetpack Compose

от автора

Фотограф: Laura Cleffmann: https://www.pexels.com/ru-ru/photo/20001993/

Фотограф: Laura Cleffmann: https://www.pexels.com/ru-ru/photo/20001993/

Jetpack Compose — это мощный инструмент, который упрощает создание UI в Android, но его освоение может быть не таким уж простым. Многие разработчики сталкиваются с неожиданными результатами и ошибками, которые на первый взгляд кажутся неочевидными. Сегодня разберем один из таких примеров и посмотрим, как зациклить рекомпозицию в Compose — и самое главное, как этого избежать.

Пример кода

Допустим, у нас есть следующий код:

data class MyDataClass(       val i: Int = 0,       val block: () -> Unit = {},   )  class MyScreenViewModel : ViewModel() {     private val dataSource = MutableSharedFlow<Int>(1)      val stateValue: StateFlow<MyDataClass>         get() = dataSource             .map { number -> MyDataClass(number, { println("Hello, World!") }) }             .stateIn(viewModelScope, SharingStarted.Eagerly, MyDataClass()) }  @Composable   fun MyScreen(viewModel: MyScreenViewModel) {       Log.d("[TAG]", "Recomposition!")      val state by viewModel.stateValue.collectAsStateWithLifecycle()     val checked = remember { mutableStateOf(false) }        Column {           Checkbox(             checked = checked.value,             onCheckedChange = { isChecked -> checked.value = isChecked }         )         Text("state: ${state.i}")     } }

На первый взгляд, код выглядит нормально. Однако если запустить его и нажать на Checkbox то посмотрев в LogCat и Layout Inspector вы увидите, что выполняется бесконечная рекомпозиция.

Бесконечная рекомпозиция. Интересно что счетчик рекомпозиций в Layout Inspector остановился на числе 80, хотя логи продолжают исправно печататься.

Бесконечная рекомпозиция. Интересно что счетчик рекомпозиций в Layout Inspector остановился на числе 80, хотя логи продолжают исправно печататься.

Почему так происходит и как это исправить? Давайте разберемся.

Причина проблемы

Давайте подробнее разберем код collectAsStateWithLifecycle() и поймем, как именно это происходит:

@Composable   fun <T> Flow<T>.collectAsStateWithLifecycle(     initialValue: T,     lifecycle: Lifecycle,     minActiveState: Lifecycle.State = Lifecycle.State.STARTED,     context: CoroutineContext = EmptyCoroutineContext ): State<T> {     return produceState(initialValue, this, lifecycle, minActiveState, context) {         lifecycle.repeatOnLifecycle(minActiveState) {             if (context == EmptyCoroutineContext) {                 this@collectAsStateWithLifecycle.collect { this@produceState.value = it }             } else withContext(context) {                 this@collectAsStateWithLifecycle.collect { this@produceState.value = it }             }         }     } }  @Composable   fun <T> produceState(       initialValue: T,       vararg keys: Any?,       producer: suspend ProduceStateScope<T>.() -> Unit   ): State<T> {       val result = remember { mutableStateOf(initialValue) }       @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")       LaunchedEffect(keys = keys) {           ProduceStateScopeImpl(result, coroutineContext).producer()       }       return result   }

Функция collectAsStateWithLifecycle() создает Compose-стейт, а затем подписывается на Flow через LaunchedEffect. Это позволяет получать новые данные из потока.

Теперь нам становятся более очевидны проблемы нашего кода, а именно:

  1. Каждый раз, когда мы обращаемся к stateValue, выполняется блок get(), создающий новый экземпляр StateFlow. Это приводит к новому запуску LaunchedEffect внутри функции collectAsStateWithLifecycle() и в результате создается еще один подписчик нашего стейта.

  2. При каждом вызове stateIn происходит новая подписка на dataSource и не смотря на то что dataSource никак не меняется, новый подписчик заново выполняет всю цепочку и создает объект MyDataClass, который, хотя и является data-классом, содержит лямбду block, что не позволяет корректно сравнить объекты MyDataClass.

Почему лямбды не равны друг другу

Простое выражение { println("Hello, World!") } != { println("Hello, World!") } может показаться странным, но оно иллюстрирует ключевую проблему. Лямбды создают экземпляры интерфейса FunctionX (где X — количество аргументов). Это значит, что два объекта FunctionX будут сравниваться по ссылкам.

// Пример одной из FunctionX: Function3 c 3мя входными параметрами  public interface Function3<in P1, in P2, in P3, out R> : kotlin.Function<R> {       public abstract operator fun invoke(p1: P1, p2: P2, p3: P3): R   }

Решение проблемы

Для решения этой проблемы можно использовать два подхода:

  1. Изменить методы equals и hashCode в MyDataClass, чтобы исключить переменную block из расчетов. Тогда объекты MyDataClass(1, {}) будут считаться равными, и Compose не будет триггерить рекомпозицию.

  2. Удалить функцию get() в stateValue, чтобы не создавать новый объект StateFlow каждый раз, а использовать один экземпляр.

Оптимально будет объединить оба подхода, и тогда мы получим следующий код:

data class MyDataClass(       val i: Int = 1,       val block: () -> Unit = {},   ) {       override fun equals(other: Any?): Boolean {           if (this === other) return true           if (javaClass != other?.javaClass) return false            other as MyDataClass           return i == other.i       }        override fun hashCode(): Int {           return i       }   }  class MyScreenViewModel : ViewModel() {     private val dataSource = MutableSharedFlow<Int>(1)      val stateValue: StateFlow<MyDataClass> = dataSource         .map { number -> MyDataClass(number, { println("Hello, World!") }) }         .stateIn(viewModelScope, SharingStarted.Eagerly, MyDataClass()) }

Теперь рекомпозиция перестанет зацикливаться, так как Compose сможет корректно сравнивать объекты MyDataClass и не будет создавать новых подписчиков StateFlow.


Спасибо, что прочитали статью! Если она была полезной, не забудьте поставить лайк и поделиться своими мыслями в комментариях. Ваш фидбек поможет мне делать материалы еще лучше.


ссылка на оригинал статьи https://habr.com/ru/articles/859084/