Исправить проблемы со стабильностью

Если вы столкнулись с нестабильным классом , вызывающим проблемы с производительностью, вам следует сделать его стабильным. В этом документе описываются несколько методов, которые вы можете использовать для этого.

Включить сильный пропуск

Сначала вам следует попробовать включить режим сильного пропуска. Режим строгого пропуска позволяет пропускать составные элементы с нестабильными параметрами и является самым простым методом устранения проблем с производительностью, вызванных стабильностью.

Дополнительную информацию см. в разделе «Сильный пропуск» .

Сделать класс неизменяемым

Вы также можете попытаться сделать нестабильный класс полностью неизменяемым.

  • Неизменяемый : указывает тип, в котором значение каких-либо свойств никогда не может измениться после создания экземпляра этого типа, и все методы являются ссылочно прозрачными.
    • Убедитесь, что все свойства класса имеют значение 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.