One Off the Slack: Authentication and Navigation

Colton Idle asked:

Is there a “better” way to make sure that all of my screens (looks like my app will likely have over 50+ screens) all handle the case where a user is signed in or out?

example: I only have 4 screens so far (signin, home, account, settings), but I’ve caught myself doing this same exact pattern 3 times now. Replace Screen1 with Screen2 and Screen3 to get an idea for my duplicated code

@Composable
fun Screen1Screen(navController: NavController, viewModel: Screen1ViewModel = hiltViewModel()) {
    if (viewModel.userManagerService.loggedIn) {
        ActualScreen1()
    } else {
        navController.navigate(Screen.SignInScreen.route)
    }
}

Making sure that most screens follow the above pattern seems super error prone.

Google’s Adam Powell offered two pieces of advice, which got a bit lengthy as Adam responded to some follow-on questions from Colton:

  1. avoid calling navigate in composables. Navigate is not idempotent. In general be suspicious of any verbs invoked on any objects that outlive the current recomposition unless those methods are specifically designed to be composition-safe. (e.g. they only mutate snapshot state.) Use the *Effect APIs to create actors in the composition that can take action if present after composition is successful.
  1. You can declare your own extensions on NavGraphBuilder that wrap the content of a destination in some navigation preamble if you like, or simply create something like a SignedInContent composable that accepts a content: @Composable () -> Unit

so once you have a SignedInContent you can do something like declare a

fun NavGraphBuilder.signedInComposable(..., content: ...) {
  composable(...) {
    SignedInContent(..., content)
  }
}

which then means when someone goes to add a screen they’ll wonder why they’re typing composable(... instead of signedInComposable like all of the other destinations in the block

as for mutating actions in composables, one mental model that might help is that the “pure” way to do this without a feedback loop would be to have

if (!signedIn) {
  Column {
    Text("You are logged out")
    Button(onClick = { navigator.navigate("signin") }) {
      Text("Sign in")
    }
  }
}

the sign in action is taken by the user, which mutates state, which triggers recomposition. Nothing too weird.

then you can factor that out into a lambda argument so that you don’t have to know what a navigator is:

Button(onClick = onSignIn) {

(or the right destination, for that matter)

then when you want things to happen as a side effect of something being present in the current composition, you can use the effect apis. The effect creates a sort of actor/robot that stands in for the user clicking a button. Consider the compose hello world counter:

var clicks by remember { mutableStateOf(0) }
Text("Clicked $clicks times")
Button(onClick = { clicks++ }) {
  Text("Click me")
}

but this could just as easily be:

var clicks by remember { mutableStateOf(0) }
Text("Clicked $clicks times")
// A clicker robot!
LaunchedEffect(Unit) {
  while (true) {
    delay(1_000)
    clicks++
  }
}

to put it another way, you shouldn’t “do stuff” during composition, but you can declare the presence of a composable that will “do stuff” for you via an effect once the composition is successful.

Composition declares what is present (and by omission, absent), not things that happen at a moment in time.

You can think of this as being no different from a Button - it’s just a button that knows how to click itself rather than waiting for a user to do it. And it doesn’t have to take up space in layout or draw anything either.

Colton responded:

I’ve been watching some of Ian Lakes videos on navigation and most recently his Q and A with Murat, but somehow I’m still missing a practical example of how this stuff ties together.

For example… the docs show this

@Composable
fun Profile(navController: NavController) {
    /*...*/
    Button(onClick = { navController.navigate("friends") }) {
        Text(text = "Navigate next")
    }
    /*...*/
}

So I thought… maybe it’s okay to just do this on my “top most” screens?

Adam explained:

the key there is that onClick doesn’t happen during composition

the topic of constructing feedback loops in a sound and manageable way is a hot one; I hope to spend some time with some of the others on the team (both eng and devrel) in the coming weeks helping with some guidance around it.

The Slack thread continues on with related questions and concerns — be sure to check it out!


Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!