A Complete Guide to Navigation in Jetpack Compose from Getting Started to Mastering

A Complete Guide to Navigation in Jetpack Compose from Getting Started to Mastering

What is Android Navigation

Navigation helps you understand how your application moves between different components.

Android JetPack Navigation helps you achieve advanced navigation in a simple way.

The navigation component consists of three main parts:

Navigation Graph: This is a resource that brings together all navigation-related data. This includes all locations in the application (called destinations) and the paths a user may take within the application. It's like a big book that contains all the places you can access in the app and how to move between them. Think of it as a combination of a map and a guide.
NavHost: This is a unique composable item that you can include in your layout. It displays various target locations from your navigation map. NavHost links a NavController with a navigation graph that specifies the location of composable items. As you navigate between composable items, NavHost's contents will automatically regroup. Each composable target location in the navigation graph is associated with a route.
NavController: NavController is the central API for navigation components. It has state and tracks the back stack of composable items that make up the application's screens, as well as the state of each screen.

Navigation in Jetpack Compose

The navigation component powers Jetpack Compose applications. You can navigate between composable items while leveraging the infrastructure and capabilities of the navigation component.

To start navigating in Jetpack Compose, you need to include the required dependencies in your project's build.gradle file:

implementation "androidx.navigation:navigation-compose:2.7.1"

Basic concepts of navigation in Jetpack Compose.

NavController :
NavController is the central API for navigation components. It has state and tracks the back stack of composable items that make up the application's screens, as well as the state of each screen.

rememberNavController()You can create a NavController by using methods in the composable :

val navController = rememberNavController()

You should create the NavController at a location in the composable hierarchy and ensure that all composables that need to reference it have access to it. This follows the principles of state promotion and allows you to use a NavController and pass the currentBackStackEntryAsState()provided state as the source of truth for updating composable items off-screen. For an example of this functionality, see Integrate with the bottom navigation bar.

Note: If you are using a navigation component in a fragment, you do not need to define a new navigation graph in Compose or use a NavHost composable. See the Interoperability section for more information.

NavHost :
Each NavControllermust be associated with a NavHostcomposable. The NavHost will NavControllerbe linked to a navigation graph that specifies the locations of composable items that should be able to be navigated between. As you navigate between composable items, NavHostthe content is automatically recombined. Each composable target location in the navigation graph is associated with a route.

Key term: A route is a string that defines the path to a specific composable item. Think of it as an implicit deep link pointing to a specific destination. Each destination should have a unique route.

Creating a NavHost requires a route that was previously rememberNavController()created NavControlleras well as the starting destination of your navigation graph. The creation of NavHost uses the lambda syntax in the navigation Kotlin DSL to build the navigation graph. You can use composable()methods to add navigation structure. This method requires you to provide a route and composable items that should be associated with the destination:

NavHost(navController = navController, startDestination = "profile") {
    
    
    composable("profile") {
    
     Profile(/*...*/) }
    composable("friendslist") {
    
     FriendsList(/*...*/) }
    /*...*/
}

Note: The navigation component requires that you follow navigation principles and use a fixed starting destination. You should not startDestinationuse composable values ​​with routes.

Example: How to set up a navigation map, NavHostand NavigationItem.

Step 1 : Define the screen name and route of the navigation in a file, eg AppNavigation.kt.

enum class Screen {
    
    
    HOME,    
    LOGIN,
}
sealed class NavigationItem(val route: String) {
    
    
    object Home : NavigationItem(Screen.HOME.name)
    object Login : NavigationItem(Screen.LOGIN.name)
}

Step 2 : Define NavHost, e.g.AppNavHost.kt

@Composable
fun AppNavHost(
    modifier: Modifier = Modifier,
    navController: NavHostController,
    startDestination: String = NavigationItem.Splash.route,
    ... // other parameters
) {
    
    
    NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = startDestination
    ) {
    
    
        composable(NavigationItem.Splash.route) {
    
    
            SplashScreen(navController)
        }
        composable(NavigationItem.Login.route) {
    
    
            LoginScreen(navController)
        }
}

Step 3MainActivity.kt : Call in yourAppNavHost

class MainActivity : ComponentActivity() {
    
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContent {
    
    
            AutoPartsAppTheme {
    
    
               Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
    
    
                    AppNavHost(navController = rememberNavController())
                }
            }
        }
    }
}

Navigation Parameters:
Navigation Compose also supports passing parameters between composable item target positions. In order to achieve this, you need to add parameter placeholders to the route, similar to how you add parameters to deep links when using the base navigation library:

Usage:

  1. no parameters
  2. Use simple parameters such as predefined data types (such as Int, String, etc.)
  3. Use complex parameters such as user-defined data types
  4. Optional parameters
  5. Navigation to return results

No parameters:

