kotlinmvvmandroid-jetpack-composekotlin-multiplatformcompose-desktop

How to deal with Compose Desktop MenuBar


Welcome all

I want to add a windows top menu bar, like in windows applications. I found MenuBar, but... how to deal with navigation or logic in general in MenuBar? All the screens of my application and navigation logic are on my commonMain module.

Navigation logic needs to be also present on my commonMain screens because they can also navigate between them with buttons, the same navigation that must have the MenuBar should be shared between MenuBar and the screens buttons navigation. This is like when you access the "Window" or "Screen" sections of a windows app top menu bar, they allow you navigate, but you also can switch screens with some buttons in the UI of the windows application.

MenuBar is a desktop only composable, so it must be created on the Window of the desktopMain module, before displaying any ui screen placed on the commonMain module. If it needs to execute navigation, it need to hold a instance of the navcontroller, and to pass the navcontroller down in the appstateholder to the commonMain module screens. I followed the pattern in nowinandroid app:

https://github.com/android/nowinandroid/blob/d15c739812f25401b21614fe0f7e18534d285921/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ui/NiaAppState.kt#L54

I created an appstateholder class with the navcontroller, and I instantiated that in the commonMain package:

@Stable
class AppStateHolder(
    val navController: NavHostController = NavHostController()
) {
    // UI State
    val currentDestination: NavDestination?
        @Composable get() = navController
            .currentBackStackEntryAsState().value?.destination

    // UI logic
    fun navigate(route: String) {
        navController.navigate(route)
    }
}

This is my desktopMain window:

fun main() = application {
    initKoin(
        databaseBuilder = createDatabaseBuilder(),
    )

    val windowState = rememberWindowState(
        size = DpSize(1200.dp, 900.dp),
        position = WindowPosition(Alignment.Center)
    )

    val appStateHolder= remember { AppStateHolder() }

    Window(
        onCloseRequest = ::exitApplication,
        title = stringResource(Res.string.app_name),
        state = windowState
    ) {
        WindowsMenuBar(
            onNavigation = { route -> appStateHolder.navigate(route) },
            onExitEvent = ::exitApplication
        )

        MyApplicationTheme {
            App(appStateHolder)
        }
    }
}

Then, in my commonMain package I receive that appStateHolder and use it for navigation:

@Composable
fun App(appStateHolder: AppStateHolder) {

    NavHost(
        navController = appStateHolder.navController,
        startDestination = ScreenRoute.MainScreen.name
    ) {
        composable(ScreenRoute.MainScreen.name) {
            MainScreen(
                modifier = Modifier.padding(16.dp),
                onBusStopsDBButtonClicked = { appStateHolder.navigate(ScreenRoute.BusStopsDB.name) }
            )
        }

Well, this doesn't work, and gives me this runtime error on composable(ScreenRoute.MainScreen.name) line of my desktopMain module:

Navigator with class "class androidx.navigation.compose.ComposeNavigator". You must call NavController.addNavigator() for each navigation type.

How can I solve it?


Solution

  • Is necessary to call rememberNavController inside the state holder:

    navController: NavHostController = rememberNavController()
    

    doing that, solves the issue, as done in nowinandroidapp:

    @Composable
    fun rememberNiaAppState(
        navController: NavHostController = rememberNavController(),
    ): NiaAppState {
        return remember(
            navController
        ) {
            NiaAppState(
                navController = navController
            )
        }
    }
    
    @Stable
    class NiaAppState(
        val navController: NavHostController
    )