Skip to main content
Nimaz uses Jetpack Navigation Compose with type-safe routing for navigating between screens. The main navigation graph is defined in NavGraph.kt:
core/navigation/NavGraph.kt
@Composable
fun NavGraph() {
    val navController = rememberNavController()
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentDestination = navBackStackEntry?.destination
    
    // Determine if bottom navigation should be shown
    val showBottomNav = currentDestination?.hierarchy?.any { dest ->
        dest.hasRoute<Route.Home>() ||
        dest.hasRoute<Route.Quran>() ||
        dest.hasRoute<Route.Tasbih>() ||
        dest.hasRoute<Route.QiblaNav>() ||
        dest.hasRoute<Route.More>()
    } == true
    
    Scaffold(
        bottomBar = {
            if (showBottomNav) {
                NavigationBar {
                    bottomNavItems.forEach { item ->
                        NavigationBarItem(
                            icon = { Icon(item.icon, item.label) },
                            label = { Text(item.label) },
                            selected = /* check if selected */,
                            onClick = { /* navigate */ }
                        )
                    }
                }
            }
        }
    ) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = startDestination,
            modifier = Modifier.padding(
                PaddingValues(bottom = innerPadding.calculateBottomPadding())
            )
        ) {
            // Route definitions
        }
    }
}

Type-safe routing

Nimaz uses Kotlin serialization for type-safe navigation:
// Define routes as serializable classes
@Serializable
sealed class Route {
    @Serializable
    data object Home : Route()
    
    @Serializable
    data object Quran : Route()
    
    @Serializable
    data class QuranReader(
        val surahNumber: Int,
        val ayahNumber: Int? = null
    ) : Route()
    
    @Serializable
    data class Tafseer(
        val surahNumber: Int,
        val ayahNumber: Int
    ) : Route()
    
    @Serializable
    data class PrayerTracker(
        val initialTab: Int = 0
    ) : Route()
}

Bottom navigation

The app uses a bottom navigation bar for primary destinations:
core/navigation/NavGraph.kt
private data class BottomNavItem(
    val route: Route,
    val label: String,
    val icon: ImageVector
)

private val bottomNavItems = listOf(
    BottomNavItem(Route.Home, "Home", Icons.Default.Home),
    BottomNavItem(Route.Quran, "Quran", Icons.Default.MenuBook),
    BottomNavItem(Route.Tasbih, "Tasbih", Icons.Default.TouchApp),
    BottomNavItem(Route.QiblaNav, "Qibla", Icons.Default.Explore),
    BottomNavItem(Route.More, "More", Icons.Default.MoreHoriz)
)

Defining routes

Simple route

composable<Route.Home> {
    HomeScreen(
        onNavigateToQuran = { navController.navigate(Route.Quran) },
        onNavigateToSettings = { navController.navigate(Route.Settings) },
        onNavigateToPrayerTracker = { 
            navController.navigate(Route.PrayerTracker())
        }
    )
}

Route with parameters

composable<Route.QuranReader> { backStackEntry ->
    val args = backStackEntry.toRoute<Route.QuranReader>()
    QuranReaderScreen(
        surahNumber = args.surahNumber,
        initialAyahNumber = args.ayahNumber,
        onNavigateBack = { navController.popBackStack() },
        onNavigateToTafseer = { surah, ayah ->
            navController.navigate(Route.Tafseer(surah, ayah))
        }
    )
}
// Simple navigation
navController.navigate(Route.Settings)

// With parameters
navController.navigate(Route.QuranReader(
    surahNumber = 1,
    ayahNumber = 1
))
// Pop current screen
navController.popBackStack()

// Pop to specific destination
navController.popBackStack(
    route = Route.Home,
    inclusive = false
)

Replace navigation

navController.navigate(Route.QuranReader(nextSurah)) {
    popUpTo<Route.QuranReader> { inclusive = true }
}

Single top

Prevent multiple instances of the same destination:
navController.navigate(Route.Settings) {
    launchSingleTop = true
}

Save and restore state

For bottom navigation:
navController.navigate(item.route) {
    popUpTo(navController.graph.findStartDestination().id) {
        saveState = true
    }
    launchSingleTop = true
    restoreState = true
}

Onboarding flow

