Source: https://libreshot.com/cs/wooden-rudder/

Introdução ao Compose Navigation

Nelson Glauber
5 min readJan 5, 2021

--

O Jetpack Compose veio com o intuito de ser o novo toolkit de UI para Android, substituindo os componentes que herdam da classe View por funções @Composable. Obviamente isso envolve toda uma mudança de paradigma de desenvolvimento de UI, que com o Compose, passa a ser declarativa, ao invés da abordagem imperativa utilizada atualmente. Mas um dos pontos que sempre chamou a atenção quando eu falava sobre o Compose era a navegação entre telas. Para isso, o Google está trabalhando na biblioteca de Navigation para Compose e neste post veremos como dar os primeiros com ela.

Adicionando a dependência

Nesse exemplo, estou utilizando o Android Studio 4.2 Canary 15 e a versão 1.0.0-alpha09 do Jetpack Compose. Sendo assim, se algo estiver diferente quando você estiver lendo esse artigo, verifique se houve mudança na API (o que é bem provável).
Além das dependências do Compose, é preciso adicionar a dependência da biblioteca de Navigation como mostrado a seguir:

dependencies {
...
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling:$compose_version"
implementation "androidx.navigation:navigation-compose:1.0.0-alpha04"
...
}

Essa versão do compose requer o Kotlin 1.4.21, então se estiver utilizando uma versão anterior, faça o devido ajuste no build.gradle do módulo e do projeto.

Implementando a navegação

Para realizar a navegação é bem simples: basta obter a instância do NavHostController por meio da função rememberNavController, e em seguida, declarar seu gráfico de navegação utilizando o componente NavHost.

// Na MainActivity
setContent
{
MyAppTheme {
val navController = rememberNavController()
NavHost
(navController, startDestination = "main") {
composable("main") { MainScreen(navController) }
composable("list") { ListScreen() }
}
}
}

Perceba que cada tela do fluxo é declarada utilizando a função composable e dentro do lambda é invocado o componente correspondente.
As duas "telas" (que na verdade são apenas funções Composable) estão listadas a seguir:

@Composable
fun MainScreen(navController: NavHostController) {
Column {
Text("Main Screen")
Button(onClick = {
navController.navigate("list")

}) {
Text("Go to Next")
}
}
}
@Composable
fun ListScreen() {
val names = listOf("Nelson", "Glauber", "Marcia", "Regina")
LazyColumn {
items(names) { name ->
Text(name)
}
}
}

A MainScreen recebe como parâmetro o objeto NavHostController, pois é com ele que é possível navegar para uma determinada tela por meio do meio do método navigate.

É possível declarar sub fluxos de navegação dentro do NavHost utilizando a função navigation.

NavHost(navController, startDestination = "route1") {
navigation(startDestination = "r1_tela1", route = "route1") {
composable("r1_tela1") { Rota1Tela1() }
composable("r1_tela2") { Rota1Tela2() }
}
navigation(startDestination = "r2_tela1", route = "route2") {
composable("r2_tela1") { Rota2Tela1() }
composable("r2_tela2") { Rota2Tela2() }
}
composable("outra_tela") { OutraTela() }
}

Passando parâmetros

A passagem de parâmetros simples pode ser feito por meio da rota como apresentado a seguir:

NavHost(navController, startDestination = "main") {
...
composable("details/{name}") { backStackEntry ->
DetailsScreen(
navController,
backStackEntry.arguments?.getString("name") ?: ""
)
}
}

A declaração dos parâmetros é feita utilizando a notação {param}. E para obter seu valor, foi utilizada a propriedade arguments do objeto backStackEntry recebido por parâmetro.

Neste exemplo, a ListScreen está chamando a DetailsScreen como pode ser observado a seguir.

@Composable
fun ListScreen(navController: NavHostController) {
val names = listOf("Nelson", "Glauber", "Marcia", "Regina")
LazyColumn {
items(names) { name ->
Text(name,
modifier = Modifier.clickable(
onClick = {
navController.navigate("details/$name")

}
)
)
}
}
}
@Composable
fun DetailsScreen(navController: NavHostController, name: String) {
Text("Details Screen: $name")
}

A passagem de parâmetros é feita utilizando um objeto Bundle, dessa forma também é possível passar objetos (Parcelable ou Serializable) similar ao que é feito em activities e fragments.

Para passar parâmetros, você pode usar:

// Tela de origem
navController.currentBackStackEntry
?.arguments?.putString("msg", "Your message")
navController.navigate("list")

E para recuperar esse parâmetro:

// Tela de destino
navController.previousBackStackEntry
?.arguments?.getString("msg", "")

Basicamente, um parâmetro é atribuído na tela atual (currentBackStackEntry) e em seguida a navegação é feita para a tela de destino (utilizando a função navigate). E na tela de destino, é possível acessar a tela anterior (previousBackStackEntry) e seus parâmetros. Assim, você pode compartilhar facilmente dados entre as telas da pilha.

Navegando de volta…

Para navegar para a tela anterior, basta invocar a função popBackStack().

navController.popBackStack()

Mas digamos que você precise voltar para um determinado ponto da navegação. Nesse exemplo, temos o fluxo "main" (A), "list" (B) e "details" (C). Como fazer para voltar da tela de "details" direto para "main"?
Basta usar o código a seguir:

// Voltando para a tela A, a patir da C (fechando a B)
navController.navigate("main") {
popUpTo("main") {
inclusive = false
}
launchSingleTop = true
}

Estamos informando que vamos navegar para a "main" usando a função navigate. Mas antes disso, queremos remover as telas da pilha (back stack) até chegar na "main" usando a função popUpTo, mas não a main não será removida (inclusive = false). Para evitar que existam duas telas “main” no topo da pilha, foi usado o atributo launchSingleTop com o valor true.

Retornando dados para a tela anterior

Retornar alguma informação para a tela anterior é quase o oposto à passagem de parâmetros. Basicamente é preciso apenas acessar as informações da tela anterior (previousBackStackEntry) e salvar as informações lá. A diferença principal é que é preciso saber quando esses dados foram modificados. Isso é feito por meio de um LiveData. Para isso, é preciso adicionar a seguinte dependência no build.gradle.

implementation "androidx.compose.runtime:runtime-livedata:$compose_version"

Feito isso, para retornar o dado para a tela anterior, faça algo como a seguir:

// Na tela que retornará a informação
navController.previousBackStackEntry
?.savedStateHandle
?.set("confirma", "sim")

navController.popBackStack()

E para observar o dado retornado pela tela utilize o seguinte:

// Tela que receberá o dado retornado
val confirma = navController.currentBackStackEntry
?.savedStateHandle
?.getLiveData<String>("confirma")?.observeAsState()
...
confirma?.value?.let {
Text(text = it)
}

A função observeAsState converte o objeto LiveData em um objeto State do Compose. Desta forma, quando o dado muda a recomposição é feita no composable.

Conclusão

O Jetpack Compose vem evoluindo rapidamente para trazer cada vez mais funcionalidades em sua API e a biblioteca de Navigation é um bom exemplo disso. Espero que esse ano de 2021 tenhamos uma versão estável do Compose, cheia de funcionalidades para os desenvolvedores.
Em breve lançarei mais um post sobre Navigation. Fique ligados!

4br4ç05
nglauber

Referências

--

--

Nelson Glauber

Android Developer. Google Developer Expert for Android/Kotlin. Author of “Dominando o Android”.