NavHost(navController = navController, startDestination = "profile") {
    
    
    composable("profile") {
    
     Profile(/*...*/) }
    composable("friendslist") {
    
     FriendsList(/*...*/) }
    /*...*/
}

Use simple parameters:
By default, all parameters are parsed as strings. composable()The arguments parameter accepts NamedNavArgumentsa list. You can navArgumentcreate it quickly using methods NamedNavArgument, specifying their exact type:

NavHost(startDestination = "profile/{userId}") {
    
    
    ...
    composable("profile/{userId}") {
    
    ...}
}

By default, all parameters are parsed as strings. composable()The arguments parameter accepts NamedNavArgumentsa list. You can navArgumentcreate it quickly using methods NamedNavArgument, specifying their exact type:

NavHost(startDestination = "profile/{userId}") {
    
    
    ...
    composable(
        "profile/{userId}",
        arguments = listOf(navArgument("userId"){
    
    
           type = NavType.StringType 
        })
    ) {
    
    ...}
}

You should extract the parameters from composable()those available in the function's lambda NavBackStackEntry.

composable("profile/{userId}") {
    
     backStackEntry ->
   val userId = backStackEntry.arguments?.getString("userId")
   // 在这里获取用户数据
   Profile(
      navController, 
      // 传递获取的用户数据,如UserInfo
   )
}

To pass parameters to the target location, you need to append them to the route when making the navigation call:

navController.navigate("profile/user1234")

For a list of supported types, see Passing Data Between Destinations.

Use complex parameters or user-defined parameters:
It is strongly recommended not to pass complex data objects when navigating, but to pass the minimum necessary information as parameters when performing navigation operations, such as a unique identifier or other form of ID:

// 仅在导航到新目标时传递用户ID作为参数
navController.navigate("profile/user1234")

Complex objects should be stored as data in a single source of truth (such as a data layer). Once you navigate to your destination, you can use the passed ID to load the required information from a single source of truth. To retrieve the parameters in the ViewModel, the ViewModel responsible for accessing the data layer, you can use ViewModel的SavedStateHandle:

class UserViewModel(
    savedStateHandle: SavedStateHandle,
    private val userInfoRepository: UserInfoRepository
) : ViewModel() {
    
    

    private val userId: String = checkNotNull(savedStateHandle["userId"])

    // Fetch the relevant user information from the data layer,
    // ie. userInfoRepository, based on the passed userId argument
    private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(userId)

   --------------- OR -----------------
 
    // fetch data from network or database    
    private val _dataFlow =
            MutableStateFlow<UserInfo>(userInfoRepository.getUserInfo(userId))
    val dataFlow get() = _dataFlow.asStateFlow()
}

Composablefunction

//Navhost 
composable("profile/{userId}") {
    
     backStackEntry ->
   val userId = backStackEntry.arguments?.getString("userId")
   // here you have to fetch user data 
   val userInfo by taskViewModel.dataFlow.collectAsState()   
   Profile(
      navController, 
      userInfo
   )
}

// Profile screen
@Composable
fun Profile(navController: NavController, userInfo:UserInfo){
    
    
    // do you work here
}

NOTE: Place the ViewModel outside the composable screen as preview will not work and best practice is to avoid coupling between composable and ViewModel.

This approach helps prevent data loss during configuration changes and any inconsistencies when objects are updated or altered.

For a more detailed explanation of why you should avoid passing complex data as parameters, and a list of supported parameter types, see Passing Data Between Destinations.

Add optional parameters

Navigation Compose also supports optional navigation parameters. There are two differences between optional parameters and required parameters:

"?argName={argName}"Must be included using query parameter syntax ( )
must be set defaultValue, or set nullable = true(which implicitly sets the default value to null).
This means that all optional parameters must be added to the function explicitly composable()as a list:

composable(
    "profile?userId={userId}/{isMember}",
    arguments = listOf(
         navArgument("userId") {
    
    
            type = NavType.StringType
            defaultValue = "user1234"
           // OR 
            type = NavType.StringType
            nullable = true
         },
         navArgument("isNewTask") {
    
    
            type = NavType.BoolType
         }
     )
) {
    
     backStackEntry ->
    val userId = backStackEntry.arguments?.getString("userId")
    val isMember = backStackEntry.arguments?.getBoolean("isMember")?:false
    Profile(navController, userId, isMember)
}

Now, even if no parameters are passed to the target, defaultValue = "user1234"they will be used.

The structure of handling parameters via routes means your composable widgets are completely independent of navigation and makes them easier to test.

Back navigation with results

Back navigation with results is the most common task. For example, when you open a filter dialog box and select filters, then navigate back with the selected filters to apply those filters to the screen.

