Site icon AppTractor

Strong Skipping Mode — новый способ оптимизации стабильности в Jetpack Compose

Новый strong skipping mode для управления стабильностью классов в Jetpack Compose меняет рекомпозиции в вашем приложении. В этой статье мы расскажем о том, какие случаи он решает за вас, а какие необходимо контролировать вручную. Мы также ответим на часто возникающие вопросы, например, нужно ли по-прежнему помнить о лямбда-функциях, нужны ли неизменяемые коллекции Kotlinx или даже как стабилизировать все классы вашей доменной модели. Если вы не знаете, что такое стабильность, ознакомьтесь с нашей документацией, чтобы понять, что это такое.

Стабильность до strong skipping mode

Есть несколько причин, по которым компилятор 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), который всегда генерирует логику пропуска независимо от стабильности входных параметров. Этот режим позволяет пропускать кмпозабл с нестабильными классами. Подробнее о режиме сильном режиме пропуска и о том, как его включить, вы можете прочитать в нашей документации или в статье Бена Тренгроува в блоге.

Сильный режим пропуска имеет два способа определения того, изменился ли входной параметр по сравнению с предыдущей композицией:

После включения 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.

Источник

Exit mobile version