Navigation System in NowInAndroid App
System Design
Navigation System in NowInAndroid App
Akshay Nandwana
January 6, 2025
5 min read
70 views

Navigation is a fundamental aspect of any Android application, especially in modern apps built with Jetpack Compose. In this blog, we will dive deep into the implementation of a navigation architecture that is modular, scalable, and easy to extend. The code we'll explore covers features such as top-level destinations, modular navigation for individual features, and deep links for specific resources.

This step-by-step guide will help you understand the architecture and how you can build a similar system for your application.

A big thanks to NowInAndroid Repository for providing the code example used in this blog, which was taken from their open-source project.

Understanding the Architecture

The provided implementation is structured to:

  1. Define Top-Level Destinations: Each top-level destination represents a primary section of the app.

  2. Utilize Modular Navigation: Each feature module handles its own navigation, making the app easier to maintain and scale.

  3. Handle Deep Links: Deep links allow users to navigate to specific screens directly from notifications or external links.

Step 1: Defining Top-Level Destinations

The TopLevelDestination enum defines the primary sections of the app:

kotlin
enum class TopLevelDestination(
    val selectedIcon: ImageVector,
    val unselectedIcon: ImageVector,
    @StringRes val iconTextId: Int,
    @StringRes val titleTextId: Int,
    val route: KClass<*>,
    val baseRoute: KClass<*> = route,
) {
    FOR_YOU(
        selectedIcon = NiaIcons.Upcoming,
        unselectedIcon = NiaIcons.UpcomingBorder,
        iconTextId = forYouR.string.feature_foryou_title,
        titleTextId = R.string.app_name,
        route = ForYouRoute::class,
        baseRoute = ForYouBaseRoute::class,
    ),
    BOOKMARKS(
        selectedIcon = NiaIcons.Bookmarks,
        unselectedIcon = NiaIcons.BookmarksBorder,
        iconTextId = bookmarksR.string.feature_bookmarks_title,
        titleTextId = bookmarksR.string.feature_bookmarks_title,
        route = BookmarksRoute::class,
    ),
    INTERESTS(
        selectedIcon = NiaIcons.Grid3x3,
        unselectedIcon = NiaIcons.Grid3x3,
        iconTextId = searchR.string.feature_search_interests,
        titleTextId = searchR.string.feature_search_interests,
        route = InterestsRoute::class,
    ),
}

Key Points:

  • Each destination has icons for selected and unselected states.

  • Text resources are associated for accessibility and titles.

  • Each destination specifies its route and optional base route.

This structure ensures that top-level navigation is clearly defined and easily extendable.

Step 2: Creating the Navigation Host

The NiaNavHost function is the heart of the navigation system. It uses Jetpack Compose's NavHost to define the navigation graph:

kotlin
@Composable
fun NiaNavHost(
    appState: NiaAppState,
    onShowSnackbar: suspend (String, String?) -> Boolean,
    modifier: Modifier = Modifier,
) {
    val navController = appState.navController
    NavHost(
        navController = navController,
        startDestination = ForYouBaseRoute,
        modifier = modifier,
    ) {
        forYouSection(
            onTopicClick = navController::navigateToTopic,
        ) {
            topicScreen(
                showBackButton = true,
                onBackClick = navController::popBackStack,
                onTopicClick = navController::navigateToTopic,
            )
        }
        bookmarksScreen(
            onTopicClick = navController::navigateToInterests,
            onShowSnackbar = onShowSnackbar,
        )
        searchScreen(
            onBackClick = navController::popBackStack,
            onInterestsClick = { appState.navigateToTopLevelDestination(INTERESTS) },
            onTopicClick = navController::navigateToInterests,
        )
        interestsListDetailScreen()
    }
}

Key Points:

  • NavHost: Centralizes navigation for the app.

  • Each section—forYouSection, bookmarksScreen, searchScreen—handles its own navigation logic.

  • Encapsulation: Each feature is isolated, making it easier to manage individual navigation logic.

Step 3: Modular Navigation for Features

Each feature module defines its own navigation. For example, the Bookmarks feature:

kotlin
@Serializable object BookmarksRoute

fun NavController.navigateToBookmarks(navOptions: NavOptions) =
    navigate(route = BookmarksRoute, navOptions)

fun NavGraphBuilder.bookmarksScreen(
    onTopicClick: (String) -> Unit,
    onShowSnackbar: suspend (String, String?) -> Boolean,
) {
    composable<BookmarksRoute> {
        BookmarksRoute(onTopicClick, onShowSnackbar)
    }
}

Key Points:

  • BookmarksRoute: Defines the route for the bookmarks screen.

  • NavGraphBuilder.bookmarksScreen: Encapsulates the composable and its associated logic.

  • The feature's navigation is self-contained, ensuring modularity.

Similarly, the ForYou feature:

kotlin
fun NavGraphBuilder.forYouSection(
    onTopicClick: (String) -> Unit,
    topicDestination: NavGraphBuilder.() -> Unit,
) {
    navigation<ForYouBaseRoute>(startDestination = ForYouRoute) {
        composable<ForYouRoute>(
            deepLinks = listOf(
                navDeepLink {
                    uriPattern = DEEP_LINK_URI_PATTERN
                },
            ),
        ) {
            ForYouScreen(onTopicClick)
        }
        topicDestination()
    }
}
  • navDeepLink: Enables navigation to a specific screen using a URI pattern.

Step 4: Extending the Navigation System

To add a new feature module:

  1. Create a Route Object: Define a Serializable route object for the new module.

  2. Extend the NavGraphBuilder: Add a function to encapsulate the navigation logic for the feature.

  3. Update the NavHost: Add the new feature to the NiaNavHost.

For example, adding a Profile feature:

kotlin
@Serializable object ProfileRoute

fun NavController.navigateToProfile(navOptions: NavOptions) =
    navigate(route = ProfileRoute, navOptions)

fun NavGraphBuilder.profileScreen(onLogout: () -> Unit) {
    composable<ProfileRoute> {
        ProfileScreen(onLogout)
    }
}

Benefits of This Architecture

  1. Modularity:

    • Each feature manages its own navigation.

    • Easy to add or remove features without affecting the core navigation logic.

  2. Scalability:

    • The architecture can handle multiple features and complex navigation flows.

  3. Maintainability:

    • Encapsulated logic makes it easier to debug and test individual features.

  4. Deep Link Support:

    • Users can navigate directly to specific screens from notifications or external sources.

Conclusion

This navigation architecture leverages Jetpack Compose’s powerful NavHost and Kotlin's modularity to create a system that is robust, scalable, and maintainable. By encapsulating navigation logic within individual modules, the architecture promotes clean code and reduces the risk of breaking changes.

Akshay Nandwana
Founder AndroidEngineers

You can connect with me on:


Book 1:1 Session here
Click Here

Join our upcoming classes
https://www.androidengineers.in/courses

Share This Article
Stay Updated

Get the latest Android development articles delivered to your inbox.