There are two screens. 1. FirstScreen(first screen) and 2. SecondScreen(second screen). We need to get data from SecondScreen(second screen) to FirstScreen(first screen).

NavHost.kt : Set up the navigation map.

val navController = rememberNavController()
 NavHost(
     navController = navController,
     startDestination = "firstscreen"
 ) {
    
    
    composable("firstscreen") {
    
    
        FirstScreen(navController)
    }
    composable("secondscreen") {
    
    
        SecondScreen(navController)
    }
}
@Composable
fun FirstScreen(navController: NavController) {
    
    
    // Retrieve data from next screen
    val msg = 
        navController.currentBackStackEntry?.savedStateHandle?.get<String>("msg")
    Column(
        Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
    
    
        Button(onClick = {
    
     navController.navigate("secondscreen") }) {
    
    
            Text("Go to next screen")
        }
        Spacer(modifier = Modifier.height(8.dp))
        msg?.let {
    
    
            Text(it)
        }
    }
}

FirstScreen.ktsavedStateHandle : Retrieve data after returning from the NavController's current return stack entry SecondScreen.

@Composable
fun FirstScreen(navController: NavController) {
    
    
    // Retrieve data from next screen
    val msg = 
        navController.currentBackStackEntry?.savedStateHandle?.get<String>("msg")
    Column(
        Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
    
    
        Button(onClick = {
    
     navController.navigate("secondscreen") }) {
    
    
            Text("Go to next screen")
        }
        Spacer(modifier = Modifier.height(8.dp))
        msg?.let {
    
    
            Text(it)
        }
    }
}

SecondScreen.kt : Put data into NavControllerthe previous return stack entry savedStateHandle.

@Composable
fun SecondScreen(navController: NavController) {
    
    
    var text by remember {
    
    
        mutableStateOf("")
    }
    Column(
        Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
    
    
        TextField(
            value = text, onValueChange = {
    
     text = it },
            placeholder = {
    
    
                Text("Enter text", color = Color.Gray)
            }
        )
        Spacer(Modifier.height(8.dp))
        Button(onClick = {
    
    

           // Put data into savedStateHandle to retrive data on the previous screen
   
            navController.previousBackStackEntry?.savedStateHandle?.set("msg", text)
            navController.popBackStack()
        }) {
    
    
            Text(text = "Submit")
        }
    }
}

https://github.com/KaushalVasava/JetPackCompose_Basic/tree/navigate-back-with-result

Deep links

Navigation Compose supports defining implicit deep links as part of the composable() function. Its deepLinks parameter accepts a list of NavDeepLinks, which can be quickly created using the navDeepLink method:

val uri = "https://www.example.com"
composable(
    "profile?id={id}",
    deepLinks = listOf(navDeepLink {
    
     uriPattern = "$uri/{id}" })
) {
    
     backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("id"))
}

These deep links can associate specific URLs, actions, or MIME types with composables. By default, these deep links are not exposed to external applications. To make these deep links available externally, the appropriate elements must be added to the application's manifest.xml file. In order to enable deep linking as described above, you should add the following inside the element of your manifest file:

<activity >
  <intent-filter>
    ...
    <data android:scheme="https" android:host="www.example.com" />
  </intent-filter>
</activity>

When a deep link is triggered by another app, navigation will automatically go to that composable.

These same deep links can also be used to build a PendingIntent from a composable with appropriate deep links:

val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://www.example.com/$id".toUri(),
    context,
    MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
    
    
    addNextIntentWithParentStack(deepLinkIntent)
    getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}

You can then use this deepLinkPendingIntent like any other PendingIntent to open your app's deep link target.

Nested Navigation


Goals can be grouped into nested graphs to modularize specific processes in your application interface. An example would be a standalone login process.

A nested shape groups its targets, just like the main shape, and it requires a specified starting target as its associated route, which you will reach when accessing that nested shape's route.

To add nested graphics to a NavHost, you can use the navigation extension functions:

NavHost(navController, startDestination = "home") {
    
    
    ...
    // 通过其路由('login')导航到图形会自动导航到图形的起始目标 - 'username'
    // 因此封装了图形的内部路由逻辑
    navigation(startDestination = "username", route = "login") {
    
    
        composable("username") {
    
     ... }
        composable("password") {
    
     ... }
        composable("registration") {
    
     ... }
    }
    ...
}

It is highly recommended to split the navigation graph into multiple methods as the graph grows. This also allows multiple modules to contribute their own navigation graphics.

fun NavGraphBuilder.loginGraph(navController: NavController) {
    
    
    navigation(startDestination = "username", route = "login") {
    
    
        composable("username") {
    
     ... }
        composable("password") {
    
     ... }
        composable("registration") {
    
     ... }
    }
}

By making this method an extension method of NavGraphBuilder, you can use it with the pre-built navigation, composable and dialog extension methods:

