Skip to main content

Overview

NASA Explorer uses Jetpack Compose Navigation with Kotlin serialization for type-safe navigation between screens. The app has two navigation levels: authentication flow and main app flow.

Type-Safe Routes

Routes are defined as a sealed class with serializable objects:
ui/core/Routes.kt
package com.ccandeladev.nasaexplorer.ui.core

import kotlinx.serialization.Serializable

sealed class Routes {

    @Serializable
    data object Splash : Routes()

    @Serializable
    data object Login : Routes()

    @Serializable
    data object SignUp : Routes()

    @Serializable
    data object Home : Routes()

    @Serializable
    data object DailyImage : Routes()

    @Serializable
    data object RangeImages : Routes()

    @Serializable
    data object RandomImage : Routes()

    @Serializable
    data object FavoriteImages: Routes()
}
Using sealed classes with @Serializable provides compile-time safety. The compiler ensures you can’t navigate to a route that doesn’t exist, and navigation arguments are type-checked.

Top-Level Navigation

The main navigation graph handles authentication flow:
ui/core/NasaExplorerNav.kt
@Composable
fun NasaExplorerNav(navHostController: NavHostController) {
    // Configurar navHost con controlador y ruta de inicio
    NavHost(
        navController = navHostController, 
        startDestination = Routes.Splash
    ) {

        // SplashScreen - decides authentication state
        composable<Routes.Splash> {
            SplashScreen(
                onNavigateToLogin = {
                    navHostController.navigate(Routes.Login)
                }, 
                onNavigateToHome = {
                    navHostController.navigate(Routes.Home)
                }
            )
        }

        // LoginScreen
        composable<Routes.Login> {
            LoginScreen(
                onNavigationToHome = {
                    navHostController.navigate(Routes.Home)
                }, 
                onNavigationToSignUp = {
                    navHostController.navigate(Routes.SignUp)
                }
            )
        }

        // SignUpScreen
        composable<Routes.SignUp> {
            SignUpScreen(
                onNavigateToHome = {
                    navHostController.navigate(Routes.Home)
                }
            )
        }

        // HomeScreen - main app entry point
        composable<Routes.Home> {
            HomeScreen(
                onNavigateToLogin = {
                    navHostController.navigate(Routes.Login)
                }
            )
        }
    }
}
1

NavHost Setup

Create a NavHost with a NavController and starting destination
2

Composable Destinations

Define each screen as a composable with type-safe route
3

Navigation Callbacks

Pass lambda callbacks to child composables for navigation actions

Nested Navigation

The HomeScreen contains its own nested navigation graph for the main app features:
ui/homescreen/HomeScreen.kt
@Composable
fun HomeScreen(
    homeScreenViewModel: HomeScreenViewModel = hiltViewModel(),
    onNavigateToLogin: () -> Unit
) {
    // NavController para gestionar navegación interna
    val navController = rememberNavController()

    // Botón "atrás" -> solo vuelve al home
    BackHandler {
        navController.popBackStack(Routes.Home, inclusive = false)
    }

    Scaffold(
        topBar = {
            HomeTopBar(
                navController = navController,
                homeScreenViewModel = homeScreenViewModel,
                onNavigateToLogin = onNavigateToLogin
            )
        },
        bottomBar = {
            HomeBottomBar(navController = navController)
        }
    ) { paddingValues ->

        // Nested NavHost for main features
        NavHost(
            navController = navController,
            startDestination = Routes.Home,
            enterTransition = { fadeIn(animationSpec = tween(1500)) },
            exitTransition = { fadeOut(animationSpec = tween(1500)) },
            modifier = Modifier
                .fillMaxSize()
                .background(Color.Black)
                .padding(paddingValues)
        ) {
            composable<Routes.Home> { HomeScreenContent() }
            composable<Routes.DailyImage> { DailyImageScreen() }
            composable<Routes.RandomImage> { RandomImageScreen() }
            composable<Routes.RangeImages> { RangeImagesScreen() }
            composable<Routes.FavoriteImages> { FavoritesScreen() }
        }
    }
}
The nested navigation uses a separate NavController and has custom transition animations (fade in/out with 1500ms duration).

Bottom Navigation Bar

