← Назад к вопросам

Для чего нужны Side-effects в Jetpack Compose?

2.3 Middle🔥 101 комментариев
#UI и вёрстка

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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Загрузка данных при появленииОтмена корутины
DisposableEffectListeners, подпискиonDispose
SideEffectЛогирование, аналитикаНет
produceStateПревращение async в StateawaitDispose
rememberCoroutineScopeРучной запуск корутинАвтоматически

Итог

Side-effects в Compose — это контролируемый способ взаимодействия с внешним миром из чистого описания UI. Правильное использование side-effects обеспечивает:

  • Корректное управление жизненным циклом
  • Отсутствие утечек памяти
  • Предсказуемое поведение
  • Легче тестировать
Для чего нужны Side-effects в Jetpack Compose? | PrepBro