Compose and Conquer: A Tale of Effortless Android Navigation

James Cullimore
Level Up Coding
Published in
8 min readJan 10, 2024

--

In Android app development, efficient navigation is critical to offering a seamless user experience. Prior to the introduction of Jetpack Navigation, developers frequently struggled with the complexities of navigating between activities and fragments, relying on complex frameworks and boilerplate code. The time-consuming nature of this method frequently resulted in tangled navigation graphs, which hampered the general maintainability of Android applications.

In the pre-Jetpack Navigation era, navigating between activities demanded intricate handling of Intent objects, resulting in verbose and error-prone code. Similarly, navigating within fragments required developers to manage FragmentTransactions, adding complexity to the navigation flow. These challenges prompted the need for a more streamlined solution, paving the way for the introduction of Jetpack Navigation.

Jetpack Navigation simplifies the navigation paradigm by providing a comprehensive framework for navigating between destinations in an Android app. It abstracts away the complexities of fragment transactions and activity intents, offering a declarative approach to navigation. With Jetpack Navigation, developers can construct a clear navigation graph, defining the flow of their application in a visually intuitive manner. This not only enhances code readability but also promotes modularization and maintainability.

As the Android ecosystem evolves, so does Jetpack Navigation. The library seamlessly integrates with Jetpack Compose, the modern UI toolkit for building native Android UIs. This integration opens up new possibilities for navigation within Jetpack Compose applications, offering a cohesive experience for developers working on the latest Android projects. Furthermore, Jetpack Navigation provides robust support for passing data between destinations, allowing developers to effortlessly handle arguments during navigation. This article delves into the multifaceted world of Jetpack Navigation, exploring its evolution and demonstrating how it addresses the challenges associated with navigating activities, fragments, and Jetpack Compose applications while emphasizing the significance of passing data in a seamless and maintainable manner.

Activities & Intents

Navigating between activities in Android traditionally involves the use of Intents, a mechanism for expressing an intention to perform an action. While this approach served its purpose, it often led to a proliferation of activities, resulting in a less modular and maintainable codebase. Each activity, being a heavyweight component, encapsulates its own lifecycle, UI, and logic, contributing to an application’s bloat. As a remedy, developers have increasingly turned to more efficient alternatives, such as fragments and Jetpack Compose, to achieve a more streamlined and modular architecture.

In the snippet below, we showcase the classic method of navigating from one activity to another using an Intent and passing data between them:

// Navigating from one activity to another
val intent = Intent(this@YourCurrentActivity, UserActivity::class.java)
intent.putExtra("USER_ID", userId)
startActivity(intent)

Here, we’re navigating to the UserActivity and passing a user ID as an extra parameter. While this approach is straightforward, it contributes to the proliferation of activities, each handling a specific aspect of the application flow. This can lead to a monolithic structure, making it challenging to maintain and extend the codebase over time.

To retrieve the passed data in the destination activity, the following code snippet demonstrates how to extract the user ID:

// Retrieving USER_ID in the destination activity
val userId = intent.getStringExtra("USER_ID")

While activities can serve certain use cases well, it’s generally recommended to minimize their usage, especially for tasks that can be efficiently handled by fragments or Jetpack Compose. These alternatives offer a more modular and flexible approach to UI development in Android, enabling developers to create richer, more maintainable applications. By embracing these alternatives, developers can navigate through their application with greater ease, avoiding the pitfalls associated with the heavyweight nature of activities.

Fragment Transactions

Navigating between fragments in Android without utilizing Jetpack Navigation often involves directly manipulating the FragmentManager and FragmentTransactions. While this approach grants developers granular control over the backstack and fragment transactions, it comes with its own set of complexities and potential pitfalls. The code snippet below illustrates how to replace a fragment and add it to the backstack:

In this example, we replace the existing fragment with an instance of ExampleFragment and add it to the backstack with a specified name. This technique allows developers to manage the backstack manually, providing fine-grained control over the navigation history.

To pass data between fragments, developers can use the setFragmentResult method, as demonstrated in the code snippet below:

button.setOnClickListener {
val result = "result"
// Use the Kotlin extension in the fragment-ktx artifact.
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}

This snippet shows how to set a fragment result, attaching a bundle containing relevant data. To receive the result, the destination fragment can register a result listener in its onCreate method:

While this manual approach to fragment navigation allows for greater control over the backstack and transactions, it introduces complexities such as potential memory leaks, increased boilerplate code, and the need for careful management of the fragment lifecycle. Additionally, the absence of a declarative navigation graph makes it challenging to visualize and maintain the navigation flow, especially in larger applications. As such, while this method may be suitable for specific scenarios requiring fine-tuned control, developers should carefully weigh the trade-offs and consider adopting Jetpack Navigation for a more streamlined and maintainable navigation solution.

Jetpack Navigation