The bottom navigation bar handles navigation between main features:
ui/homescreen/HomeScreen.kt
@Composable
fun HomeBottomBar(navController: NavController) {
    // Estado para el item seleccionado
    var selectedItem by remember { mutableIntStateOf(-1) }

    NavigationBar(
        containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
    ) {
        // Home item
        NavigationBarItem(
            selected = selectedItem == 0,
            onClick = {
                if (selectedItem != 0) {
                    selectedItem = 0
                    navController.navigate(Routes.Home) {
                        popUpTo<Routes.Home> { inclusive = false }
                    }
                }
            },
            icon = {
                Icon(
                    imageVector = Icons.Default.Home,
                    contentDescription = "Home",
                    tint = MaterialTheme.colorScheme.onPrimary
                )
            },
            label = { Text(text = "Inicio") }
        )
        
        // Daily Image item
        NavigationBarItem(
            selected = selectedItem == 1,
            onClick = {
                if (selectedItem != 1) {
                    selectedItem = 1
                    navController.navigate(Routes.DailyImage) {
                        popUpTo<Routes.Home> { inclusive = false }
                    }
                }
            },
            icon = {
                Icon(
                    imageVector = Icons.Filled.Today,
                    contentDescription = "Daily Image"
                )
            },
            label = { Text(text = "Diaria") }
        )
        
        // Random Images item
        NavigationBarItem(
            selected = selectedItem == 2,
            onClick = {
                selectedItem = 2
                navController.navigate(Routes.RandomImage) {
                    popUpTo<Routes.Home> { inclusive = false }
                }
            },
            icon = {
                Icon(
                    imageVector = Icons.Filled.Shuffle,
                    contentDescription = "Random images"
                )
            },
            label = { Text(text = "Aleatorias") }
        )
        
        // Range Images item
        NavigationBarItem(
            selected = selectedItem == 3,
            onClick = {
                selectedItem = 3
                navController.navigate(Routes.RangeImages) {
                    popUpTo<Routes.Home> { inclusive = false }
                }
            },
            icon = {
                Icon(
                    imageVector = Icons.Default.DateRange,
                    contentDescription = "Period images"
                )
            },
            label = { Text(text = "Rango") }
        )
        
        // Favorites item
        NavigationBarItem(
            selected = selectedItem == 4,
            onClick = {
                selectedItem = 4
                navController.navigate(Routes.FavoriteImages) {
                    popUpTo<Routes.Home> { inclusive = false }
                }
            },
            icon = {
                Icon(
                    imageVector = Icons.Default.Favorite,
                    contentDescription = "Favorites"
                )
            },
            label = { Text(text = "Favoritos") }
        )
    }
}

Simple Navigation

Navigate to a new screen:
navController.navigate(Routes.DailyImage)
Navigate and clear back stack:
navController.navigate(Routes.Home) {
    popUpTo<Routes.Home> { inclusive = false }
}
This pattern prevents building up a large back stack when navigating between bottom navigation items. It clears everything up to (but not including) the Home route.
Pass navigation actions as callbacks to child composables:
SplashScreen(
    onNavigateToLogin = {
        navHostController.navigate(Routes.Login)
    },
    onNavigateToHome = {
        navHostController.navigate(Routes.Home)
    }
)

Back Navigation Handling

Custom back button behavior:
BackHandler {
    navController.popBackStack(Routes.Home, inclusive = false)
}
Add custom animations to navigation:
NavHost(
    navController = navController,
    startDestination = Routes.Home,
    enterTransition = { 
        fadeIn(animationSpec = tween(1500)) 
    },
    exitTransition = { 
        fadeOut(animationSpec = tween(1500)) 
    }
) {
    // ... composables
}

Fade In

Screens fade in over 1500ms when entering

Fade Out

Screens fade out over 1500ms when exiting

1. Type-Safe Routes

Always use sealed classes with @Serializable for compile-time safety:
// Good - Type-safe
sealed class Routes {
    @Serializable
    data object Home : Routes()
}

// Avoid - String-based routes
const val HOME_ROUTE = "home"

2. Single NavController per Level

Use separate NavController instances for different navigation levels:
  • Top-level: Authentication flow (Splash, Login, SignUp, Home)
  • Nested level: Main features (DailyImage, RandomImage, etc.)

3. Callback-Based Navigation

Pass navigation actions as callbacks rather than passing the NavController:
// Good - Callbacks
@Composable
fun LoginScreen(
    onNavigateToHome: () -> Unit,
    onNavigateToSignUp: () -> Unit
)

// Avoid - Passing NavController
@Composable
fun LoginScreen(navController: NavController)

4. Manage Back Stack

Use popUpTo to prevent back stack buildup:
navController.navigate(Routes.Home) {
    popUpTo<Routes.Home> { inclusive = false }
}

Authentication Flow

Main App Flow

Architecture Overview

See how navigation fits into the overall architecture

MVVM Pattern

Learn how ViewModels work with navigation

Build docs developers (and LLMs) love