Blog Infos
Author
Published
Topics
, , , ,
Published

As part of a recent project, I decided to utilize Jetpack Compose for my view layer entirely. While Google’s Getting Started examples for the UI are fairly simple, you quickly reach a point when you want to navigate between different screens (or Composables). Although Google also has you covered here with a Compose-component of its navigation library, what Google does not provide is a holistic view, so I want to share some lessons learned with you.

In the beginning there was l̵i̵g̵h̵t̵ startActivity(AnyActivity::class.java) whenever we wanted to show a new screen, alternatively you would use fragments with the FragmentManager and FragmentTransactions, manage the backstack(s), remember to use the ChildFragmentManager, too, whenever you had to, remember there is an “old” FragmentManager and a SupportFragmentManager that you could mix up etc. Google decided that this sucks and developed the navigation component, giving us navigation graphs and a NavController that has all the power of previous times combined.

Let’s follow the tutorial on the Compose navigation component, use the Compose version of the new NavController, and we quickly have something like this:

class HomeActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val navController = rememberNavController()

            MyTheme {
                Scaffold {
                    NavigationComponent(navController)
                }
            }
        }
    }
}

@Composable
fun NavigationComponent(navController: NavHostController) {
    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        composable("home") {
            HomeScreen(navController)
        }
        composable("details") {
            DetailScreen()
        }
    }
}

@Composable
fun HomeScreen(navController: NavController) {
    Button(onClick = { navController.navigate("detail") }) {
        Text(text = "Go to detail")
    }
}

@Composable
fun DetailScreen() {
    Text(text = "Detail")
}

We created a simple NavHost with two routes, home and detail, where the home screen has a button to go to the detail screen, each consisting of a simple text field.

When you reach a certain point, you will want to introduce some more complex logic, and this is typically done with ViewModels from the Android Architecture package. This is explained in detail here. Fortunately, they also explained how to connect this with the navigation component from earlier, which is described here.

Let’s create a ViewModel following the tutorials for our detail screen and get the text to display from it, while also providing it from our navigation component:

@Composable
fun NavigationComponent(navController: NavHostController) {
    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        composable("home") {
            HomeScreen(navController)
        }
        composable("details") {
            DetailScreen(viewModel())
        }
    }
}

@Composable
fun DetailScreen(viewModel: DetailViewModel) {
    Text(text = viewModel.getDetailText())
}

class DetailViewModel : ViewModel() {

    fun getDetailText(): String {
        // some imaginary backend call
        return "Detail"
    }
}

In contrast with the “ancient times”, where we retrieved a ViewModel within an activity or fragment and it was pretty obvious when ViewModel.onCleared() was called, being that it was tied to the lifecycle of the activity/fragment, when is it now called?

Regardless of whether you use viewModel() or with Hilt hiltViewModel() to retrieve your ViewModel, they both will call onCleared() when the NavHost finishes transitioning to a different route. So whenever you navigate to another Composable, it will be cleaned up. This is achieved by defining a DisposableEffect on the navigation route when the NavHost is created and you can mimic the behavior even if you’re not using the navigation library and need to clean up the ViewModel yourself.

With the introduction of ViewModels our detail screen now needs a DetailViewModel instance as an input. So, if you defined a Preview anywhere, it is broken now.

@Preview
@Composable
fun DetailScreenPreview() {
    DetailScreen(viewModel = ??)
}

After reading into this problem I found this Slack conversation with Google’s Jim Sproch:

Best practice is probably to avoid referencing AAC ViewModels in your composable functions.

Oh.. so, great, let’s forget all the examples we read in the tutorials and refactor our composable functions:

@Composable
fun NavigationComponent(navController: NavHostController) {
    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        composable("home") {
            HomeScreen(navController)
        }
        composable("details") {
            val viewModel = viewModel<DetailViewModel>()
            DetailScreen(viewModel::getDetailText)
        }
    }
}

@Composable
fun DetailScreen(textProvider: () -> String) {
    Text(text = textProvider())
}

@Preview
@Composable
fun DetailScreenPreview() {
    DetailScreen { "Sample text" }
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Testing: how hard can it be?

When people start looking into the testing domain, very similar questions arise: What to test? And more important, what not? What should I mock? What should I test with unit tests and what with Instrumentation?…
Watch Video

Testing: how hard can it be?

DANNY PREUSSLER
Android Lead
Soundcloud

Testing: how hard can it be?

DANNY PREUSSLER
Android Lead
Soundcloud

Testing: how hard can it be?

DANNY PREUSSLER
Android Lead
Soundcloud

Jobs

The idea is that your composable functions only takes low level inputs, like lambdas, LiveData or a Flow (which you might need if want to work with a state). This actually also enables us, now, to easily preview different texts 🎉.

Remember our initial screen which we created exactly like shown in the navigation tutorial and passed the NavController to it so we are able to navigate to the detail screen?

@Composable
fun HomeScreen(navController: NavController) {
    Button(onClick = { navController.navigate("detail") }) {
        Text(text = "Go to detail")
    }
}

In order to create a preview for this we would need to provide a value for NavController obviously. You don’t have a mock version handy you say..? How to actually connect this now for the case the ViewModel asks to navigate to some other screen?

My recommendation is to move any navigation logic out of your composable functions. My suggestion is to create a middle layer for navigation:

class Navigator {

    private val _sharedFlow = 
      MutableSharedFlow<NavTarget>(extraBufferCapacity = 1)
    val sharedFlow = _sharedFlow.asSharedFlow()

    fun navigateTo(navTarget: NavTarget) {
        _sharedFlow.tryEmit(navTarget)
    }

    enum class NavTarget(val label: String) {

        Home("home"),
        Detail("detail")
    }
}

Instead of Kotlin’s SharedFlow you’re of course free to use whatever you’d like. Pass the singleton reference to your ViewModels and whenever you want to navigate to another screen simply call the navigateTo() function.

The last step is to actually navigate to a different screen, which will be done inside our composable NavigationComponent function from the beginning:

@Composable
fun NavigationComponent(
  navController: NavHostController, 
  navigator: Navigator
) {
    LaunchedEffect("navigation") {
        navigator.sharedFlow.onEach {
            navController.navigate(it.label)
        }.launchIn(this)
    }
    
    NavHost(
        navController = navController,
        startDestination = NavTarget.Home.label
    ) {
        ...
    }
}

With LaunchedEffect we create a CoroutineScope that is started as soon as our composable component is created and canceled as soon as the composition is removed. As a result, whenever Navigator.navigateTo() is called, this snippet listens to it and performs the actual transition.

Thanks for reading 🙂

Thanks to Mohsen Bahman.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
Menu