Как работает State и MutableState под капотом? Что такое Snapshot Mutation Policy?

В Compose State<T> — это наблюдаемый контейнер значения. Обратите внимание на слова «наблюдаемый«, «контейнер«, «значения«) Они очень важны. Есть разные реализации (SnapshotMutableStateImpl, DerivedState, и т.п.), но на уровне API идея простая:

  • MutableState<T> можно ещё и писать => при изменении значение инвалидирует соответствующие участки композиции и вызывает рекомпозицию
  • State<T> можно читать => и Compose будет знать, что composable зависит от этого стейта

Snapshot system: как это работает под капотом

Snapshot — основа всей реактивности Compose

Compose использует систему снапшотов (Snapshot system). Это своего рода STM (software transactional memory), которая отслеживает кто и что прочитал, кто и что изменил и сводит это в актуальное состояние + уведомления.

При чтении state.value в композиции — текущий Snapshot запоминает, что этот composable зависит от этого state.

При записи в state.value:

  • меняется значение в текущем snapshot’е
  • помечает этот state как изменённый
  • при применении снапшота (apply в Applier-е) — запускается механизм, который планирует рекомпозицию для всех зависимых composable-групп

MutableState как наблюдаемая ячейка снапшота

Реализация типа MutableState внутри:

  • хранит текущее значение T
  • хранит ссылку на SnapshotMutationPolicy<T>
  • содержит логику:
    • get() — регистрирует считывание в текущем Snapshot
    • set(new) — проверяет policy, изменилось ли значение и нужно ли триггерить изменение; помечает state изменённым в Snapshot

То есть MutableState — это не просто поле + listeners; это ячейка памяти, встроенная в глобальную систему Snapshot’ов.

Как всё это связано с рекомпозицией

Когда @Composable читает state.value, компилятор Compose превращает это в вызов, при котором:

  • Composer регистрирует «здесь прочли этот State»
  • Snapshot отмечает зависимость: groupId -> этот State

Когда ты делаешь state.value = newValue:

  1. Через Snapshot помечается, что у этого state новое значение
  2. Snapshot при apply вызывает observer изменений
  3. Runtime Compose получает список изменённых State и знает, какие group в дереве композиции зависят от них
  4. Планировщик запускает recomposition этих групп

И тут в игру входит SnapshotMutationPolicy.

Что такое SnapshotMutationPolicy

SnapshotMutationPolicy это политика (набор действий, алгоритмов), которая определяет, считается ли новое значение state «действительно новым«. Политика имеет примерно следующий интерфейс:

interface SnapshotMutationPolicy&lt;T> {
    fun equivalent(a: T, b: T): Boolean
}

И когда мы присваиваем какое-то значение state.value = x, то политика делает примерно следующее:

if (!policy.equivalent(oldValue, newValue)) {
    // значение реально изменилось => помечаем state как изменённый
}

Т.е. SnapshotMutationPolicy решает: нужно ли считать old-значение и new-значение равными, а значит — нужно ли триггерить рекомпозицию или можно её пропустить.

Есть несколько готовых видов SnapshotMutationPolicy:

  • structuralEqualityPolicy() — политика по умолчанию. Сравнивает значения через «==" (структурное равенство)
  • referentialEqualityPolicy() — сравнение идёт по ссылке(на объект в куче) "===«. Полезно если интересует сам объект, а не его логическое равенство. Особенно актуально для мутабельных объектов, где equals не всегда может быть тем, что нужно
  • neverEqualPolicy() — любой новый набор данных считается изменением. То всегда будет инвалидация, даже если объекты равны по значениям и-или ссылкам.


Опубликовано

в

,

от

Метки: