Skip to main content
The TecNM Control Escolar app uses Navigation Compose to handle all screen transitions. This provides a declarative, type-safe way to navigate between screens while maintaining a single-activity architecture. All navigation routes are defined using a sealed class in AppScreens.kt:
AppScreens.kt
package com.example.appcontrolescolar.navigation

sealed class AppScreens(val route: String) {

    object Home : AppScreens("home")

    object Schedule : AppScreens("schedule")

    object Map : AppScreens("map")

    object Profile : AppScreens("profile")
}

Why Sealed Classes?

Type Safety

Compile-time safety ensures you can’t navigate to non-existent routes

Autocomplete

IDE provides autocomplete for all available screens

Exhaustive When

Kotlin ensures all cases are handled in when expressions

Refactor Friendly

Renaming routes updates all references automatically

Route Naming Convention

Routes use lowercase strings matching the screen purpose:
ObjectRoute StringScreen
AppScreens.Home"home"HomeScreen
AppScreens.Schedule"schedule"ScheduleScreen
AppScreens.Map"map"MapScreen
AppScreens.Profile"profile"ProfileScreen
The AppNavigation composable sets up the navigation graph and integrates the bottom navigation bar:
AppNavigation.kt
package com.example.appcontrolescolar.navigation

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.example.appcontrolescolar.ui.components.BottomBar
import com.example.appcontrolescolar.ui.screens.HomeScreen
import com.example.appcontrolescolar.ui.screens.MapScreen
import com.example.appcontrolescolar.ui.screens.ProfileScreen
import com.example.appcontrolescolar.ui.screens.ScheduleScreen

@Composable
fun AppNavigation() {
    val navController = rememberNavController()

    Scaffold(
        bottomBar = {
            BottomBar(navController)
        }
    ) { innerPadding ->

        NavHost(
            navController = navController,
            startDestination = AppScreens.Home.route,
            modifier = Modifier.padding(innerPadding)
        ) {
            composable(AppScreens.Home.route) { HomeScreen() }
            composable(AppScreens.Schedule.route) { ScheduleScreen() }
            composable(AppScreens.Map.route) { MapScreen() }
            composable(AppScreens.Profile.route) { ProfileScreen() }
        }
    }
}

Key Components

1

NavController Creation

rememberNavController() creates and remembers the navigation controller across recompositions
2

Scaffold Layout

Material 3 Scaffold provides the app structure with a bottom bar
3

NavHost Configuration

Defines the navigation graph with all routes and their destinations
4

Composable Destinations

Each composable() call maps a route to a screen composable

Bottom Navigation Integration

The navigation system is tightly integrated with the bottom navigation bar. The BottomBar component receives the NavController to handle navigation:
Scaffold(
    bottomBar = {
        BottomBar(navController)
    }
) { innerPadding ->
    // NavHost uses innerPadding to avoid content overlap
    NavHost(
        modifier = Modifier.padding(innerPadding)
        // ...
    )
}
The innerPadding ensures that screen content doesn’t render underneath the bottom navigation bar.

Start Destination

The app launches to the Home screen by default:
NavHost(
    navController = navController,
    startDestination = AppScreens.Home.route,
    // ...
)

Route-to-Screen Mapping

Each route is mapped to its corresponding screen composable:
composable(AppScreens.Home.route) { HomeScreen() }
composable(AppScreens.Schedule.route) { ScheduleScreen() }
composable(AppScreens.Map.route) { MapScreen() }
composable(AppScreens.Profile.route) { ProfileScreen() }
Navigation actions are handled in the BottomBar component. Here’s how navigation is triggered:
BottomBar.kt (excerpt)
NavigationBarItem(
    selected = currentRoute == screen.route,
    onClick = {
        navController.navigate(screen.route) {
            popUpTo(navController.graph.startDestinationId)
            launchSingleTop = true
        }
    },
    // ...
)
popUpTo
Int
Clears the back stack up to the start destination, preventing stack buildup when switching between bottom nav items
launchSingleTop
Boolean
Prevents multiple instances of the same screen being added to the back stack

Current Route Detection

The bottom bar highlights the current screen by observing the back stack:
val navBackStackEntry = navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry.value?.destination?.route
This is used to set the selected state of navigation items:
NavigationBarItem(
    selected = currentRoute == screen.route,
    // ...
)

Adding New Screens

To add a new screen to the navigation system:
1

Define the Route

Add a new object to the AppScreens sealed class:
object NewScreen : AppScreens("new_screen")
2

Create the Screen Composable

Implement the screen in ui/screens/NewScreen.kt
3

Register in NavHost

Add a composable entry in AppNavigation.kt:
composable(AppScreens.NewScreen.route) { NewScreen() }
4

Update Bottom Bar (Optional)

If the screen should appear in the bottom navigation, update BottomBar.kt

Future Enhancements

Deep links can be added to support navigation from notifications or external intents.
Complex features may benefit from nested navigation graphs to better organize related screens.
Custom animations can be added to enhance the user experience during screen transitions.

Best Practices

Single NavController

Always use a single NavController for the entire app. Avoid creating multiple NavControllers as this complicates state management and back stack handling.

Stateless Screens

Keep screen composables stateless and let the ViewModel or state hoisting handle business logic. This makes screens easier to navigate to and test.

Safe Navigation

Always use the sealed class routes instead of string literals to prevent typos and ensure type safety.