8

The following code are from the official sample project.

There are two branches, main and end.

I found the Code main and the Code end using different ways to navigate.

Code main is simple and clear, and in other projects, it navigate based State just like Code A which is from the project.

Code end use NavHostController to navigate, but It seems that we need't to use Navigation again when we use Jetpack Compose, right?

Code main

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        var currentScreen by rememberSaveable { mutableStateOf(RallyScreen.Overview) }
        Scaffold(
          ...
        ) { innerPadding ->
            Box(Modifier.padding(innerPadding)) {
                currentScreen.content(
                    onScreenChange = { screen ->
                        currentScreen = RallyScreen.valueOf(screen)
                    }
                )
            }
        }
    }
}

enum class RallyScreen(
    val icon: ImageVector,
    val body: @Composable ((String) -> Unit) -> Unit
) {
    Overview(
        icon = Icons.Filled.PieChart,
        body = { OverviewBody() }
    ),
    Accounts(
        icon = Icons.Filled.AttachMoney,
        body = { AccountsBody(UserData.accounts) }
    ),
    Bills(
        icon = Icons.Filled.MoneyOff,
        body = { BillsBody(UserData.bills) }
    );

    @Composable
    fun content(onScreenChange: (String) -> Unit) {
        body(onScreenChange)
    }
}

Code end

@Composable
fun RallyNavHost(navController: NavHostController, modifier: Modifier = Modifier) {
    NavHost(
        navController = navController,
        startDestination = Overview.name,
        modifier = modifier
    ) {
        composable(Overview.name) {
            OverviewBody(
              ...
            )
        }
        composable(Accounts.name) {
          ...
        }
        composable(Bills.name) {
          ...
        }       
    }
}



enum class RallyScreen(
    val icon: ImageVector,
) {
    Overview(
        icon = Icons.Filled.PieChart,
    ),
    Accounts(
        icon = Icons.Filled.AttachMoney,
    ),
    Bills(
        icon = Icons.Filled.MoneyOff,
    );

    companion object {
        fun fromRoute(route: String?): RallyScreen =
            when (route?.substringBefore("/")) {
                Accounts.name -> Accounts
                Bills.name -> Bills
                Overview.name -> Overview
                null -> Overview
                else -> throw IllegalArgumentException("Route $route is not recognized.")
            }
    }

Code A

fun CraneHomeContent(
   ...
) {
    val suggestedDestinations by viewModel.suggestedDestinations.collectAsState()

    val onPeopleChanged: (Int) -> Unit = { viewModel.updatePeople(it) }
    var tabSelected by remember { mutableStateOf(CraneScreen.Fly) }

    BackdropScaffold(
        ...
        frontLayerContent = {
            when (tabSelected) {
                CraneScreen.Fly -> {
                  ...
                }
                CraneScreen.Sleep -> {
                   ...
                }
                CraneScreen.Eat -> {
                   ...
                }
            }
        }
    )
}
HelloCW
  • 843
  • 22
  • 125
  • 310

3 Answers3

8

I've worked with Compose since the early alpha stages and became quickly disappointed with Google's lame attempt at providing a more modern approach to navigating a single-activity app. When you consider that Android's view-based system was entirely replaced with the declaration approach that Compose uses, you have to seriously wonder why they would stick with a navigation controller that doesn't allow you pass objects from one screen to another. There was also the issue that adding animation when transitioning from one screen to another was an afterthought. There is an add-on that supports animation transitions.

But perhaps the worst thing about Compose was its lack of handling device configuration changes. Under the older view-based system, you defined your layouts in xml files and placed these in resource folders that had qualifiers in the folder name that would aid Android in picking the correct layout based on things like screen density, orientation, screen size, etc. That went out the window with Compose. Eventually Google did add APIs to handle composables that need to be selected based on screen sizes and densities. But ultimately, you end up writing this decision logic within your composable and your code starts to look like spaghetti. Google's Android team completely forgot about the most basic "Separation of Concerns" when they chose to mix UI layouts with the logic that determines which layout gets selected. Designing your composables is one thing and how they get selected is another. Don't mix the two. Your composables become increasingly less reusable when you integrate those APIs. The original developers of Android (who weren't from Google) knew well enough to have the operating system manage the layout selection based upon changes to device configurations.

I chose to create my own framework that handles navigation and manages composables in almost an identical way that the view-based system works. It also remedies the issue of not being able to pass objects from screen to screen. If you are interested, you can check it out at:

https://github.com/JohannBlake/Jetmagic

So to answer you question about whether Compose Navigation is good for navigating, I would have to say no. I have worked with it and have found it severely lacking. If you want to be just-another-run-of-the-mill-Android-developer, then use Compose Navigation. But if you want to chart your own path and liberate yourself from Google's poor design practices, then use Jetmagic or even better, create your own navigation framework. It isn't that hard.

Johann
  • 27,536
  • 39
  • 165
  • 279
  • Thanks! Are the Code main and the Code A good ways for navigating? – HelloCW Jan 04 '22 at 12:35
  • I can't recommend either of those because they have no support for configuration settings like screen density/size or orientation. How do you plan on supporting that in your app when navigating? As I mentioned in my response, if you include that in your composables, your composables end up looking like spaghetti code and are less reusable. Also, I don't see any clean way of passing dynamic parameters from one screen to another. Many screens in an app often require a different set of parameters. – Johann Jan 04 '22 at 15:58
3

To answer your question: Code Main is not the right way to navigate.

This is an example to get started. In that case you simply hide and show Composables that's it.

Why is it better to use navigation?

  1. Lifecylce is being handled better. Imagine you want to start a cameraX compasable screen and then you want to return back to your initial composable. Navigation component will handle and release the resources.

  2. Composable are added to the back stack. So when you press back it automatically returns to previous screen composable.

  3. You get this nice animation and you do not just see the next screen instantly.

I am sure there are other points as well but those are some that came up to my mind right now....

F.Mysir
  • 2,838
  • 28
  • 39
  • Thanks! It seems that in most case I need only one screen which can recompose different UI based the Status changed when I use Jetpack Compose, just like `Code Main` and `Code A`. And more I think `Code Main` and `Code A` can do the same thing as Jetpack Compose Navigation except for deep link. Is my thinking right? – HelloCW Jan 05 '22 at 06:09
  • BTW, it seems that `Code Main` and `Code A` don't support back stack, I can't return previous UI by pressing back key right? – HelloCW Jan 05 '22 at 08:54
  • Yes this was my second point. Deep link is another reason... Try to think the first option as `visibility: gone` from xml this is it. – F.Mysir Jan 05 '22 at 09:47
  • Thanks! What does "visibility: gone from xml this is it" mean ? I think it's different between Jetpack Compose and xml layout UI, Jetpack Compose repaint UI automatically based the Status changed, and it doesn't control to display or hide XML layout for different UI based instruction just like before. – HelloCW Jan 05 '22 at 12:58
  • I do not understand what you mean either. Navigation is the proper way to go. Other ways you just hide and reveal composables (I gave you an example as to what could correspond to the XML system). The basic downsides are the one I mentioned. I hope you found my answer a little helpful... – F.Mysir Jan 05 '22 at 15:21
1

It depends on the specific use case you want to implement. In general, yes, any sort of navigation library is more suitable for navigation. It is literally in the name. Most navigation libraries, including Navigation Component, provide such essentials as:

  1. storing the whole navigation history (the backstack) and the saved state (rememberSaveable) of every entry in the backstack. The same destination may be several times in the backstack, but they would be stored as different backstack entries. This allows storing the separate saved states for each of them.

  2. providing separate scoped Lifecycle, ViewModelStore and SavedStateRegistry for each of them. This may be useful for allocating/cleaning up resources while the destination is visible or while it is in the backstack.

Doing the simple switching between destinations as in your Code main or Code A MAY be a valid option, but it lacks the functionality mentioned above. All your destinations use the same Lifecycle, ViewModelStore and SavedStateRegistry of the parent composable. Also, rememberSaveable wouldn't save state when switching between destinations. In some cases it might be fine, but you should be aware of the difference.

On the question whether Navigation Component for Compose is good for navigation? It's fine, but there are flaws and complications. You may read a good article by CommonsWare on the topic: Navigating in Compose: Criteria

Considering all the weirdness of the official library, I've even crafted my own navigation library: Compose Navigation Reimagined

olshevski
  • 4,971
  • 2
  • 35
  • 34