В 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()— регистрирует считывание в текущем Snapshotset(new)— проверяет policy, изменилось ли значение и нужно ли триггерить изменение; помечает state изменённым в Snapshot
То есть MutableState — это не просто поле + listeners; это ячейка памяти, встроенная в глобальную систему Snapshot’ов.
Как всё это связано с рекомпозицией
Когда @Composable читает state.value, компилятор Compose превращает это в вызов, при котором:
Composerрегистрирует «здесь прочли этот State»- Snapshot отмечает зависимость:
groupId -> этот State
Когда ты делаешь state.value = newValue:
- Через
Snapshotпомечается, что у этого state новое значение - Snapshot при
applyвызывает observer изменений - Runtime Compose получает список изменённых
Stateи знает, какиеgroupв дереве композиции зависят от них - Планировщик запускает recomposition этих групп
И тут в игру входит SnapshotMutationPolicy.
Что такое SnapshotMutationPolicy
SnapshotMutationPolicy это политика (набор действий, алгоритмов), которая определяет, считается ли новое значение state «действительно новым«. Политика имеет примерно следующий интерфейс:
interface SnapshotMutationPolicy<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() — любой
новый набор данныхсчитается изменением. То всегда будет инвалидация, даже если объекты равны по значениям и-или ссылкам.