Если вы столкнулись с нестабильным классом , вызывающим проблемы с производительностью, вам следует сделать его стабильным. В этом документе описываются несколько методов, которые вы можете использовать для этого.
Включить сильный пропуск
Сначала вам следует попробовать включить режим сильного пропуска. Режим строгого пропуска позволяет пропускать составные элементы с нестабильными параметрами и является самым простым методом устранения проблем с производительностью, вызванных стабильностью.
Дополнительную информацию см. в разделе «Сильный пропуск» .
Сделать класс неизменяемым
Вы также можете попытаться сделать нестабильный класс полностью неизменяемым.
- Неизменяемый : указывает тип, в котором значение каких-либо свойств никогда не может измениться после создания экземпляра этого типа, и все методы являются ссылочно прозрачными.
- Убедитесь, что все свойства класса имеют значение
val
, а неvar
, и имеют неизменяемые типы. - Примитивные типы, такие как
String, Int
иFloat
, всегда неизменяемы. - Если это невозможно, вы должны использовать состояние Compose для любых изменяемых свойств.
- Убедитесь, что все свойства класса имеют значение
- Стабильный : указывает тип, который является изменяемым. Среда выполнения Compose не узнает, приведет ли какое-либо из общедоступных свойств или поведения метода к результатам, отличным от предыдущего вызова.
Неизменяемые коллекции
Распространенной причиной, по которой Compose считает класс нестабильным, являются коллекции. Как отмечено на странице Диагностика проблем стабильности , компилятор Compose не может быть полностью уверен, что такие коллекции, как List, Map
и Set
, действительно неизменяемы, и поэтому помечает их как нестабильные.
Чтобы решить эту проблему, вы можете использовать неизменяемые коллекции. Компилятор Compose включает поддержку Kotlinx Immutable Collections . Эти коллекции гарантированно будут неизменяемыми, и компилятор 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
.
Аннотирование класса переопределяет то, что в противном случае компилятор мог бы сделать вывод о вашем классе. Это похоже на !!
оператор в Котлине . Вы должны быть очень осторожны с использованием этих аннотаций. Переопределение поведения компилятора может привести к непредвиденным ошибкам, например, к тому, что компоновка не будет перекомпонована, когда вы этого ожидаете.
Если возможно сделать ваш класс стабильным без аннотации, вам следует стремиться добиться стабильности таким образом.
Следующий фрагмент представляет собой минимальный пример класса данных, помеченного как неизменяемый:
@Immutable
data class Snack(
…
)
Независимо от того, используете ли вы аннотацию @Immutable
или @Stable
, компилятор Compose помечает класс Snack
как стабильный.
Аннотированные классы в коллекциях
Рассмотрим компонуемый объект, включающий параметр типа List<Snack>
:
restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
…
unstable snacks: List<Snack>
…
)
Даже если вы аннотируете Snack
с помощью @Immutable
, компилятор Compose по-прежнему помечает параметр snacks
в HighlightedSnacks
как нестабильный.
Когда дело касается типов коллекций, параметры сталкиваются с той же проблемой, что и классы : компилятор Compose всегда помечает параметр типа List
как нестабильный , даже если он представляет собой коллекцию стабильных типов.
Вы не можете пометить отдельный параметр как стабильный или аннотировать составной элемент, чтобы его всегда можно было пропустить. Есть несколько путей вперед.
Есть несколько способов обойти проблему нестабильных коллекций. В следующих подразделах описаны эти различные подходы.
Конфигурационный файл
Если вы согласны соблюдать контракт стабильности в своей кодовой базе, вы можете согласиться считать коллекции Kotlin стабильными, добавив kotlin.collections.*
в свой файл конфигурации стабильности .
Неизменяемая коллекция
Для обеспечения безопасности неизменяемости во время компиляции вы можете использовать неизменяемую коллекцию Kotlinx вместо List
.
@Composable
private fun HighlightedSnacks(
…
snacks: ImmutableList<Snack>,
…
)
обертка
Если вы не можете использовать неизменяемую коллекцию, вы можете создать свою собственную. Для этого оберните List
в аннотированный стабильный класс. Общая оболочка, вероятно, будет лучшим выбором для этого, в зависимости от ваших требований.
@Immutable
data class SnackCollection(
val snacks: List<Snack>
)
Затем вы можете использовать это как тип параметра в вашем компонуемом объекте.
@Composable
private fun HighlightedSnacks(
index: Int,
snacks: SnackCollection,
onSnackClick: (Long) -> Unit,
modifier: Modifier = Modifier
)
Решение
После применения любого из этих подходов компилятор Compose теперь помечает HighlightedSnacks
Composable как 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")
}
```
As the Compose compiler runs on each module in your project separately, you can
provide different configurations to different modules if needed. Alternatively,
have one configuration at the root level of your project and pass that path to
each module.
## Multiple modules {:#multiple-modules}
Another common issue involves multi-module architecture. The Compose compiler
can only infer whether a class is stable if all of the non-primitive types that
it references are either explicitly marked as stable or in a module that was
also built with the Compose compiler.
If your data layer is in a separate module to your UI layer, which is the
recommended approach, this may be an issue you encounter.
### Solution {:#modules-solution}
To solve this issue you can take one of the following approaches:
1. Add the classes to your [Compiler configuration file](#configuration-file).
1. Enable the Compose compiler on your data layer modules, or tag your classes
with `@Stable` or `@Immutable` where appropriate.
- This involves adding a Compose dependency to your data layer. However,
it is just the dependency for the Compose runtime and not for
`Compose-UI`.
1. Within your UI module, wrap your data layer classes in UI-specific wrapper
classes.
The same issue also occurs when using external libraries if they don't use the
Compose compiler.
## Not every composable should be skippable {:#not-composable}
When working to fix issues with stability, you shouldn't attempt to make every
composable skippable. Attempting to do so can lead to premature optimisation
that introduces more issues than it fixes.
There are many situations where being skippable doesn't have any real benefit
and can lead to hard to maintain code. For example:
- A composable that is not recomposed often, or at all.
- A composable that in itself just calls skippable composables.
- A composable with a large number of parameters with expensive equals
implementations. In this case, the cost of checking if any parameter has
changed could outweigh the cost of a cheap recomposition.
When a composable is skippable it adds a small overhead which may not be worth
it. You can even annotate your composable to be [non-restartable][9] in cases
where you determine that being restartable is more overhead than it's worth.