Если вы столкнулись с нестабильным классом, который вызывает проблемы с производительностью, сделайте его стабильным. В этом документе описаны некоторые способы, которые можно использовать для этого.
Включить строгий пропуск
Сначала попробуйте включить режим строгого пропуска. Режим строгого пропуска позволяет пропускать composable-функции с нестабильными параметрами и является самым простым способом устранения проблем с производительностью, вызванных стабильностью.
Подробнее о строгом пропуске…
Сделайте класс неизменяемым
Также можно попробовать сделать нестабильный класс полностью неизменяемым.
- Неизменяемый. Указывает на тип, значение любого свойства которого нельзя изменить после создания экземпляра этого типа, а все методы являются референциально прозрачными.
- Убедитесь, что все свойства класса имеют тип
val, а неvar, и являются неизменяемыми. - Примитивные типы, такие как
String, IntиFloat, всегда неизменяемы. - Если это невозможно, то для любых изменяемых свойств необходимо использовать состояние Compose.
- Убедитесь, что все свойства класса имеют тип
- Стабильный – тип, который можно изменять. Среда выполнения Compose не знает, когда и почему публичные свойства или методы типа могут давать результаты, отличные от предыдущих вызовов.
Неизменяемые коллекции
Распространенной причиной, по которой Compose считает класс нестабильным, являются коллекции. Как отмечалось на странице Диагностика проблем со стабильностью, компилятор Compose не может быть полностью уверен, что такие коллекции, как List, Map и Set, действительно неизменяемы, и поэтому помечает их как нестабильные.
Чтобы решить эту проблему, можно использовать неизменяемые коллекции. Компилятор Compose поддерживает неизменяемые коллекции Kotlinx. Эти коллекции разработаны как неизменяемые, и компилятор Compose обрабатывает их как таковые. Эта библиотека все ещё находится на стадии альфа-тестирования, поэтому в ее API могут быть внесены изменения.
Рассмотрим нестабильный класс из руководства Диагностика проблем со стабильностью:
unstable class Snack {
…
unstable val tags: Set<String>
…
}
Вы можете сделать tags стабильным, используя неизменяемую коллекцию. На курсе измените тип tags на ImmutableSet<String>:
data class Snack{
…
val tags: ImmutableSet<String> = persistentSetOf()
…
}
После этого все параметры класса становятся неизменяемыми, и компилятор Compose помечает класс как стабильный.
Добавьте аннотацию с помощью Stable или Immutable
Возможный способ решения проблем со стабильностью – аннотировать нестабильные классы с помощью @Stable или @Immutable.
Аннотирование класса переопределяет то, что компилятор мог бы вывести о вашем классе. Это похоже на
!! оператор в Kotlin. Вы должны быть очень осторожны с тем, как вы используете эти аннотации. Переопределение поведения компилятора может привести к непредвиденным ошибкам, например к тому, что composable-функция не будет перекомпонована, когда вы этого ожидаете.
Если класс можно сделать стабильным без аннотации, лучше сделать это.
В следующем фрагменте кода показан пример класса данных, помеченного как неизменяемый:
@Immutable
data class Snack(
…
)
Независимо от того, какую аннотацию вы используете (@Immutable или @Stable), компилятор Compose помечает класс Snack как стабильный.
Классы с аннотациями в коллекциях
Рассмотрим composable-функцию, которая включает параметр типа List<Snack>:
restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
…
unstable snacks: List<Snack>
…
)
Даже если вы добавите к Snack аннотацию @Immutable, компилятор Compose все равно пометит параметр snacks в HighlightedSnacks как нестабильный.
С параметрами возникает та же проблема, что и с классами, когда речь идет о типах коллекций. Компилятор Compose всегда помечает параметр типа List как нестабильный, даже если это коллекция стабильных типов.
Вы не можете пометить отдельный параметр как стабильный или добавить аннотацию к composable-функции, чтобы она всегда была пропускаемой. Есть несколько вариантов дальнейших действий.
Есть несколько способов решить проблему нестабильных коллекций. В следующих подразделах описаны эти подходы.
Файл конфигурации
Если вы готовы соблюдать контракт стабильности в своей базе кода, то можете включить рассмотрение коллекций Kotlin как стабильных, добавив kotlin.collections.* в файл конфигурации стабильности.
Неизменяемая коллекция
Чтобы обеспечить неизменяемость во время компиляции, вместо List можно использовать неизменяемую коллекцию kotlinx.
@Composable
private fun HighlightedSnacks(
…
snacks: ImmutableList<Snack>,
…
)
Wrapper
Если вы не можете использовать неизменяемую коллекцию, создайте свою. Для этого оберните List в аннотированный стабильный класс. В зависимости от ваших требований, лучшим выбором для этого, скорее всего, будет универсальный контейнер.
@Immutable
data class SnackCollection(
val snacks: List<Snack>
)
Затем вы можете использовать его в качестве типа параметра в composable-функции.
@Composable
private fun HighlightedSnacks(
index: Int,
snacks: SnackCollection,
onSnackClick: (Long) -> Unit,
modifier: Modifier = Modifier
)
Решение
После применения любого из этих подходов компилятор Compose помечает composable-функцию HighlightedSnacks как skippable и restartable.
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
stable index: Int
stable snacks: ImmutableList<Snack>
stable onSnackClick: Function1<Long, Unit>
stable modifier: Modifier? = @static Companion
)
Теперь при рекомпозиции Compose может пропускать HighlightedSnacks, если входные данные не изменились.
Файл конфигурации стабилизации
Начиная с версии 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 в модулях уровня данных или добавьте к классам аннотации
@Stableили@Immutable.- Для этого нужно добавить зависимость Compose на уровень данных. Однако это просто зависимость для среды выполнения Compose, а не для
Compose-UI.
- Для этого нужно добавить зависимость Compose на уровень данных. Однако это просто зависимость для среды выполнения Compose, а не для
- В модуле интерфейса оберните классы уровня данных в классы-обертки, относящиеся к интерфейсу.
Та же проблема возникает при использовании внешних библиотек, если они не используют компилятор Compose.
Не все composable-функции должны быть с возможностью пропуска
При устранении проблем со стабильностью не пытайтесь сделать пропускаемыми все композиции. Попытка сделать это может привести к преждевременной оптимизации, которая создаст больше проблем, чем решит.
Во многих ситуациях возможность пропуска не дает никаких реальных преимуществ и может привести к сложному в поддержке коду. Пример:
- Composable-функция, которая редко или никогда не рекомпонуется.
- Компонуемый элемент, который сам по себе вызывает только пропускаемые компонуемые элементы.
- Composable-функция с большим количеством параметров и дорогостоящей реализацией функции equals. В этом случае стоимость проверки того, изменился ли какой-либо параметр, может превысить стоимость дешевой рекомпозиции.
Когда composable-функция пропускается, это добавляет небольшие накладные расходы, которые могут не стоить того. Вы даже можете добавить к composable-функции аннотацию non-restartable, если считаете, что возможность перезапуска не стоит затраченных ресурсов.