Navigating between fragments with Jetpack Navigation introduces a declarative and visually intuitive approach to managing the navigation flow in Android applications. One of the key advantages is the use of a navigation graph (nav_graph.xml), which acts as a centralized representation of the app’s navigation structure. This graph allows developers to define fragment destinations and the associated actions between them. Additionally, it provides a clear overview of the entire navigation flow, enhancing code readability and maintainability.

In the snippet below, an excerpt from a nav_graph.xml file demonstrates how to declare a fragment with an associated argument:

Here, we define a fragment called myFragment with an argument named myArg of type integer and a default value of 0.

To facilitate navigation to myFragment with a specific argument, the following action is declared in the navigation graph:

This action, named startMyFragment, is associated with the destination myFragment and specifies an argument myArg with a default value of 1.

In the code snippet below, we demonstrate how to navigate to myFragment and pass data using safe args in a fragment:

Here, SpecifyAmountFragmentDirections.confirmationAction is an automatically generated class by the Safe Args plugin, providing a type-safe way to pass data between fragments. The amount argument is set, and the navigation is triggered.

To receive the data in the destination fragment, the args property is used:

The navArgs() delegate simplifies the retrieval of arguments, ensuring type safety and reducing boilerplate code.

In summary, Jetpack Navigation streamlines fragment navigation through a visual representation in the navigation graph and leverages safe args for a type-safe and concise way to pass and receive data between fragments. This approach enhances code quality, reduces errors, and promotes a more maintainable and scalable architecture for Android applications.

Navigating with Compose

Jetpack Compose Navigation brings a breath of fresh air to the navigation landscape in Android development, offering a remarkably simple and intuitive way to handle navigation within Compose-based applications. A key advantage lies in the integration of the Jetpack Compose Navigation library, which aligns seamlessly with the principles of Compose, promoting a more declarative and concise approach to building UIs.

To begin, developers can include the necessary dependency in their project, as demonstrated in the code snippet below:

dependencies {
def nav_version = "2.7.6"
implementation "androidx.navigation:navigation-compose:$nav_version"
}

With the library integrated, developers can initialize a NavController using the rememberNavController() function. The NavHost then serves as the container for defining the application’s navigation graph, specifying the start destination and composing various destinations:

In this example, the NavHost is configured with a start destination of “profile/{userId}” and additional composable destinations, including one that takes a dynamic argument, userId. The use of a navigation graph, reminiscent of the Jetpack Navigation for XML-based navigation, contributes to a clear and visually intuitive representation of the app’s navigation flow.

Navigating to a destination is as simple as invoking the navigate function on the NavController, as illustrated in the following code snippet:

navController.navigate("profile/user1234")

One of the strengths of Jetpack Compose Navigation is its integration with ViewModels and the ability to seamlessly pass and retrieve data. In the ViewModel example below, a UserViewModel takes advantage of the SavedStateHandle to retrieve the userId argument:

This approach ensures that the ViewModel has access to the necessary data, promoting a separation of concerns and adhering to best practices in Android architecture.

In conclusion, Jetpack Compose Navigation simplifies the navigation paradigm in Android, embracing the principles of Jetpack Compose and providing a powerful yet elegant solution for building modern UIs. The seamless integration with ViewModels and the familiar navigation graph structure make it a preferred choice for developers seeking a straightforward and maintainable approach to navigation in Compose-based applications.

Conclusion

The journey into Jetpack Compose Navigation has been a compelling exploration of Android’s evolving navigation landscape. Admittedly, the initial adjustment posed its challenges, as any paradigm shift would. However, as I delved deeper into the intricacies of Compose Navigation, it became evident that the paradigm offers a refreshingly slim and intuitive approach to crafting navigation flows in modern Android applications.

The simplicity of Jetpack Compose Navigation shines through its seamless integration with Jetpack Compose, aligning perfectly with the declarative nature of Compose UI development. The ability to effortlessly navigate through various screens and pass data with concise code has streamlined the navigation process significantly. This newfound simplicity not only enhances the development experience but also contributes to the creation of more maintainable and scalable applications.

Despite the merits of Jetpack Compose Navigation, there remains a personal preference for the familiar comfort of the navigation graph, a stalwart companion in Android development for years. The comprehensive separation of concerns afforded by the navigation graph, combined with its visual representation of the entire app’s navigation flow, continues to be a compelling choice. Perhaps it’s the years of familiarity or the comfort derived from its widespread adoption, but for now, the navigation graph stands as a preferred tool in the arsenal.

As the Android ecosystem continues to evolve, so too will the developer’s toolkit. The choice between Jetpack Compose Navigation and the trusted navigation graph may ultimately come down to personal workflow preferences. Nevertheless, the journey into the world of Compose Navigation has been an enlightening one, showcasing the adaptability and resilience of the Android development community in embracing innovative solutions for crafting the next generation of user interfaces.

--

--