The app checks onboarding status to determine the start destination:
core/navigation/NavGraph.kt
val onboardingViewModel: OnboardingViewModel = hiltViewModel()
val onboardingState by onboardingViewModel.state.collectAsState()

// Show loading while checking onboarding status
if (onboardingState.isLoading) {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        CircularProgressIndicator()
    }
    return
}

// Determine start destination
val startDestination: Route = if (onboardingState.onboardingCompleted) {
    Route.Home
} else {
    Route.Onboarding
}

// Onboarding route
composable<Route.Onboarding> {
    OnboardingScreen(
        onComplete = {
            navController.navigate(Route.Home) {
                popUpTo(Route.Onboarding) { inclusive = true }
            }
        }
    )
}

Nested navigation

Quran feature

composable<Route.Quran> {
    QuranHomeScreen(
        onNavigateToSurah = { surahNumber ->
            navController.navigate(Route.QuranReader(surahNumber))
        },
        onNavigateToJuz = { juzNumber ->
            navController.navigate(Route.QuranJuz(juzNumber))
        },
        onNavigateToPage = { pageNumber ->
            navController.navigate(Route.QuranPage(pageNumber))
        },
        onNavigateToBookmarks = { 
            navController.navigate(Route.QuranBookmarks)
        }
    )
}

composable<Route.QuranReader> { backStackEntry ->
    val args = backStackEntry.toRoute<Route.QuranReader>()
    QuranReaderScreen(
        surahNumber = args.surahNumber,
        initialAyahNumber = args.ayahNumber,
        onNavigateBack = { navController.popBackStack() },
        onNavigateToTafseer = { surah, ayah ->
            navController.navigate(Route.Tafseer(surah, ayah))
        }
    )
}

Settings feature

composable<Route.Settings> {
    SettingsScreen(
        onNavigateBack = { navController.popBackStack() },
        onNavigateToPrayerSettings = { 
            navController.navigate(Route.SettingsPrayerCalculation)
        },
        onNavigateToNotifications = { 
            navController.navigate(Route.SettingsNotifications)
        },
        onNavigateToAppearance = { 
            navController.navigate(Route.SettingsAppearance)
        }
    )
}

Deep linking

Handle intents from notifications:
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    // Handle prayer notification tap
    handleIntent(intent)
}

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    handleIntent(intent)
}

private fun handleIntent(intent: Intent?) {
    if (intent?.getBooleanExtra(EXTRA_STOP_ADHAN, false) == true) {
        // Stop adhan playback
        AdhanPlaybackService.stopAdhan(this)
    }
}

App restart

Restart the app after settings changes:
core/navigation/NavGraph.kt
private fun restartApp(context: Context) {
    val intent = context.packageManager
        .getLaunchIntentForPackage(context.packageName)
    intent?.addFlags(
        Intent.FLAG_ACTIVITY_CLEAR_TOP or 
        Intent.FLAG_ACTIVITY_CLEAR_TASK or 
        Intent.FLAG_ACTIVITY_NEW_TASK
    )
    context.startActivity(intent)
    if (context is Activity) {
        context.finish()
    }
    exitProcess(0)
}

// Usage in settings screen
composable<Route.Settings> {
    val context = LocalContext.current
    SettingsScreen(
        onRestartApp = { restartApp(context) }
    )
}
ViewModels should not directly navigate. Instead, emit events:
// ViewModel
class QuranViewModel : ViewModel() {
    private val _navigationEvent = MutableSharedFlow<NavigationEvent>()
    val navigationEvent = _navigationEvent.asSharedFlow()
    
    fun onSurahClick(surahNumber: Int) {
        viewModelScope.launch {
            _navigationEvent.emit(NavigationEvent.NavigateToSurah(surahNumber))
        }
    }
}

// Screen
@Composable
fun QuranScreen(
    onNavigateToSurah: (Int) -> Unit,
    viewModel: QuranViewModel = hiltViewModel()
) {
    LaunchedEffect(Unit) {
        viewModel.navigationEvent.collect { event ->
            when (event) {
                is NavigationEvent.NavigateToSurah -> {
                    onNavigateToSurah(event.surahNumber)
                }
            }
        }
    }
}

Build docs developers (and LLMs) love