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.
Navigation Routes
All navigation routes are defined using a sealed class in 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:
Object Route String Screen AppScreens.Home"home"HomeScreen AppScreens.Schedule"schedule"ScheduleScreen AppScreens.Map"map"MapScreen AppScreens.Profile"profile"ProfileScreen
Navigation Graph Setup
The AppNavigation composable sets up the navigation graph and integrates the bottom navigation bar:
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
NavController Creation
rememberNavController() creates and remembers the navigation controller across recompositions
Scaffold Layout
Material 3 Scaffold provides the app structure with a bottom bar
NavHost Configuration
Defines the navigation graph with all routes and their destinations
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.
Navigation Configuration
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
Navigation actions are handled in the BottomBar component. Here’s how navigation is triggered:
NavigationBarItem (
selected = currentRoute == screen.route,
onClick = {
navController. navigate (screen.route) {
popUpTo (navController.graph.startDestinationId)
launchSingleTop = true
}
},
// ...
)
Navigation Options
Clears the back stack up to the start destination, preventing stack buildup when switching between bottom nav items
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:
Define the Route
Add a new object to the AppScreens sealed class: object NewScreen : AppScreens ( "new_screen" )
Create the Screen Composable
Implement the screen in ui/screens/NewScreen.kt
Register in NavHost
Add a composable entry in AppNavigation.kt: composable (AppScreens.NewScreen.route) { NewScreen () }
Update Bottom Bar (Optional)
If the screen should appear in the bottom navigation, update BottomBar.kt
Future Enhancements
Future screens may need to pass arguments (e.g., class ID for detail screens). Navigation Compose supports type-safe arguments using route parameters.
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.