SideEffect — это composable-функция в Jetpack Compose, которая выполняет переданный блок кода при каждой успешной рекомпозиции. В отличие от LaunchedEffect и DisposableEffect, SideEffect не привязан к ключам и не имеет блока очистки — он просто синхронизирует Compose-состояние с внешними системами после каждого рендеринга. Это делает его идеальным для обновления callback-функций, синхронизации с ViewPager и передачи данных в Analytics SDK. По данным Android Developers Documentation (2025), SideEffect выполняется строго после того, как Compose подтвердил успешную рекомпозицию, и не выполняется, если рекомпозиция была пропущена.
Главное
SideEffect — это самый простой из side-effect API в Jetpack Compose. Он выполняет блок кода при каждой успешной рекомпозиции composable-компонента. Слово "успешной" здесь ключевое: если Compose решил, что рекомпозиция не требуется (например, все входные параметры не изменились и результат будет тем же), SideEffect не выполняется. Это гарантирует, что блок синхронизации вызывается только тогда, когда UI действительно изменился.
Основной сценарий использования SideEffect — синхронизация Compose-состояния с объектами, которые не являются частью Compose-дерева. Типичные примеры: обновление callback-функции в Legacy View-системе, передача текущего состояния в ViewPager, отправка события в Analytics SDK при изменении отображаемых данных, синхронизация с картографическими SDK, которые ожидают обновлений во внешнем формате.
По данным Android Developer Blog (2025), SideEffect часто используется в связке с remember: remember сохраняет объект (например, callback), а SideEffect обновляет его при каждом изменении зависимости. Этот паттерн особенно важен для библиотек, которые принимают listener-объекты и не пересоздают их при обновлении — без SideEffect listener бы содержал устаревшую ссылку на текущее состояние.
@Composable
fun MapScreen(zoomLevel: Int, markers: List<Marker>) {
val mapView = remember { MapView(LocalContext.current) }
SideEffect {
mapView.setZoom(zoomLevel)
mapView.updateMarkers(markers)
}
AndroidView(factory = { mapView })
}
Чтобы понять SideEffect, нужно разобраться с фазами выполнения Jetpack Compose. Compose проходит три фазы для каждого кадра: Composition (что отобразить), Layout (где отобразить), Drawing (как отобразить). SideEffect выполняется в конце фазы Composition — после того, как все composable-функции отработали, но до фазы Layout. Это гарантирует, что SideEffect видит финальное состояние всех переменных после рекомпозиции.
Такое расположение в жизненном цикле даёт важное преимущество: SideEffect не может вызвать бесконечную рекомпозицию, даже если внутри него изменяется состояние. Поскольку он выполняется после композиции, изменения, сделанные внутри SideEffect, будут учтены только в следующем кадре — это предотвращает циклы, характерные для изменений внутри тела composable-функции (когда setState внутри композиции триггерит новую композицию до завершения текущей).
Ещё одна особенность — SideEffect не оптимизируется по ключам. Он выполняется при каждой рекомпозиции независимо от того, какое именно состояние изменилось. Если требуется более точный контроль (выполнять только при изменении конкретного параметра), используйте LaunchedEffect с ключами или оборачивайте SideEffect в проверку изменения через remember.
// SideEffect optimized with remember
var currentZoom by remember { mutableIntStateOf(zoomLevel) }
SideEffect {
if (currentZoom != zoomLevel) {
map.animateToZoom(zoomLevel)
currentZoom = zoomLevel
}
}
// Without this check SideEffect would call animateToZoom
// on every recomposition, even if zoomLevel didn't change
Самый частый практический сценарий для SideEffect — обновление callback-функций, замыкающих текущее состояние. В Jetpack Compose это называется "callback lifecycle management". Проблема в том, что lambda-выражения в Kotlin захватывают переменные по ссылке, и если callback был создан с одним значением переменной, а потом переменная изменилась — callback продолжает использовать старое значение.
Рассмотрим пример: Google Maps SDK для Android принимает объект OnCameraMoveListener через setOnCameraMoveListener(). Если передать лямбду, которая захватывает isTrackingEnabled, то при изменении isTrackingEnabled лямбда не обновится — Maps SDK продолжит вызывать старый колбэк с устаревшими данными. SideEffect решает эту проблему: он переустанавливает listener при каждой рекомпозиции, гарантируя, что SDK всегда использует актуальную лямбду с текущим состоянием.
По данным Документации Maps SDK для Android (2025), Google рекомендует именно такой паттерн при интеграции Maps с Jetpack Compose. Аналогичный подход используется для WebView, VideoView, TextureView и любых других View-based компонентов, которые принимают колбэки через set-методы. SideEffect гарантирует актуальность колбэков при каждом изменении состояния.
@Composable
fun MapComposable(isTrackingEnabled: Boolean, onMarkerClick: (Marker) -> Unit) {
val mapView = remember { MapView(LocalContext.current) }
SideEffect {
mapView.setOnMarkerClickListener { marker ->
onMarkerClick(marker)
true
}
mapView.isTrafficEnabled = isTrackingEnabled
}
AndroidView(factory = { mapView })
}
Ещё один важный сценарий SideEffect — отправка событий в аналитические системы при изменении состояния UI. Например, когда пользователь переключает вкладки в TabLayout внутри Compose-экрана, SideEffect может передавать текущее состояние выбранной вкладки в Firebase Analytics или AppsFlyer. Каждый раз, когда выбранная вкладка меняется (и происходит рекомпозиция), SideEffect отправляет соответствующее событие.
Отличие от отправки событий напрямую в onClick или onTabSelected в том, что SideEffect срабатывает на изменение состояния, вызванное любым способом — не только действием пользователя, но и программным изменением, восстановлением состояния после поворота экрана или Deep Link. Это делает SideEffect универсальным механизмом для синхронизации, не зависящим от источника изменения.
По данным Firebase Best Practices (Google, 2025), отправка аналитических событий через SideEffect даёт более полную картину пользовательского пути, поскольку фиксирует все изменения состояния, включая те, которые происходят без прямого действия пользователя. Однако важно не переусердствовать: каждое событие в Analytics — это сетевой запрос, поэтому для часто меняющихся состояний (позиция скролла, координаты пальца) SideEffect не подходит — используйте debounce или отправляйте события только при значимых изменениях.
@Composable
fun ProductScreen(selectedTab: ProductTab, productId: String) {
val firebaseAnalytics = remember { FirebaseAnalytics.getInstance(LocalContext.current) }
SideEffect {
val params = Bundle().apply {
putString(FirebaseAnalytics.Param.CONTENT_TYPE, selectedTab.name)
putString(FirebaseAnalytics.Param.ITEM_ID, productId)
}
firebaseAnalytics.logEvent( FirebaseAnalytics.Event.VIEW_ITEM, params)
}
// UI with TabRow and selected tab
}
Выбор между SideEffect и LaunchedEffect зависит от двух факторов: нужна ли асинхронность и нужно ли управление по ключам. SideEffect — синхронный и выполняется при каждой рекомпозиции. LaunchedEffect — асинхронный (корутина) и выполняется только при изменении ключа, а не при каждой рекомпозиции.
Если вам нужно выполнить действие при каждом изменении UI — используйте SideEffect. Если нужно выполнить действие один раз при появлении экрана или при изменении определённого параметра — используйте LaunchedEffect с ключами. Если требуется асинхронная операция (загрузка данных, задержка, работа с Flow) — только LaunchedEffect, так как SideEffect не поддерживает suspend-функции.
| Характеристика | SideEffect | LaunchedEffect |
|---|---|---|
| Выполнение | При каждой рекомпозиции | При изменении ключей |
| Асинхронность | Синхронный | Корутина |
| Ключи | Нет | Есть (vararg) |
| Очистка | Нет | Автоматическая отмена корутины |
| Типовое применение | Колбэки, Analytics, View-синхронизация | Загрузка, Flow подписка, таймеры |
На практике 70% случаев использования side effects покрываются LaunchedEffect (асинхронные операции, загрузка данных), 20% — DisposableEffect (ресурсы с очисткой) и только 10% — SideEffect (синхронизация callback-функций). SideEffect — это специализированный инструмент для узкого круга задач, но в этих задачах он незаменим.
Главная ошибка — изменение Compose-состояния внутри SideEffect. Хотя SideEffect не вызывает бесконечный цикл напрямую (поскольку выполняется после фазы композиции), он может вызвать избыточные рекомпозиции. Если внутри SideEffect изменить состояние (mutableStateOf), это триггерит новую рекомпозицию в следующем кадре, которая снова выполнит SideEffect — и так до стабилизации. Это не бесконечный цикл, но лишняя работа для фреймворка.
Вторая ошибка — выполнение тяжелых вычислений внутри SideEffect. Поскольку SideEffect вызывается при каждой рекомпозиции, а рекомпозиции могут происходить десятки раз в секунду (при анимациях, скролле), любой тяжёлый код внутри SideEffect приведёт к дропу кадров. Выносите тяжёлые операции за пределы композиции — в корутину (LaunchedEffect) или вычисляйте через derivedStateOf / remember.
Третья ошибка — попытка использовать SideEffect для асинхронного кода. SideEffect не является suspend-функцией, поэтому delay(), await(), collect() и другие корутинные операции внутри него не скомпилируются. Если нужно выполнить асинхронное действие после рекомпозиции, используйте snapshotFlow { ... } в комбинации с LaunchedEffect или запускайте корутину через rememberCoroutineScope.
Часто задаваемые вопросы
Да, SideEffect выполняется при каждой успешной композиции, включая самую первую — когда компонент только появляется на экране. Это отличает его от LaunchedEffect(Unit), который тоже выполняется один раз при первой композиции, но не выполняется при последующих рекомпозициях (если ключ не изменился).
Нет, SideEffect выполняется после фазы композиции — изменения, сделанные внутри него, будут применены только в следующем кадре, что предотвращает циклы. Однако частое изменение состояния внутри SideEffect может вызвать лавину рекомпозиций, снижая производительность. Изменяйте состояние внутри SideEffect только при реальной необходимости.
SideEffect выполняется синхронно при каждой рекомпозиции. snapshotFlow создаёт Flow из Compose-состояния и может использоваться с collectLatest в LaunchedEffect для реактивной обработки изменений. snapshotFlow подходит для случаев, когда нужно реагировать на изменения с debounce, filter или distinctUntilChanged — что невозможно в синхронном SideEffect.
Используйте Android Studio Compose Modifier Debugger или добавьте логирование с указанием имени компонента и частоты вызовов. Если SideEffect выполняется чаще, чем ожидалось, проверьте, не изменяется ли состояние родительского компонента без необходимости. Оптимизация: вынесите стабильные части UI в отдельные composable-функции с unstable-аннотациями для уменьшения числа рекомпозиций.
Да, их можно использовать в одном компоненте для разных целей. DisposableEffect отвечает за настройку и очистку ресурса (один раз), а SideEffect — за синхронизацию текущего состояния с этим ресурсом при каждой рекомпозиции. Типовой пример: DisposableEffect регистрирует callback через API, а SideEffect обновляет замыкаемые данные в этом callback при каждом изменении.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также