A nice detail I’ve noticed in the Compose Navigation library is the usage of strong types.

Let’s take a look at the NavHost composable API.

@Composable
fun NavHost(
    navController: NavHostController,
    graph: NavGraph,
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = {
            fadeIn(animationSpec = tween(700))
        },
    exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = {
            fadeOut(animationSpec = tween(700))
        },
    popEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = enterTransition,
    popExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = exitTransition,
    sizeTransform: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> SizeTransform?)? = null
): Unit

The definition of enterTransition is fantastic in its simplicity. Its receiver and return type are doing a remarkable amount of heavy lifting. I’m interested

enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition

Its return type, EnterTransition, prevents a user from adding a new NavBackStackEntry and using fadeOut to add it to the screen. The code enterTransition = { fadeOut(/* args */ } cannot compile, preventing a category of runtime errors. A far cry from the days of @AnimRes or Animation objects, types that lack the semantic simplicity and richness of

AnimatedContentTransitionScope<*> as a Receiver is also nice. Any functions added are a receiver-scope extension; only accessible in functions with this as a receiver.

These are small details, but they make these APIs incredibly accessible and straightforward to use.