androidmoduleandroid-jetpack-composejetpack-compose-navigation

Multi module architecture with compose navigation


What do I attach view models to in a modularized purely compose app?

I have a top level module called :app. Inside of MainActivity.kt I have my navigation code that looks like this:

NavHost(navController, startDestination = BottomNavItem.Daily.route) {
    composable(BottomNavItem.Daily.route) {
        DailyWordScreen()
    }
    composable(BottomNavItem.Profile.route) {
       ProfileScreen()
    }
}

The DailyWordScreen() is a composable in its' own module called :feature:daily . I want the DailyWordViewModel() to be encapsulated in the :feature:daily module. I do not want :app to have knowledge of the DailyWordViewModel(). I want my composables to be testable via passing in required args.

Is the solution to have DailyWordScreen() to become a parent composable that will create the view model inside of this parent composable to pass view model state down into a child composable?

I feel like I need a fragment to host the view model and setup the composable inside setContent {}.

I'm currently doing this:

@Composable
fun DailyWordScreen(
    modifier: Modifier = Modifier,
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    dailyWordViewModel: DailyWordViewModel = viewModel()
) {

    val state by dailyWordViewModel.state.collectAsStateWithLifecycle()
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                dailyWordViewModel.setupGame()
            }
        }

        lifecycleOwner.lifecycle.addObserver(observer)

        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

This is working fine for me, but is this correct practice?


Solution

  • Based on official Now In Android application, every feature module would have a navigate extension method, for example inside BookmarkNavigation.kt:

    fun NavGraphBuilder.bookmarksScreen(
        onTopicClick: (String) -> Unit,
        onShowSnackbar: suspend (String, String?) -> Boolean,
    ) {
        composable(route = bookmarksRoute) {
            BookmarksRoute(onTopicClick, onShowSnackbar)
        }
    }
    

    and the BookmarksRoute, BookmarksViewModel and BookmarksScreen would be encapsulated inside :feature:bookmarks by using internal, so all your bookmark stuffs would not be exposed to outside, except the navigation method

    @Composable
    internal fun BookmarksRoute(
        viewModel: BookmarksViewModel = hiltViewModel()
    ) {
        val feedState by viewModel.feedUiState.collectAsStateWithLifecycle()
        BookmarksScreen(feedState = feedState)
    }
    

    your navigation host :app:

    @Composable
    fun NavHost(startDestination: String = forYouNavigationRoute) {
        val navController = appState.navController
        NavHost(
            navController = navController,
            startDestination = startDestination,
            modifier = modifier,
        ) {
            bookmarksScreen() // <=== Here is the extension method
        }
    }
    

    For your reference: https://github.com/android/nowinandroid/blob/main/feature/bookmarks/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/bookmarks/navigation/BookmarksNavigation.kt