07 - Introdução à navegação no Jetpack Compose
Navegação é o processo de mover o usuário entre diferentes telas (ou funcionalidades) do App.
Com Jetpack Compose, isso é feito através de componentes baseados na ideia de UI declarativa. Ou seja, ainda utilizaremos Composables
.
A navegação com Jetpack Compose tem alguns pontos interessantes:
- Facilita a organização, controle e o fluxo entre telas em apps criados com Compose.
- Reduz acoplamento e gerencia automaticamente a pilha de telas (back stack).
Conceitos Básicos
NavHost
: Contêiner que exibe as telas (destinos) conforme o fluxo de navegação.NavController
: Controlador responsável por gerenciar comandos de navegação (ir para próxima tela, voltar, etc).NavGraph
: Também chamado de Composable Destinations, define as trocas de telas possíveis na aplicação.
important
Cada tela navegável é uma função @Composable.
Abordaremos cada um desses conceitos a seguir. Mas antes, é necessário configurar o projeto.
Configurando a Navegação
Basta adicionarmos uma dependência ao projeto para que as funções de navegação estejam disponíveis.
dependencies {
val nav_version = "2.8.9"
implementation("androidx.navigation:navigation-compose:$nav_version")
}
Uma aplicação simples com navegação
Link para o repositório com o código completo: https://github.com/tads-ufpr-alexkutzke/ds151-aula-07-movies-app/tree/main
A aplicação proposta é bastante simples e terá apenas duas telas.
Em primeiro lugar, criaremos dois Composables para representar as telas da aplicação.
@Composable
fun MoviesScreen(onGoToMovieDetailsClick: () -> Unit = {}){
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text="Tela de Filmes")
ElevatedButton(
onClick = onGoToMovieDetailsClick
) {
Text("Vai para tela de detalhes")
}
}
}
@Preview
@Composable
fun MoviesScreenPreview() {
MoviesAppTheme{
MoviesScreen()
}
}
@Composable
fun MovieDetailsScreen(){
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text= "Tela de detalhes")
}
}
@Preview
@Composable
fun MovieDetailsPreview(){
MoviesAppTheme{
MovieDetailsScreen()
}
}
Os composables MoviesScreen
e MovieDetailsScreen
não tem nada de diferente dos composables vistos nas últimas aulas.
O componente MoviesApp
Agora criaremos um componente que irá representar a aplicação como um todo e será responsável por abrigar todos os dados referentes à navegação.
A criação desse componente não é obrigatória, mas é um padrão comum de desenvolvimento com o Jetpack Compose.
@Composable
fun MoviesApp(
navController: NavHostController = rememberNavController()
){
NavHost(
navController = navController,
startDestination = "movies",
){
composable("movies"){
MoviesScreen()
}
composable("movieDetails"){
MovieDetailsScreen()
}
}
}
@Preview
@Composable
fun MoviesAppPreview(){
MoviesAppTheme{}
MoviesApp()
}
}
No código acima, já temos os 3 componentes da navegação:
NavHost
: define um componente de navegação:navController
: variável vem derememberNavController()
;startDestination
: define a tela inicial para a navegação;
composable
ouNavGraph
: cadacomposable
define uma rota, ou tela, possível para a navegação:- Uma rota pode ser entendida como uma url;
- Existem várias formas de nomear uma rota: a mais simples é apenas com uma string contendo um nome único;
- A seguir veremos outras formas;
No exemplo, temos duas rotas: "movies"
e "movieDetails"
. Cada uma renderiza uma das telas criadas.
Já podemos atualizar a MainActivity
para que utilize nosso novo componente;
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MoviesAppTheme{}
MoviesApp()
}
}
}
}
@Composable
fun MoviesApp(
navController: NavHostController = rememberNavController(),
){
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
NavHost(
modifier = Modifier.padding(innerPadding),
navController = navController,
startDestination = "movies",
) {
composable("movies") {
MoviesScreen()
}
composable("movieDetails") {
MovieDetailsScreen()
}
}
}
}
No exemplo, já aproveitamos para passar o Scaffold
também para dentro do componente MoviesApp
.
Navegando para a segunda tela
Para trocarmos de tela, é necessário utilizarmos o método navigate
do componente navController
.
A tela MoviesScreen
já possui um evento para definir o comportamento do botão para trocar de tela.
Portanto, basta atribuir esse evento:
composable("movies") {
MoviesScreen(
onGoToMovieDetailsClick = {
navController.navigate("movieDetails")
})
}
Dessa forma, o botão já realizar a troca de telas.
Um ponto interessante, é que o NavHost
já irá cuidar de toda a pilha de telas automaticamente. Por exemplo, ao rodar a aplicação no emulador, o botão de Voltar
da interface do smartphone já é capaz de retornar à tela anterior.
Porém, também é possível adicionarmos um botão na tela movieDetails
para realizarmos a volta para a tela anterior.
important
É importante que a lógica de navegação não seja compartilhada entre os componentes. Ou seja, os componentes filhos (telas e outros) devem apenas utilizar funções definidas no escopo do NavHost
. Por exemplo, não é uma boa prática, passar a variável navController
como parâmetro para outros componentes.
Primeiro, adicionamos o botão:
@Composable
fun MovieDetailsScreen(
onGoBackClick: () -> Unit = {}
){
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text= "Tela de detalhes")
ElevatedButton(
onClick = onGoBackClick
) {
Text("Voltar")
}
}
}
Agora, basta atribuir o evento corretamente no componente MoviesApp
:
composable("movieDetails") {
MovieDetailsScreen(
onGoBackClick = {
navController.navigate("movies")
}
)
}
Passando argumentos para telas
Ao trocarmos de tela, por vezes, é interessante que argumentos sejam passados. Por exemplo, ao trocar para a tela de detalhes de um filme, talvez seja interessante informarmos o id
do filme a ser mostrado na tela seguinte.
Para realizar a passagem desses argumentos, pode alterar um pouco a rota:
composable("movies") {
MoviesScreen(
onGoToMovieDetailsClick = {
navController.navigate("movieDetails/10")
})
}
composable("movieDetails/{movieId}") { backStackEntry ->
val movieId:String = backStackEntry.arguments?.getString("movieId") ?: ""
MovieDetailsScreen(
movieId = movieId,
onGoBackClick = {
navController.navigate("movies")
}
)
}
// ...
@Composable
fun MovieDetailsScreen(
movieId: String = "",
onGoBackClick: () -> Unit = {}
){
// ...
Text(text= "Tela de detalhes")
Text(text= "MovieId: $movieId")
// ...
}
É preciso prestar um pouco de atenção aos tipos de variáveis. Como um determinado argumento pode ser nulo, a variável que o recebe deve ser nullable
, ou uma checagem deve ser realizada antes (operador ?:
).
Definindo tipos de argumentos
É possível, ainda, definir mais detalhes sobre os argumentos passados. Para isso, utilizaremos o parâmetro argumentos
e o componente navArgument
:
composable(
route="movieDetails/{movieId}",
arguments = listOf(
navArgument("movieId") {
defaultValue = 0
type = NavType.IntType
}
)
) { backStackEntry ->
val movieId:Int? = backStackEntry.arguments?.getInt("movieId")
movieId?.let{
MovieDetailsScreen(
movieId = it,
onGoBackClick = {
navController.navigate("movies")
}
)
}
}
}
// ...
@Composable
fun MovieDetailsScreen(
movieId: Int = -1,
onGoBackClick: () -> Unit = {}
){
// ...
Text(text= "Tela de detalhes")
Text(text= "MovieId: $movieId")
// ...
}
Outras formas de retornar na navegação
Navegação aninhada
Referência: https://medium.com/@KaushalVasava/navigation-in-jetpack-compose-full-guide-beginner-to-advanced-950c1133740
Destinações podem ser agrupadas em um grafo mais complexo, ou aninhado:
Para isso, agrupe rotas relacionadas em um componente navigation
:
NavHost(navController, startDestination = "home") {
...
// Navigating to the graph via its route ('login') automatically
// navigates to the graph's start destination - 'username'
// therefore encapsulating the graph's internal routing logic
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
...
}