rememberUpdatedState
LaunchedEffect reinicia quando o valor de uma das keys muda. No entanto, em algumas situações você pode querer capturar um valor em seu efeito que, se for alterado, você não deseja que o efeito seja reiniciado. Para isso, é necessário utilizar rememberUpdatedState para criar uma referência a este valor que possa ser capturado e atualizado. Essa abordagem é útil para efeitos que contêm operações de longa duração que podem ser caras ou proibitivas para recriar e reiniciar.
Lendo o enunciado acima, pode parecer muito confuso e a documentação oficial também não ajuda muito nesse caso. Então vamos criar um exemplo simplificado de download de arquivo. Novamente, o código demonstrado abaixo não é uma abordagem recomendada, mas serve de exemplo para esse tópico.
Primeiro, vamos criar uma suspend function downloadFile():
private suspend fun downloadFile(onDownloadFinished: () -> Unit) {
withContext(Dispatchers.IO) {
delay(3000)
withContext(Dispatchers.Main) {
onDownloadFinished.invoke()
}
}
}
Ela não contém nada demais, possui apenas um delay de 3 segundos para simular um download de arquivo. Após o delay, invoca onDownloadFinished(), que será usado posteriormente para exibir um Toast.
Agora vamos criar a nossa função que utiliza um LaunchedEffect:
@Composable
private fun FileDownload(
url: String,
onFileNameObtained: (String) -> Unit,
fileName: String,
onDownloadFinished: () -> Unit
) {
val context = LocalContext.current
LaunchedEffect(Unit) {
onFileNameObtained.invoke(URLUtil.guessFileName(url, null, null))
downloadFile(
onDownloadFinished = {
Toast.makeText(
context,
"Arquivo \"$fileName\" baixado com sucesso!",
Toast.LENGTH_SHORT
).show()
onDownloadFinished.invoke()
}
)
}
}
Já vimos LaunchedEffect antes, então sabemos o que vai acontecer. Não queremos que ele seja reiniciado quando o valor de alguma key muda, por isso passamos Unit como key, informando que o nosso código no LaunchedEffect só será executado uma vez na composição. Assim que entramos no LaunchedEffect, usamos URLUtil.guessFileName()
, que é uma função que obtém o nome de arquivo a partir de uma URL, e então informamos ao chamador da FileDownload() que o nome de arquivo real foi obtido e ele faz o que desejar com isso. No nosso caso, isso servirá para atualizar fileName posteriormente, pois inicialmente ele possui um nome desconhecido.
Quando o download é concluído, uma mensagem Toast é exibida na tela.
Agora vamos criar a DownloadScreen() que faz uso da FileDownload():
@Composable
private fun DownloadScreen() {
val fileUrl = "https://site.com/files/video-123.mp4"
val defaultFileName = "???"
var fileName by remember { mutableStateOf(defaultFileName) }
var isDownloadingFile by remember { mutableStateOf(false) }
if (isDownloadingFile) {
FileDownload(
url = fileUrl,
fileName = fileName,
onFileNameObtained = { realFileName ->
fileName = realFileName
},
onDownloadFinished = {
isDownloadingFile = false
}
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
if (isDownloadingFile) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
LinearProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(14.dp))
Text(
text = "Baixando arquivo... Aguarde.",
fontSize = 22.sp,
textAlign = TextAlign.Center
)
}
} else {
Text(
text = "Nenhum arquivo sendo baixado no momento.",
fontSize = 22.sp,
textAlign = TextAlign.Center
)
}
Spacer(Modifier.height(18.dp))
Button(
onClick = {
fileName = defaultFileName
isDownloadingFile = true
},
enabled = !isDownloadingFile
) {
Text(
text = "Baixar",
fontSize = 18.sp
)
}
}
}
É uma tela simples que simula um download de arquivo. Quando tocamos no botão de baixar, definimos isDownloadingFile como true, fazendo então nossa FileDownload() entrar na composição e iniciar o download. Como já vimos na FileDownload(), antes mesmo do download iniciar, fileName é atualizado de "???" para o nome real obtido a partir da URL, que no caso é "video-123.mp4".
O resultado que esperamos é que um Toast com a mensagem "Arquivo "video-123.mp4" baixado com sucesso!" seja exibido na tela após 3 segundos, quando o download finalizar. Veja o resultado real:
Claramente não funcionou. Mas o que aconteceu? Vamos reler o que foi afirmado antes sobre o LaunchedEffect na nossa FileDownload(): Não queremos que ele seja reiniciado quando o valor de alguma key muda, por isso passamos Unit como key, informando que o nosso código no LaunchedEffect só será executado uma vez na composição.
fileName até é atualizado com sucesso ao obter o nome real do arquivo e isso acontece bem antes do download finalizar (o delay de 3 segundos), porém, o código no LaunchedEffect não saberá disso, pois ele não será reiniciado. Significa que quando usamos o trecho a seguir no onDownloadFinished:
Toast.makeText(
context,
"Arquivo \"$fileName\" baixado com sucesso!",
Toast.LENGTH_SHORT
).show()
A referência que temos de fileName ainda é a primeira antes de entrarmos no LaunchedEffect, ou seja, na primeira composição. Após entrarmos, mesmo que onFileNameObtained() seja invocado logo no início e atualize com sucesso o fileName, o que ainda temos é ??? como o valor de fileName.
Nesse tipo de situação, podemos utilizar rememberUpdatedState. Vamos fazer apenas uma pequena alteração na FileDownload():
@Composable
private fun FileDownload(
url: String,
onFileNameObtained: (String) -> Unit,
fileName: String,
onDownloadFinished: () -> Unit
) {
val context = LocalContext.current
val realFileName by rememberUpdatedState(newValue = fileName)
LaunchedEffect(Unit) {
onFileNameObtained.invoke(URLUtil.guessFileName(url, null, null))
downloadFile(
onDownloadFinished = {
Toast.makeText(
context,
"Arquivo \"$realFileName\" baixado com sucesso!",
Toast.LENGTH_SHORT
).show()
onDownloadFinished.invoke()
}
)
}
}
Agora temos o trecho mágico val realFileName by rememberUpdatedState(newValue = fileName)
que usa fileName para se manter atualizada. Dessa forma, ao usarmos ela no Toast, obteremos o resultado desejado, pois realFileName agora terá o valor atualizado de fileName, mesmo sem precisarmos reiniciar o LaunchedEffect e consequentemente o "download".
É claro que todo esse exemplo foi criado apenas com o intuito de ilustrar a funcionalidade do rememberUpdatedState. Para fins práticos, a função FileDownload() não precisaria existir e poderíamos ter o seguinte código na DownloadScreen():
@Composable
private fun DownloadScreen() {
...
if (isDownloadingFile) {
val context = LocalContext.current
LaunchedEffect(Unit) {
fileName = URLUtil.guessFileName(fileUrl, null, null)
downloadFile(
onDownloadFinished = {
Toast.makeText(
context,
"Arquivo \"$fileName\" baixado com sucesso!",
Toast.LENGTH_SHORT
).show()
isDownloadingFile = false
}
)
}
}
...
}
O resultado seria o mesmo, pois agora estamos alterando o valor de fileName de fato antes de ser usado pelo Toast, dentro do mesmo escopo, o que não acontece no caso da FileDownload(), que delega essa função para seu chamador e o LaunchedEffect/Toast não tem mais ciência sobre a atualização após a primeira composição.