Pular para conteúdo

Side Effects

Um side effect (efeito colateral) é uma mudança no estado do aplicativo que ocorre fora do escopo de uma função Composable. Devido ao ciclo de vida e às propriedades dos Composables, como recomposições imprevisíveis, execução de recomposições de Composables em ordens diferentes ou recomposições que podem ser descartadas (como falamos na seção de conhecimentos básicos iniciais), o ideal é que funções Composable sejam livres de efeitos colaterais.

No entanto, efeitos colaterais às vezes são necessários. Por exemplo, para desencadear um evento único, como mostrar um Snackbar ou navegar para outra tela dada uma determinada condição de estado. Essas ações devem ser chamadas a partir de um ambiente controlado que esteja ciente do ciclo de vida do Composable.

LaunchedEffect

Para chamar suspend functions dentro de uma função Composable, podemos utilizar o LaunchedEffect. Quando LaunchedEffect entrar na composição, ele lança uma coroutine com o bloco de código passado como parâmetro. A coroutine será cancelada se o LaunchedEffect sair da composição.

Vamos ver um exemplo simples, que pode não ser o mais ideal em um projeto "real", mas que pode exemplificar bem um uso do LaunchedEffect. Temos uma classe ProfileDataSource com uma suspend function getProfileName(). Por fim, temos uma Composable Profile() responsável por exibir um loading ou texto de sucesso, exibindo o nome de perfil obtido através da getProfileName(), ou um texto de erro. Obviamente, isso não é possível no cenário comum de uma função Composable, pois não podemos simplesmente chamar getProfileName() na Profile(). Primeiro, porque é uma suspend function, e, segundo, porque ela seria chamada inúmeras vezes devido a recomposição. Veja o exemplo de código:

class ProfileDataSource {

    suspend fun getProfileName(): String {
        delay(3000L)
        return "Compose Journey"
    }
}

enum class State {
    Loading,
    Success,
    Error
}

@Composable
private fun Profile() {
    val profileDataSource = remember { ProfileDataSource() }
    var state by remember { mutableStateOf(State.Loading) }
    var profileName by remember { mutableStateOf("") }

    // Isso não é possível
    profileName = profileDataSource.getProfileName()

    Column(
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.fillMaxSize()
    ) {
        when (state) {
            State.Loading -> {
                CircularProgressIndicator(color = Color.Blue)
            }
            State.Success -> {
                Text(text = "Nome do perfil: $profileName")
            }
            State.Error -> {
                Text(text = "Erro ao obter o nome do perfil.")
            }
        }
    }
}

Para resolver esse problema, podemos utilizar o LaunchedEffect:

@Composable
private fun Profile() {
    val profileDataSource = remember { ProfileDataSource () }
    var state by remember { mutableStateOf(State.Loading) }
    var profileName by remember { mutableStateOf("") }

    LaunchedEffect(Unit) {
        profileName = profileDataSource.getProfileName()
        state = State.Success
    }

    ...
}

Dessa forma, obtemos o nome do perfil e fazemos a atualização necessária em state.

Chaves

O argumento Unit que passamos no código que vimos antes é uma chave (key). LaunchedEffect aceita um número variável de chaves. Quando o valor de uma das chaves muda, a coroutine existente será cancelada e o trecho de LaunchedEffect será iniciado em uma nova coroutine. Basicamente é um mecanismo parecido com o remember() e também vemos isso em outros side effects. Passamos Unit como chave porque queremos executar o trecho de código apenas na primeira composição, já que Unit serve como uma constante.

Veja um pequeno código de timer abaixo que exemplifica o LaunchedEffect reagindo com as mudanças de valores de uma chave. Poderíamos fazer o código abaixo de inúmeras formas diferentes, mas novamente, serve como um exemplo.

@Composable
private fun CountdownTimer() {
    val initialTime = 5
    var currentTime by remember { mutableIntStateOf(initialTime) }

    LaunchedEffect(key1 = currentTime) {
        if (currentTime > 0) {
            delay(1000L)
            currentTime -= 1
        }
    }

    Box(
        contentAlignment = Alignment.Center,
        modifier = Modifier
            .size(250.dp)
            .clickable { currentTime = initialTime }
            .padding(50.dp)
            .border(
                width = 3.dp,
                color = Color.Red,
                shape = CircleShape
            )
    ) {
        Text(
            text = "$currentTime",
            fontSize = 24.sp,
            fontWeight = FontWeight.Bold
        )
    }
}

LaunchedEffect

No exemplo acima, estamos usando a suspend function delay() e o LaunchedEffect irá atualizar currentTime enquanto ele for maior que 0. Uma vez que ele atualiza currentTime para 4 na primeira execução após 1 segundo do delay, o valor da chave muda e o LaunchedEffect reinicia.

🔗 Conteúdos auxiliares: