Новый strong skipping mode для управления стабильностью классов в Jetpack Compose меняет рекомпозиции в вашем приложении. В этой статье мы расскажем о том, какие случаи он решает за вас, а какие необходимо контролировать вручную. Мы также ответим на часто возникающие вопросы, например, нужно ли по-прежнему помнить о лямбда-функциях, нужны ли неизменяемые коллекции Kotlinx или даже как стабилизировать все классы вашей доменной модели. Если вы не знаете, что такое стабильность, ознакомьтесь с нашей документацией, чтобы понять, что это такое.
Стабильность до strong skipping mode
Есть несколько причин, по которым компилятор Compose может считать класс нестабильным:
- Это мутабельный класс. Например, он содержит мутабельное свойство (не подкрепленное состоянием снапшота).
- Это класс, определенный в модуле Gradle, который не использует Compose (не имеет зависимости в компиляторе Compose).
- Это класс, содержащий нестабильное свойство (вложенность нестабильности).
Рассмотрим следующий класс:
data class Subscription( // class is unstable val id: Int, // stable val planName: String, // stable val renewalOn: LocalDate // unstable )
Свойства id
и name
стабильны, потому что они относятся к примитивному типу, который является неизменяемым. Однако свойство renewalOn
нестабильно, потому что java.time.LocalDate
берется из стандартной библиотеки Java, которая не имеет зависимости от компилятора Compose. Из-за этого весь класс Subscription
является нестабильным.
Рассмотрим следующий пример со свойством state
, использующим класс Subscription
, которое передается в SubscriptionComposable
:
// create in a state holder (for example, ViewModel) var state by mutableStateOf(Subscription( id = 1, planName = "30 days", renewalOn = LocalDate.now().plusDays(30) )) @Composable fun SubscriptionComposable(input: Subscription) { // always recomposed regardless if input changed or not }
Исторически сложилось так, что composable с входным параметром этого нестабильного класса не определялся как пропускаемый, и он всегда перекомпоновывался независимо от того, менялись входы или нет.
Стабильность с strong skipping mode
В компиляторе Jetpack Compose версии 1.5.4 и выше есть возможность включить сильного режима пропуска (strong skipping mode), который всегда генерирует логику пропуска независимо от стабильности входных параметров. Этот режим позволяет пропускать кмпозабл с нестабильными классами. Подробнее о режиме сильном режиме пропуска и о том, как его включить, вы можете прочитать в нашей документации или в статье Бена Тренгроува в блоге.
Сильный режим пропуска имеет два способа определения того, изменился ли входной параметр по сравнению с предыдущей композицией:
- Если класс стабилен, он использует структурное равенство (
.equals()
). - Если класс нестабилен, используется ссылочное равенство (
===
).
После включения strong skipping mode в проекте composable, использующие нестабильный класс Subscription
, не будут перекомпоновываться, если экземпляр такой же, как и в предыдущей композиции.
Допустим, вы используете SubscriptionComposable
в другом композабл Screen
, который принимает параметр inputText
. Если параметр inputText
изменится, а параметр subscription
— нет, SubscriptionComposable
не будет перекомпонован и будет пропущен:
@Composable fun Screen(inputText: String, subscription: Subscription) { Text(inputText) // It's skipped when subscription parameter didn't change SubscriptionComposable(subscription) }
Но, допустим, у вас есть функция renewSubscription
, которая обновляет переменную state
текущим днем, чтобы отслеживать последний день, когда произошло изменение:
fun renewSubscription() { state = state.copy(renewalOn = LocalDate.now().plusDays(30)) }
Функция copy
создает новый экземпляр класса с теми же структурными свойствами (если это происходит в тот же день), а это значит, что SubscriptionComposable
будет снова перекомпонован, потому что режим сильного пропуска сравнивает нестабильные классы с помощью ссылочного равенства (===
), а copy
создает новый экземпляр нашей подписки. Несмотря на то, что дата та же, из-за использования ссылочного равенства composable Subscription
все равно перекомпонуется.
Контроль стабильности с помощью аннотаций
Если вы хотите предотвратить перекомпоновку SubscriptionComposable
, когда структурные данные не меняются (equals()
возвращает тот же результат), вам нужно вручную пометить класс Subscription
как стабильный.
В данном случае это просто исправить, аннотировав класс с помощью @Immutable
, поскольку представленный здесь класс не может быть мутирован:
В этом примере при вызове renewSubscription
SubscriptionComposable
снова будет пропущена, потому что теперь она использует функцию equals()
вместо ===
, которая вернет true
по сравнению с предыдущим состоянием.
Когда это может произойти?
Реальный пример того, когда вам все же понадобится аннотировать свои классы как @Immutable
, — это использование сущностей, поступающих из периферийных устройств вашей системы, таких как сущности баз данных, сущности API, изменения Firestore или другие.
Поскольку эти сущности каждый раз поступают из базовых данных, они каждый раз создают новые экземпляры. Поэтому без аннотации они будут перекомпоновываться.
Примечание: Перекомпоновка может быть быстрее, чем вызов equals()
для каждого параметра. При оптимизации стабильности всегда следует измерять эффект от изменений.
Контроль стабильности с помощью файла конфигурации стабильности
Для классов, которые не являются частью вашей кодовой базы, мы раньше советовали стабилизировать их только обертыванием класса в класс, который является частью вашей кодовой базы, и аннотировать этот класс как @Immutable
.
Рассмотрим пример, в котором у вас есть компонент, непосредственно принимающий параметр java.time.LocalDate
:
@Composable fun LatestChangeOn(updated: LocalDate) { // present the day parameter on screen }
Если вы вызовете функцию renewSubscription
для обновления последнего изменения, то окажетесь в аналогичной ситуации — композит LatestChangeOn
продолжит перекомпоновываться, независимо от того, тот же это день или нет. Однако в такой ситуации нет возможности аннотировать этот класс, поскольку он является частью стандартной библиотеки.
Чтобы исправить ситуацию, можно включить файл конфигурации стабильности, который может содержать классы или шаблоны классов, которые компилятор Compose будет считать стабильными.
Чтобы включить его, добавьте stabilityConfigurationFile
в конфигурацию composeCompiler
:
composeCompiler { ... // Set path of the config file stabilityConfigurationFile = rootProject.file("stability_config.conf") }
И создайте файл stability_config.conf
в корневой папке вашего проекта, в который добавьте класс LocalDate
:
// add the immutable classes outside of your codebase java.time.LocalDate // alternatively you can stabilize all java.time classes with * java.time.*
Стабилизируйте классы доменной модели
В дополнение к классам, которые не являются частью вашей кодовой базы, файл конфигурации стабильности может быть полезен для стабилизации всех ваших классов данных или доменной модели (при условии, что они неизменяемы). Таким образом, модуль домена может быть модулем Java Gradle и не нуждается в зависимости от компилятора Compose.
// stabilize all classes in model package com.example.app.domain.model.*
Помните о нарушении правил
Помните, что аннотирование мутабельного класса аннотацией @Immutable
или добавление класса в конфигурационный файл стабильности может стать источником ошибок в вашей кодовой базе, поскольку компилятор Compose не способен проверить контракт, и это может проявиться в том, что что-то не перекомпонуется, когда вы думаете, что это должно произойти.
Отказ от remember() в лямбдах
Еще одно преимущество сильного пропуска заключается в том, что он «запоминает» все лямбды, используемые в композиции, даже те, которые имеют нестабильные капчи. Раньше лямбды, использующие нестабильный класс, например ViewModel
, могли стать причиной перекомпозиции. Одним из распространенных обходных путей было запоминание лямбда-функций.
Поэтому, если в вашей кодовой базе есть лямбды, обернутые с помощью remember
, вы можете смело удалять вызов remember
, поскольку это делается автоматически компилятором Compose:
Нужны ли еще immutable коллекции?
Коллекции kotlinx.collections.immutable
, такие как ImmutableList
, могли использоваться в прошлом для того, чтобы сделать список элементов стабильным и тем самым предотвратить перекомпоновку composable. Если вы используете их в своей кодовой базе исключительно для предотвращения перекомпоновки композитов с параметрами List
, вы можете рассмотреть возможность рефакторинга их в обычный List
и добавить java.util.List
в файл конфигурации стабильности.
Но!
Если вы это сделаете, ваш компонент может работать медленнее, чем если бы параметр List
был нестабильным!
Добавление List
в файл конфигурации стабильности означает, что параметр List
сравнивается вызовом equals
, что в конечном итоге приводит к вызову equals
для каждого элемента этого списка. В контексте ленивого списка та же самая проверка equals
затем вызывается снова с точки зрения composable элемента, что приводит к вызову equals()
дважды для многих видимых элементов и, возможно, без необходимости для всех элементов, которые не видны!
Если composable элемент, содержащий параметр List
, не содержит много других компонентов пользовательского интерфейса, его перекомпоновка может быть быстрее, чем вычисление проверки equals()
.
Однако здесь нет универсального подхода, поэтому вам следует проверить свой выбор с помощью бенчмарков!
Резюме
Включив сильный режим пропуска в своей кодовой базе, вы можете уменьшить необходимость вручную дорабатывать классы, чтобы они были стабильными. Имейте в виду, что в некоторых случаях они все еще нуждаются в ручной обработке, но теперь это можно упростить с помощью файла конфигурации стабильности!
Мы надеемся, что все эти изменения упростят умственную нагрузку при размышлении о стабильности в Compose.
Хотите больше? Посмотрите наш кодлаб о практическом решении проблем производительности в Compose.