Для чего нужны Side-effects в Jetpack Compose?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Side-effects в Jetpack Compose
Side-effects — это операции, которые должны происходить как результат композиции, но находятся вне рамок собственно рендеринга UI. Это одна из ключевых концепций в Compose для управления взаимодействием с внешним миром.
Почему нужны Side-effects
Compose компоненты должны быть чистыми функциями — при одинаковом input они должны возвращать одинаковый результат. Но в реальности нужно:
- Запускать асинхронные операции
- Обновлять базу данных
- Отправлять аналитику
- Работать с камерой или GPS
- Управлять жизненным циклом
Для этого существуют Side-effects API.
LaunchedEffect — запуск при появлении
@Composable
fun UserProfile(userId: String) {
var userData by remember { mutableStateOf<User?>(null) }
var isLoading by remember { mutableStateOf(false) }
// Этот блок запустится когда composable появится на экране
LaunchedEffect(userId) {
isLoading = true
userData = fetchUser(userId) // suspend function
isLoading = false
}
if (isLoading) {
CircularProgressIndicator()
} else if (userData != null) {
Text("User: ${userData.name}")
}
}
Как работает:
- Запускается когда composable впервые появляется
- Если
userIdменяется, блок переstarты (старый отменяется) - Отлично для инициализации и загрузки данных
DisposableEffect — управление ресурсами
@Composable
fun LocationTracker() {
var location by remember { mutableStateOf<Location?>(null) }
// Подписываемся на обновления локации при появлении
// Отписываемся когда composable исчезает
DisposableEffect(Unit) {
val locationListener = object : LocationListener {
override fun onLocationChanged(loc: Location) {
location = loc
}
}
// Подписываемся
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
1000,
0f,
locationListener
)
// cleanup блок (onDispose) — выполнится при удалении
onDispose {
locationManager.removeUpdates(locationListener)
}
}
Text("Location: ${location?.latitude}, ${location?.longitude}")
}
Когда использовать:
- Работа с listeners/observers
- Управление подписками
- Очистка ресурсов
SideEffect — побочный эффект каждый раз
@Composable
fun AnalyticsScreen(screenName: String) {
// Отправляем аналитику каждый раз когда composable рендерится
SideEffect {
Analytics.logScreenView(screenName)
}
Text("Welcome to $screenName")
}
// Или более сложный пример
@Composable
fun UserBadge(userId: String) {
var renderCount by remember { mutableStateOf(0) }
SideEffect {
renderCount++
Log.d("TAG", "UserBadge rendered $renderCount times")
}
Text("Render count: $renderCount")
}
Особенности:
- Выполняется после каждого успешного рендеринга
- Нет зависимостей (dependencies)
- Для операций которые зависят от текущего состояния
produceState — превращение обычного кода в State
@Composable
fun ImageWithSize(url: String) {
// produceState запускает блок и выдает State
val imageSize: State<Size?> = produceState<Size?>(initialValue = null) {
val bitmap = loadImage(url)
value = Size(bitmap.width, bitmap.height)
}
val size = imageSize.value
if (size != null) {
Text("Size: ${size.width}x${size.height}")
} else {
Text("Loading...")
}
}
// Более практичный пример
@Composable
fun NetworkStatus() {
val isOnline: State<Boolean> = produceState(initialValue = false) {
val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
value = true
}
override fun onLost(network: Network) {
value = false
}
}
connectivityManager.registerDefaultNetworkCallback(networkCallback)
awaitDispose {
connectivityManager.unregisterNetworkCallback(networkCallback)
}
}
val statusText = if (isOnline.value) "Online" else "Offline"
Text(statusText, color = if (isOnline.value) Green else Red)
}
rememberCoroutineScope — запуск корутин вручную
@Composable
fun SearchScreen() {
var query by remember { mutableStateOf("") }
var results by remember { mutableStateOf(emptyList<SearchResult>()) }
val scope = rememberCoroutineScope()
Column {
TextField(
value = query,
onValueChange = { newQuery ->
query = newQuery
// Запускаем поиск когда пользователь меняет текст
scope.launch {
results = searchAPI(query)
}
}
)
LazyColumn {
items(results) { result ->
SearchResultItem(result)
}
}
}
}
// Более сложный пример с debounce
@Composable
fun DebouncedSearch() {
var query by remember { mutableStateOf("") }
var results by remember { mutableStateOf(emptyList<SearchResult>()) }
val scope = rememberCoroutineScope()
var searchJob by remember { mutableStateOf<Job?>(null) }
TextField(
value = query,
onValueChange = { newQuery ->
query = newQuery
// Отменяем старый поиск если он еще выполняется
searchJob?.cancel()
// Запускаем новый поиск с задержкой
searchJob = scope.launch {
delay(500) // debounce 500ms
results = searchAPI(query)
}
}
)
}
Практический пример: полная экран с side-effects
@Composable
fun PostDetailScreen(postId: String) {
var post by remember { mutableStateOf<Post?>(null) }
var comments by remember { mutableStateOf(emptyList<Comment>()) }
var isLoading by remember { mutableStateOf(true) }
var error by remember { mutableStateOf<String?>(null) }
val scope = rememberCoroutineScope()
// Загружаем пост при появлении экрана
LaunchedEffect(postId) {
try {
isLoading = true
post = api.getPost(postId)
comments = api.getComments(postId)
} catch (e: Exception) {
error = e.message
} finally {
isLoading = false
}
}
// Отправляем аналитику
SideEffect {
Analytics.logPostViewed(postId)
}
Column {
when {
isLoading -> CircularProgressIndicator()
error != null -> Text("Error: $error")
post != null -> {
PostContent(post!!)
CommentsList(comments)
Button(onClick = {
// Запускаем добавление комментария
scope.launch {
try {
api.addComment(postId, "Great post!")
comments = api.getComments(postId)
} catch (e: Exception) {
error = e.message
}
}
}) {
Text("Add Comment")
}
}
}
}
}
Правила Side-effects
Выполняйте их после рендеринга — side-effects должны быть в специальных API, не в теле composable
Управляйте жизненным циклом — используйте dependencies и onDispose
Не создавайте утечки — всегда отписывайтесь и очищайте ресурсы
Избегайте бесконечных циклов — правильно настраивайте dependencies
Сравнение всех Side-effects
| API | Когда использовать | Cleanup |
|---|---|---|
| LaunchedEffect | Загрузка данных при появлении | Отмена корутины |
| DisposableEffect | Listeners, подписки | onDispose |
| SideEffect | Логирование, аналитика | Нет |
| produceState | Превращение async в State | awaitDispose |
| rememberCoroutineScope | Ручной запуск корутин | Автоматически |
Итог
Side-effects в Compose — это контролируемый способ взаимодействия с внешним миром из чистого описания UI. Правильное использование side-effects обеспечивает:
- Корректное управление жизненным циклом
- Отсутствие утечек памяти
- Предсказуемое поведение
- Легче тестировать