NavHost(navController, startDestination = "home") {
    
    
    ...
    loginGraph(navController)
    ...
}

Example:

val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
    
    
      composable("about") {
    
    }
      navigation(
         startDestination = "login",
        route = "auth"
      ) {
    
    
          composable("login") {
    
    
          val viewModel = it.sharedViewModel<SampleViewModel>(navController)

          Button(onClick = {
    
    
               navController.navigate("calendar") {
    
    
                  popUpTo("auth") {
    
    
                     inclusive = true
                  }
               }
           }) {
    
    
          }
         }
         composable("register") {
    
    
             val viewModel = it.sharedViewModel<SampleViewModel>(navController)
         } 
         composable("forgot_password") {
    
    
             val viewModel = it.sharedViewModel<SampleViewModel>(navController)
         }
      }
      navigation(
          startDestination = "calendar_overview",
          route = "calendar"
      ) {
    
    
           composable("calendar_overview") {
    
     }
           composable("calendar_entry") {
    
     }
      }
    }

NavBackStackextension function

@Composable
inline fun <reified T : ViewModel> NavBackStackEntry.sharedViewModel(navController: NavController): T {
    
    
    val navGraphRoute = destination.parent?.route ?: return viewModel()
    val parentEntry = remember(this) {
    
    
        navController.getBackStackEntry(navGraphRoute)
    }
    return viewModel(parentEntry)
}

Integration with bottom nav bar

By defining it at a higher level of the composable hierarchy NavController, you can connect the navigation with other components, such as the bottom navigation component. Doing so enables navigation by selecting the icons in the bottom bar.

To use BottomNavigationthe and BottomNavigationItemcomponents, androidx.compose.materialadd the dependency to your Android application.

dependencies {
    
    
    implementation "androidx.compose.material:material:1.5.1"
}
android {
    
    
    buildFeatures {
    
    
        compose true
    }
    composeOptions {
    
    
        kotlinCompilerExtensionVersion = "1.5.3"
    }
    kotlinOptions {
    
    
        jvmTarget = "1.8"
    }
}

In order to link items in the bottom navigation bar to routes in the navigation graph, it is recommended to define a sealed class (such as Screen here) that contains the target's route and String resource ID.

sealed class Screen(val route: String, @StringRes val resourceId: Int) {
    
    
    object Profile : Screen("profile", R.string.profile)
    object FriendsList : Screen("friendslist", R.string.friends_list)
}

Then put these items into a list that can be used by BottomNavigationItem:

val items = listOf(
   Screen.Profile,
   Screen.FriendsList,
)

In BottomNavigationa composable function, use currentBackStackEntryAsState()the function to get the current one NavBackStackEntry. This entry gives you access to the current NavDestination. BottomNavigationItemThe selected state of each can then be determined by comparing the item's route with the routes of the current target and its parent target (to handle the case when using nested navigation) , through NavDestinationthe hierarchy.

The item's route is also used to connect the onClick lambda to the navigation call so that when the item is clicked, it navigates to it. Correctly save and restore the item's state and back stack when switching between bottom navigation items by using saveStatethe and flags.restoreState

val navController = rememberNavController()
Scaffold(
  bottomBar = {
    
    
    BottomNavigation {
    
    
      val navBackStackEntry by navController.currentBackStackEntryAsState()
      val currentDestination = navBackStackEntry?.destination
      items.forEach {
    
     screen ->
        BottomNavigationItem(
          icon = {
    
     Icon(Icons.Filled.Favorite, contentDescription = null) },
          label = {
    
     Text(stringResource(screen.resourceId)) },
          selected = currentDestination?.hierarchy?.any {
    
     it.route == screen.route } == true,
          onClick = {
    
    
            navController.navigate(screen.route) {
    
    
              // Pop up to the start destination of the graph to
              // avoid building up a large stack of destinations
              // on the back stack as users select items
              popUpTo(navController.graph.findStartDestination().id) {
    
    
                saveState = true
              }
              // Avoid multiple copies of the same destination when
              // reselecting the same item
              launchSingleTop = true
              // Restore state when reselecting a previously selected item
              restoreState = true
            }
          }
        )
      }
    }
  }
) {
    
     innerPadding ->
  NavHost(navController, startDestination = Screen.Profile.route, Modifier.padding(innerPadding)) {
    
    
    composable(Screen.Profile.route) {
    
     Profile(navController) }
    composable(Screen.FriendsList.route) {
    
     FriendsList(navController) }
  }
}

Here you can leverage NavController.currentBackStackEntryAsState()methods to promote navControllerstate, extract it from the NavHost function, and BottomNavigationshare it with the component. This means BottomNavigationautomatically having the latest status.

Guess you like

Origin blog.csdn.net/u011897062/article/details/133267783