La majorité des applications Android sont des applications multi-écrans.
Dans Jetpack Compose, la navigation entre écrans repose sur un composable appelé NavHost, qui contient les différentes destinations de l'application. Le NavController gère les interactions de navigation, permettant de passer d'un écran à un autre. Les destinations sont définies à l'aide de la fonction composable(), et les routes peuvent inclure des arguments pour transmettre des données entre les écrans. Le NavController fournit des méthodes comme navigate(), popBackStack(), et navigateUp() pour gérer les transitions et la pile de retour
La première version de la librairie de navigation est assez fastidieuse à utiliser surtout quand il s'agit de naviguer vers un écran en passant des paramètres
A partir de la version 2.8.0 dite "Type-Safe Navigation" publiée en 2024, la syntaxe de la librairie est devenue un peu plus "amicale" grace à la sérialisation du langage Kotlin. C'est cette version qui sera abordée dans ce tutoriel
Pour pouvoir utiliser la librairie de navigation, il faut quelques ajustement dans les fichiers du gradle:
L'implémentation exposée dans ce paragraphe n'est pas conseillée par la documentation officielle car elle consiste à passer le NavController en paramètre aux fonctions qui composent les écrans. Je l'expose tout de même car je la trouve plus facile à mettre en place
@Serializable
object EcranHome
@Serializable
object EcranA
@Serializable
data class EcranB(
val param1: String,
val param2: Int,
...
)
val myNavController = rememberNavController()
NavHost(navController = myNavController, startDestination = EcranHome)
{
composable <EcranHome> {
ComposerEcranHome(myNavController)
}
composable <EcranA> {
ComposerEcranA(myNavController)
}
composable <EcranB> {
val args = it.toRoute<EcranB>()
ComposerEcranB(myNavController, args)
}
}
fun ComposerEcranA(myNavController: NavController) {
...
...
}
fun ComposerEcranB(myNavController: NavController, args: EcranB) {
...
...
}
myNavController.navigate(EcranA)
myNavController.navigate(EcranB(param1 = "Dubois", param2 = 25))
myNavController.navigateUp()
myNavController.navigate(EcranHome){
popUpTo(myNavController.graph.startDestinationId) { inclusive = true }
launchSingleTop = true
}
fun ComposerEcranB(myNavController: NavController, args: EcranB) {
...
Text(text = "${args.param1} , ${args.param2} ans")
...
}
Voici un petit exemple constitué de trois écrans. Pour l'instant toute l'application est dans le même fichier. On verra plus tard comment améliorer l'architecture de l'application
package com.example.navigationtuto1
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Home
import androidx.compose.material3.Button
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import kotlinx.serialization.Serializable
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Navigateur()
}
}
}
@Serializable
object Home
@Serializable
object Clients
@Serializable
data class Profil(
val name: String,
val age: Int
)
@Composable
fun Navigateur(){
val myNavController = rememberNavController()
NavHost(navController = myNavController, startDestination = Home)
{
composable <Home> {
ScreenHome(myNavController)
}
composable <Clients> {
ScreenClients(myNavController)
}
composable <Profil> {
val profil = it.toRoute<Profil>()
ScreenProfil(myNavController, profil)
}
}
}
@Composable
fun ScreenHome(myNavController: NavController) {
Column(
verticalArrangement = Arrangement.spacedBy(40.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
/*======== Barre de Titre et navigation =========================*/
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Blue)
)
{
Text(
text = "Home",
fontSize = 30.sp,
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
Button(onClick = { myNavController.navigate(Clients) }) {
Text(text = "Liste des clients", fontSize = 20.sp)
}
}
}
@Composable
fun ScreenClients(myNavController: NavController) {
Column(
modifier = Modifier.fillMaxSize()
) {
/*======== Barre de Titre et navigation =========================*/
Row(horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Blue)
)
{
FilledIconButton(onClick = { myNavController.navigateUp() })
{
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}
Text(
text = "Clients",
fontSize = 30.sp,
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
TextButton(onClick = { myNavController.navigate(Profil(name = "Dubois", age = 25)) }) {
Text(text = "Dubois", fontSize = 20.sp)
}
TextButton(onClick = { myNavController.navigate(Profil(name = "Leblanc", age = 35)) }) {
Text(text = "Leblanc", fontSize = 20.sp)
}
TextButton(onClick = { myNavController.navigate(Profil(name = "Lenoir", age = 45)) }) {
Text(text = "Lenoir", fontSize = 20.sp)
}
}
}
@Composable
fun ScreenProfil(myNavController: NavController, profil: Profil) {
Column(
verticalArrangement = Arrangement.spacedBy(40.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
/*======== Barre de Titre et navigation =========================*/
Row(horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Blue)
)
{
FilledIconButton(onClick = { myNavController.navigateUp() })
{
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}
Text(
text = "Profil",
fontSize = 30.sp,
color = Color.White,
)
FilledIconButton(onClick = { myNavController.navigate(Home){
popUpTo(myNavController.graph.startDestinationId) { inclusive = true }
launchSingleTop = true
} })
{
Icon(imageVector = Icons.Default.Home, contentDescription = null)
}
}
Text(text = "Nom: ${profil.name}", fontSize = 20.sp)
Text(text = "Age: ${profil.age} ans", fontSize = 20.sp)
}
}
La documentation officielle recommande de ne pas passer le navcontroller comme paramètre et passer plutôt des événements correspondant à une destination.
Il y a assez peu de changements par rapport à l'implémentation présentée plus haut. Reprenons l'exemple avec trois écrans
@Serializable
object Home
@Serializable
object Clients
@Serializable
data class Profil(
val name: String,
val age: Int,
...
)
val myNavController = rememberNavController()
NavHost(navController = myNavController, startDestination = EcranHome)
{
// Définir le graphe de navigation ici
}
// Navigation vers l'écran précédent
val navigateBack: () -> Unit = { myNavController.navigateUp() }
// Navigation vers l'écran Home en effaçant la pile de retour
val navigateHome: () -> Unit = {
myNavController.navigate(Home) {
popUpTo(myNavController.graph.startDestinationId) { inclusive = true }
launchSingleTop = true
}
}
// Navigation (sans paramètres) vers l'écran 'Clients'
val navigateToClients: () -> Unit = { myNavController.navigate(Clients) }
// Navigation (avec paramètres) vers l'écran 'Profil'
val navigateToProfil: (Profil) -> Unit = { profil -> myNavController.navigate(profil) }
composable<Home> {
ScreenHome(navigateToClients)
}
composable<Clients> {
ScreenClients(navigateBack,navigateToProfil)
}
composable<Profil> {
val profil = it.toRoute<Profil>() // arguments
ScreenProfil(navigateBack, navigateHome, profil)
}
@Composable
fun ScreenHome(navigateToClients: () -> Unit) {
...
Button(onClick = navigateToClients) {
Text(text = "Clients")
}
...
}
@Composable
fun ScreenClients(navigateBack: () -> Unit, navigateToProfil: (Profil) -> Unit) {
...
TextButton(onClick = { navigateToProfil(Profil("Dubois",25)) }) {
Text(text = "Dubois",
fontSize = 20.sp,
)
}
...
}
@Composable
fun ScreenProfil(navigateBack: () -> Unit, navigateHome: () -> Unit, profil: Profil) {
...
FilledIconButton(onClick = navigateBack)
{
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}
...
FilledIconButton(onClick = navigateHome)
{
Icon(imageVector = Icons.Default.Home, contentDescription = null)
}
...
Text(text = "Nom: ${profil.name}")
Text(text = "Age: ${profil.age} ans")
...
}
Voici le code entier. Tout dans le même fichier.
package com.example.navigationts4
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Home
import androidx.compose.material3.Button
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import kotlinx.serialization.Serializable
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Navigateur()
}
}
}
@Serializable
object Home
@Serializable
object Clients
@Serializable
data class Profil(
val name: String,
val age: Int,
)
@Composable
fun Navigateur() {
val myNavController = rememberNavController()
NavHost(navController = myNavController, startDestination = Home)
{
// Navigation vers l'écran précédent
val navigateBack: () -> Unit = { myNavController.navigateUp() }
// Navigation vers l'écran Home en effaçant la pile de retour
val navigateHome: () -> Unit = {
myNavController.navigate(Home) {
popUpTo(myNavController.graph.startDestinationId) { inclusive = true }
launchSingleTop = true
}
}
// Navigation (sans paramètres) vers l'écran 'Clients'
val navigateToClients: () -> Unit = { myNavController.navigate(Clients) }
// Navigation (avec paramètres) vers l'écran 'Profil'
val navigateToProfil: (Profil) -> Unit = { profil -> myNavController.navigate(profil) }
composable<Home> {
ScreenHome(navigateToClients)
}
composable<Clients> {
ScreenClients(navigateBack,navigateToProfil)
}
composable<Profil> {
val profil = it.toRoute<Profil>() //Arguments
ScreenProfil(navigateBack, navigateHome, profil)
}
}
}
@Composable
fun ScreenHome(navigateToClients: () -> Unit) {
Column(
verticalArrangement = Arrangement.spacedBy(40.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
/*======== Barre de Titre et navigation =========================*/
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Blue)
)
{
Text(
text = "Home",
fontSize = 30.sp,
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
Button(onClick = navigateToClients) {
Text(text = "Clients")
}
}
}
@Composable
fun ScreenClients(navigateBack: () -> Unit, navigateToProfil: (Profil) -> Unit) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
// horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
/*======== Barre de Titre et navigation =========================*/
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Blue)
)
{
FilledIconButton(onClick = navigateBack)
{
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}
Text(
text = "Clients",
fontSize = 30.sp,
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
TextButton(onClick = { navigateToProfil(Profil("Dubois",25)) }) {
Text(text = "Dubois",
fontSize = 20.sp,
)
}
TextButton(onClick = { navigateToProfil(Profil("Lenoir",30)) }) {
Text(text = "Lenoir",
fontSize = 20.sp,
)
}
TextButton(onClick = { navigateToProfil(Profil("Leblanc",45)) }) {
Text(text = "Leblanc",
fontSize = 20.sp,
)
}
}
}
@Composable
fun ScreenProfil(navigateBack: () -> Unit, navigateHome: () -> Unit, profil: Profil) {
Column(
verticalArrangement = Arrangement.spacedBy(40.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
/*======== Barre de Titre et navigation =========================*/
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Blue)
)
{
FilledIconButton(onClick = navigateBack)
{
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}
Text(
text = "Profil",
fontSize = 30.sp,
color = Color.White,
)
FilledIconButton(onClick = navigateHome)
{
Icon(imageVector = Icons.Default.Home, contentDescription = null)
}
}
Text(text = "Nom: ${profil.name}")
Text(text = "Age: ${profil.age} ans")
}
}
On reprend le code précédent mais on le réorganise en plusieurs fichiers
package com.example.navigationfinal
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Navigateur()
}
}
}
package com.example.navigationfinal
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import kotlinx.serialization.Serializable
@Serializable
object Home
@Serializable
object Clients
@Serializable
data class Profil(
val name: String,
val age: Int,
)
@Composable
fun Navigateur() {
val myNavController = rememberNavController()
NavHost(navController = myNavController, startDestination = Home)
{
// Navigation vers l'écran précédent
val navigateBack: () -> Unit = { myNavController.navigateUp() }
// Navigation vers l'écran Home en effaçant la pile de retour
val navigateHome: () -> Unit = {
myNavController.navigate(Home) {
popUpTo(myNavController.graph.startDestinationId) { inclusive = true }
launchSingleTop = true
}
}
// Navigation (sans paramètres) vers l'écran 'Clients'
val navigateToClients: () -> Unit = { myNavController.navigate(Clients) }
// Navigation (avec paramètres) vers l'écran 'Profil'
val navigateToProfil: (Profil) -> Unit = { profil -> myNavController.navigate(profil) }
composable<Home> {
ScreenHome(navigateToClients)
}
composable<Clients> {
ScreenClients(navigateBack,navigateToProfil)
}
composable<Profil> {
val profil = it.toRoute<Profil>() //Arguments
ScreenProfil(navigateBack, navigateHome, profil)
}
}
}
package com.example.navigationfinal
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun ScreenHome(navigateToClients: () -> Unit) {
Column(
verticalArrangement = Arrangement.spacedBy(40.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
/*======== Barre de Titre et navigation =========================*/
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Blue)
)
{
Text(
text = "Home",
fontSize = 30.sp,
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
Button(onClick = navigateToClients) {
Text(text = "Clients")
}
}
}
package com.example.navigationfinal
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun ScreenClients(navigateBack: () -> Unit, navigateToProfil: (Profil) -> Unit) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.fillMaxSize()
) {
/*======== Barre de Titre et navigation =========================*/
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Blue)
)
{
FilledIconButton(onClick = navigateBack)
{
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}
Text(
text = "Clients",
fontSize = 30.sp,
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
TextButton(onClick = { navigateToProfil(Profil("Dubois",25)) }) {
Text(text = "Dubois",
fontSize = 20.sp,
)
}
TextButton(onClick = { navigateToProfil(Profil("Lenoir",30)) }) {
Text(text = "Lenoir",
fontSize = 20.sp,
)
}
TextButton(onClick = { navigateToProfil(Profil("Leblanc",45)) }) {
Text(text = "Leblanc",
fontSize = 20.sp,
)
}
}
}
package com.example.navigationfinal
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Home
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun ScreenProfil(navigateBack: () -> Unit, navigateHome: () -> Unit, profil: Profil) {
Column(
verticalArrangement = Arrangement.spacedBy(40.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
/*======== Barre de Titre et navigation =========================*/
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Blue)
)
{
FilledIconButton(onClick = navigateBack)
{
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}
Text(
text = "Profil",
fontSize = 30.sp,
color = Color.White,
)
FilledIconButton(onClick = navigateHome)
{
Icon(imageVector = Icons.Default.Home, contentDescription = null)
}
}
Text(text = "Nom: ${profil.name}")
Text(text = "Age: ${profil.age} ans")
}
}