Если нестабильный класс вызывает проблемы с производительностью, оптимизируйте то, как Compose сравнивает его или создает его экземпляры. В этой статье описано несколько способов, которые помогут вам это сделать.
Включить строгий пропуск
Режим строгого пропуска включен по умолчанию, начиная с Kotlin 2.0.20.
Режим строгого пропуска позволяет пропускать composable-функции с нестабильными параметрами, сравнивая нестабильные параметры с помощью равенства экземпляров (===), а стабильные параметры по-прежнему сравниваются с помощью структурного равенства (Object.equals()).
Если вы используете Kotlin версии ниже 2.0.20, сначала включите режим строгого пропуска. Если режим строгого пропуска уже включен, а composable-функция все равно рекомпонуется без необходимости, проверьте, не выделяются ли новые экземпляры нестабильных параметров при каждом проходе рекомпозиции.
Подробнее о строгом пропуске…
Сделайте класс неизменяемым.
Вы также можете сделать нестабильный класс полностью неизменяемым, чтобы компилятор мог автоматически определить его стабильность.
- Неизменяемый. Указывает на тип, в котором значение любого свойства не может быть изменено после создания экземпляра этого типа, а все методы являются ссылочно прозрачными.
- Убедитесь, что все свойства класса являются
val, а неvar, и имеют неизменяемые типы. - Примитивные типы, такие как
String,IntиFloat, всегда неизменяемы. - Простые типы
data classс примитивными свойствамиvalилиStringв модулях, созданных с помощью компилятора Compose, уже считаются стабильными и автоматически сравниваются с.equals(). Вам не нужно помечать их аннотацией@Immutable. - Если неизменяемость невозможна, вы должны использовать состояние Compose для любых изменяемых свойств.
- Убедитесь, что все свойства класса являются
- Стабильный. Указывает на изменяемый тип. Среда выполнения Compose не узнает, если и когда какие-либо общедоступные свойства типа или поведение метода дадут результаты, отличные от предыдущего вызова.
Подборки и стабильность
Распространенной причиной, по которой Compose считает класс нестабильным, являются коллекции. Как отмечается в статье Диагностика проблем со стабильностью, компилятор Compose не может быть полностью уверен, что интерфейсы коллекций, такие как List, Map и Set, действительно неизменяемы, и поэтому помечает их как нестабильные.
Рассмотрим класс из руководства Диагностика проблем со стабильностью:
unstable class Snack {
…
unstable val tags: Set<String>
…
}
Если включен режим строгого пропуска (по умолчанию в Kotlin 2.0.20 и более поздних версиях), нестабильные коллекции и классы, содержащие коллекции, уже пропускают рекомпозицию, используя равенство экземпляров (===) всякий раз, когда передается одна и та же ссылка.
Будьте осторожны, заменяя kotlin.collections.List, Set или Map на неизменяемые коллекции Kotlinx (ImmutableList, ImmutableSet, ImmutableMap) или добавляя правила @Immutable, @Stable или stability_config.conf в модели, содержащие коллекции. В режиме строгого пропуска стандартные List пропускаются с использованием равенства ссылок (===), тогда как ImmutableList и аннотированные модели коллекций выполняют поэлементные сравнения .equals() при каждом проходе рекомпозиции.
Для больших коллекций или ленивых макетов (LazyColumn, LazyRow, LazyGrid) проверка .equals() для каждого элемента может занять больше времени, чем рекомпозиция. Эта стоимость удваивается, если вы передаете свойство ImmutableList из data class в дочернюю composable-функцию:
data class SnackCategory(
val name: String,
val snacks: ImmutableList<Snack>
)
@Composable
fun CategorySection(category: SnackCategory) {
Text(category.name)
// Compose compares category.snacks twice: once in SnackCategory.equals()
// for CategorySection, and again for HighlightedSnacks.
HighlightedSnacks(snacks = category.snacks)
}
Вместо того чтобы принудительно применять структурное равенство к коллекциям, сохраняйте ссылку на экземпляр коллекции между эмиссиями (например, кэшируя преобразования в ViewModel или с помощью remember), чтобы Compose мог пропускать использование равенства экземпляров (===). Рассмотрите возможность использования ImmutableList или аннотаций стабильности для моделей, содержащих коллекции, только в том случае, если вы не можете сохранить ссылку на экземпляр коллекции и тесты подтверждают, что проверки структурного равенства выполняются быстрее, чем рекомпозиция.
Если вы используете Kotlin версии ниже 2.0.20, сначала включите режим строгого пропуска.
Добавьте аннотацию с помощью Stable или Immutable
Возможный способ решения проблем со стабильностью для небольших классов с недорогими проверками .equals() – аннотировать нестабильные классы с помощью @Stable или @Immutable.
Аннотирование класса переопределяет то, что компилятор в противном случае вывел бы о вашем классе. Это похоже на оператор !! в Kotlin. Будьте очень осторожны при использовании этих аннотаций. Переопределение поведения компилятора может привести к непредвиденным ошибкам, например, к тому, что ваша composable-функция не будет перекомпонована, когда вы этого ожидаете.
Если включен режим строгого пропуска, следуйте этим рекомендациям, чтобы решить, добавлять ли @Stable или @Immutable:
- Не аннотируйте классы, которые уже считаются стабильными. Классы со свойствами
valстабильных типов в модулях, созданных с помощью компилятора Compose, уже считаются стабильными. - Добавляйте аннотации только к классам с простыми проверками
.equals(), когда экземпляры создаются заново. Добавляйте@Stableили@Immutable, когда компилятор не может определить стабильность (например, для интерфейсов или классов из модулей, не относящихся к Compose), и источник данных выделяет новые экземпляры объектов для неизмененных элементов при каждой эмиссии, если класс небольшой и имеет простую проверку.equals(). - Будьте осторожны с моделями, содержащими коллекции. Не добавляйте
@Stableили@Immutableв модели данных, содержащие большие коллекции (List,Set,Map), особенно в модели, используемые в ленивых макетах (LazyColumn,LazyRow,LazyGrid,Pager). Если оставить эти модели без аннотаций, Compose сможет сравнивать их, используя равенство экземпляров (===), вместо того чтобы выполнять структурные проверки.equals()для каждого элемента.
В следующем фрагменте кода показан минимальный пример плоского класса данных (без свойств коллекции), аннотированного как неизменяемый при определении в модуле, где компилятор не может определить стабильность:
@Immutable
data class SnackSummary(
val id: Long,
val name: String,
val price: Long
)
Независимо от того, используете ли вы аннотацию @Immutable или @Stable, компилятор Compose помечает класс SnackSummary как стабильный и сравнивает его с помощью Object.equals().
Классы с аннотациями в коллекциях
Рассмотрим composable-функцию, которая включает параметр типа List<Snack>:
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
stable index: Int
unstable snacks: List<Snack>
stable onSnackClick: Function1<Long, Unit>
stable modifier: Modifier? = @static Companion
)
Даже если Snack стабилен или аннотирован с помощью @Immutable, компилятор Compose по-прежнему помечает параметр snacks в HighlightedSnacks как unstable, поскольку List является интерфейсом.
Если включен режим строгого пропуска, HighlightedSnacks по-прежнему помечается как skippable и пропускает рекомпозицию, используя равенство экземпляров (===), когда передается тот же экземпляр List. Вы можете сделать параметр snacks stable, обернув List<Snack> в класс-оболочку @Immutable, заменив List<Snack> на ImmutableList<Snack> или добавив kotlin.collections.* в файл конфигурации стабильности.
Однако в этом случае Compose будет выполнять поэлементное .equals()
сравнение при каждом проходе рекомпозиции. Используйте эти подходы только в том случае, если вы не можете сохранить ссылку на экземпляр списка и тесты показывают преимущество.
Файл конфигурации стабилизации
Начиная с версии Compose Compiler 1.5.5, во время компиляции можно предоставить файл конфигурации классов, которые будут считаться стабильными. Это позволяет считать стабильными классы, которые вы не контролируете, например классы стандартной библиотеки, такие как LocalDateTime.
Файл конфигурации – это обычный текстовый файл, в котором каждый класс указан в отдельной строке. Поддерживаются комментарии, а также одиночные и двойные подстановочные знаки.
Пример конфигурации:
// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider my datalayer stable
com.datalayer.*
// Consider my datalayer and all submodules stable
com.datalayer.**
// Consider my generic type stable based off it's first type parameter only
com.example.GenericClass<*,_>
Чтобы включить эту функцию, передайте путь к файлу конфигурации в блок параметров composeCompiler конфигурации плагина Gradle для компилятора Compose.
composeCompiler {
stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}
Поскольку компилятор Compose запускается для каждого модуля в проекте отдельно, вы можете при необходимости задать разные конфигурации для разных модулей. Вы также можете создать одну конфигурацию на корневом уровне проекта и передать этот путь каждому модулю.
Несколько модулей
Ещё одна распространенная проблема связана с многомодульной архитектурой. Компилятор Compose может определить, является ли класс стабильным, только если все непримитивные типы, на которые он ссылается, либо явно помечены как стабильные, либо находятся в модуле, который также был создан с помощью компилятора Compose.
Если слой данных находится в отдельном модуле от слоя пользовательского интерфейса (рекомендуемый подход), вы можете столкнуться с этой проблемой. В режиме строгого пропуска классы из модулей, не относящихся к Compose, считаются нестабильными и по-прежнему пропускают рекомпозицию, используя равенство экземпляров (===), когда передается один и тот же экземпляр. Однако если ваш слой данных часто выделяет новые экземпляры небольших плоских моделей с идентичными значениями при каждом испускании, вы можете включить структурное равенство (Object.equals()).
Решение
Чтобы решить эту проблему, вы можете воспользоваться одним из следующих способов:
- Добавьте небольшие плоские классы с дешевыми проверками
.equals()в файл конфигурации компилятора. - Включите компилятор Compose в модулях уровня данных или добавьте к классам аннотации
@Stableили@Immutable.- Для этого нужно добавить зависимость Compose на уровень данных. Однако это просто зависимость для среды выполнения Compose, а не для
Compose-UI.
- Для этого нужно добавить зависимость Compose на уровень данных. Однако это просто зависимость для среды выполнения Compose, а не для
- В модуле пользовательского интерфейса оберните классы слоя данных в классы-оболочки, специфичные для пользовательского интерфейса.
Та же проблема возникает при использовании внешних библиотек, если они не используют компилятор Compose.
Не все composable-функции должны быть пропускаемыми
При устранении проблем со стабильностью не пытайтесь сделать каждую composable-функцию пропускаемой или принудительно обеспечить структурное равенство для каждого параметра. Пропуск composable-функции не бесплатен: каждый параметр пропускаемой composable-функции сохраняется средой выполнения Compose, как если бы он был запомнен, и Compose должен сравнивать каждый параметр с его предыдущим значением каждый раз, когда функция перекомпонуется, чтобы определить, следует ли ее пропустить. Попытка принудительно пропустить везде может привести к преждевременной оптимизации, которая приведет к большим накладным расходам, чем сэкономит.
Есть много ситуаций, когда принудительное обеспечение структурной стабильности не приносит реальной пользы и может снизить производительность или удобство обслуживания. Пример:
- Composable-функция, которая редко или никогда не рекомпонуется.
- Composable-функция, которая сама по себе вызывает только пропускаемые composable-функции.
- composable-функция с большим количеством параметров или моделей коллекций с ресурсоемкими реализациями
.equals(). В этом случае стоимость проверки того, изменился ли какой-либо параметр, может превысить стоимость дешевой рекомпозиции.
Не полагайтесь только на количество рекомпозиций. Сосредоточьтесь на том, чтобы сделать составные элементы легкими для выполнения, отложите быстро меняющиеся операции чтения состояния до этапов макета или рисования и проверьте изменения с помощью тестов. Когда composable-функция пропускаемая, это добавляет небольшие накладные расходы, которые могут того не стоить. Вы даже можете пометить composable-функцию как неперезапускаемую или непропускаемую, если считаете, что перезапуск или пропуск не стоят дополнительных затрат ресурсов.