Blog Infos

If you’re reading this article, you’re probably thinking:

“Hmm I wonder what to use to navigate in my Jetpack Compose app. Should I use navigation-compose as sugested by Google or stick with Fragments and use Compose only for rendering views?”

In this article, I’m going to talk about my experience with both Navigation-Compose and Fragments in a Jetpack Compose app. You will learn which approach is better (in my opinion) and what obstacles may await you by using each of them.


If you’re a Polish speaker, you can listen to a podcast I’ve made about this topic:

Let’s start

If you Google “Jetpack Compose Navigation” you will probably see the Navigation-Compose library as one of the first results. Google describes it as a new navigation component that supports apps written in Compose and claims that thanks to it we can navigate between composables while taking advantage of the Navigation component’s infrastructure and features.

Sounds perfect, but is it? We should talk about what the new navigation looks like…

Google really wanted to create a Framework that would be able to manage lifecycles, navigation and everything around it while eliminating the need to use Fragments. They came up with the idea to create a navigation graph as Composable (NavHost) instead of an XML file, where we can define routes to which the application can navigate. In fact, if you look closely, Fragments are replaced by navBackstackEntry in this way. Unfortunately, it doesn’t work very well and there are a lot of issues with it.

Route paths

First of all — route paths.

Google probably envied Flutter, React and other similar Frameworks and thought that the best solution for Android would be to define screen routes as in a web app — despite the fact that we create mobile applications, not web ones, right?

What does this mean for us?

This means that each route looks like an URL ending, e.g.

  • /users — for the user list screen
  • /users/2 — for the detail screen for user with id = 2.

From now on, the developer must maintain the order of the paths and their parts, remember them and their params, or create constants for individual screens, which already means that we have a lot more work to start than before.

What comes next is even more confusing.

Navigation arguments

Earlier, using Fragments and Safe Args, we could define in XML what arguments a given Fragment takes. When creating an action from one screen to another, we had an automatically generated code that asked us for arguments of the correct type and packed them into the Bundle. Next, to extract these arguments in the target Fragment, we can use the by navArgs() delegate for example, which will do it for us automatically.

We were able to pass a lot of different types as parameters, such as Int, String, Boolean, etc. but also custom ones, such as Enums, Serializables and Parcelables. The full list of supported types can be found here.

Is it just as convenient in navigation compose? NOT AT ALL

Arguments can be passed only in the URL path of a given route, for example:

  • Path params
  • Or to make it even more complicated, you can also pass query params and optional parameters after the question mark

Oh, and don’t forget that for each destination and param that you want to use, YOU are responsible for all of the type safety. Compose navigation is not going to tell you that you mistook an Int param as a String. It will just crash the app.

Each param has to be declared in composable in the NavHost:

NavHost(startDestination = "profile/{userId}") {
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {...}

And also extracted with a proper type later:

arguments = listOf(navArgument("userId") { defaultValue = "me" })
) { backStackEntry ->
Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->

So now, not only that the programmer has to manage all routes himself, he also has to remember what arguments are taken by a particular screen and what are the keys and data types of parameters and their order.

Will you get the errors at compile time that you messed something up? Nope. You will get an error or an app crash only while the app is running.

Everything is a String

Also, don’t forget that since the screen route is like an url, each parameter must be passed anyway as a String and must be encoded.

Let’s say we want to pass this String as a param:

val urlParam = ""

To navigate with it we have to write this:


And then receive it like this:


Job Offers

Job Offers

There are currently no vacancies.



Navigation superpowers at your fingertips

This talk will begin by the demonstration of a beautiful sample app built with Compose Mulitplatform and Appyx, complete with:
Watch Video

Navigation superpowers at your fingertips

Zsolt Kocsi
Principal Android engineer

Navigation superpowers at your fingertips

Zsolt Kocsi
Principal Android en ...

Navigation superpowers at your fingertips

Zsolt Kocsi
Principal Android enginee ...


Even better, if you use URLEncoder.encode(…) over a String that contains a ‘\n’ character, it will crash because of the ‘%0A’, so the only way to make it work is to use Base64 encoding first.

This makes the Navigation-Compose API completely NON type safe.

What about Enums, Serializables and Parcelables?

Like I’ve mentioned before, any argument we want to pass, regardless of type, has to be converted to a String in order to add it to the path.


Compose navigation won’t let you pass Enums as params, but this can be done differently. In theory, we can convert it into a String, and call the valueOfmethod on the target screen to find the value in the enum that we previously passed as String.

Parcelables and Serializables

This is where the fun begins. Passing Serializable and Parcelable as a navigation param in Compose-Navigation is not supported too.

I see a lot of people on the Internet saying that it is possible to get into the backStackEntry arguments and manually pass the object there that extends Serializable or Parcelable.

This is just a hack that Google does not recommend and does not give any guarantee that this will work.

Personally, I have had a lot of situations where passing arguments in this way caused the app to crash.

Don’t believe me? Just go here and read:

Is there anything we can do?

Well, theoretically… yeah. We could convert our Serializable or Parcelable object to JSON and pass it as a String. I feel sorry for anyone who would like to pass params this way. This is a way to do it, but it’s so stupid and brings so much unnecessary boilerplate and complexity that I won’t even consider using it.


Since Compose version 1.0.3 and NavigationX 2.4.0-alpha10 we are now able to create a custom NavType:

Let’s say you have a class like this:

data class Example(val id: String, val name: String) : Parcelable

You can define the NavType like this:

class ExampleNavType : NavType<Example>(isNullableAllowed = false) {
override fun get(bundle: Bundle, key: String): Example? {
return bundle.getParcelable(key)
override fun parseValue(value: String): Example {
return Gson().fromJson(value,
override fun put(bundle: Bundle, key: String, value: Example) {
bundle.putParcelable(key, value)

And use it:

NavHost(...) {
composable("home") {
onClick = {
val example = Example("1", "Example")
val json = Uri.encode(Gson().toJson(example))
arguments = listOf(
navArgument("example") {
type = ExampleNavType()
) {
val example = it.arguments?.getParcelable<Example>("example")

This seems better than what we had before BUT still requires us to use JSON inside. Also imagine creating a new custom NavType every time you want to pass a Parcelable. This just adds more boilerplate code to our app. Moreover there is no mention in the documentation from Google that this solution will work with Compose-Navigation too.

But WHY?

And now the main question arises — why?

Why is Navigation-Compose like that? Why doesn’t it support Serializables, Parcelables and why does it have so many issues?

It is very possible that Google wants to encourage developers to pass only id of the object and not the static copy of it. It would make sense if, for example, we were operating on the Room database and observing the data using Flow. For example, if something in the meantime changed this data, then the user would automatically see the current values on the screen.

Indeed, maybe in a few cases like this, it would make sense. Unfortunately, there are often situations in which we want to consciously pass a static object, or at least some part of it so that the user immediately sees the data on the screen. After that, we could load some details in the background or simply check if the data is up-to-date.

Just use Fragments

Okay, then how should I use Compose to avoid these problems? The answer is simple. Use Fragments and old navigation.

Reasons why you should still use Fragments

You can use whatever you want: XML, Compose, you choose. The easiest way is to add ComposeView to the Fragment view and set Composable of the given Fragment there.

Not a fan of LaunchedEffect, DisposableEffect and so on? You don’t have to use them as you can write some code the old way, just like it was in Fragments.

Depending on what DI Framework you are using, you may also run into some issues with SavedStateHandle if you use Navigation-Compose.
In my current project, we are using Koin and unfortunately SavedStateHandle cannot be injected into the ViewModel in the new navigation. This problem, of course, has to be fixed by Koin Team, but for now, it disappears if you use Fragments and inject the ViewModel using by viewModel() delegate.

Of course, Fragments aren’t perfect. What issues with them should you consider in this case?

First of all, you are using Fragments. This may seem funny at first, but some people don’t like them, for example Jake Wharton. In short, he says you can use Fragments, but the backstack is a nightmare.

You have a little more code. After all, for each screen, you have to write both Fragment and Composable.

Another thing is that you have two lifecycles that you need to manage — Fragment and Composable lifecycle. This may seem problematic at first, but fortunately, there is a quick way to deal with it. Just use the setViewCompositionStrategy method on a ComposeView and set how the Composable’s lifecycle should behave in relation to the Fragment’s lifecycle.

Last but not least, you must remember that when passing a Parcelable object as a Safe Args argument, you must also add it to the proguard configuration, otherwise you will get a crash on production.


So is using Fragments the best solution? Probably not.

In my opinion, it would be best if Google provided us with a ready, working and good API for navigation in Compose. Unfortunately, as I mentioned before, we can forget about it for now. At the moment, the solution that I presented is something that I use myself and I’m OK with it, at least for now.

We should also remember that many libraries and tools do not yet have 100% support with Compose and sometimes the ability to write something in the old way, in a Fragment or even in XML is exactly what we need.

Thanks for reading!

That’s it for this article. I hope you like it and should you have any questions, do not hesitate to comment or reach out to me 🙂

This article was originally published on on February 13, 2022



It’s one of the common UX across apps to provide swipe to dismiss so…
In this part of our series on introducing Jetpack Compose into an existing project,…
In the world of Jetpack Compose, where designing reusable and customizable UI components is…

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.