Android Jetpack Compose Architecture: why it is not advisable to pass ViewModel directly to Screen as its argument/parameter?

Viewed 188

I am trying to undersdand Android JetNews application which is provided as the canonical best practice architecture example application. Specifically - each screen has 3 files/classes: Route / ViewModel / Screen. E.g. Interests view contains those 3 files plus one additional component - those files can be accessed https://github.com/android/compose-samples/tree/main/JetNews/app/src/main/java/com/example/jetnews/ui/interests and the whole application is accesible in the highe level of this repository.

One can see that InterestsViewModel https://github.com/android/compose-samples/blob/main/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsViewModel.kt contains the event-handlers that can be assigned to specific events:

fun toggleTopicSelection(topic: TopicSelection) {
    viewModelScope.launch {
        interestsRepository.toggleTopicSelection(topic)
    }
}

Of course - screen have many components and they can raise such events and handlers should be assigned to them. E.g. there is list if topics in this example and the user than toggle each topic to select/deselect as interesting/uninteresting. This can be done from the screen and that is why the screen should be able to call toggleTopicSelection and assign to the even of some UI component. So, one can logically expect that one should pass InterestsViewModel to the InterestScreen as an argument (i.e. their respective instances, of course).

But the exactly opposite thing is happening in InterestsRoute https://github.com/android/compose-samples/blob/main/JetNews/app/src/main/java/com/example/jetnews/ui/interests/InterestsRoute.kt :

@Composable
fun InterestsRoute(
    interestsViewModel: InterestsViewModel,
    isExpandedScreen: Boolean,
    openDrawer: () -> Unit,
    scaffoldState: ScaffoldState = rememberScaffoldState()
) {
    val tabContent = rememberTabContent(interestsViewModel)
    val (currentSection, updateSection) = rememberSaveable {
        mutableStateOf(tabContent.first().section)
    }

    InterestsScreen(
        tabContent = tabContent,
        currentSection = currentSection,
        isExpandedScreen = isExpandedScreen,
        onTabChange = updateSection,
        openDrawer = openDrawer,
        scaffoldState = scaffoldState
    )
}

There is rememberTabContent that receives InterestsViewModel and that constructs some UI Composable components and assigns InterestsViewModel.toggle... handlers to their events. But otherwise InterestsScreen is not receiving InterestsViewModel?

Why is that? My sense is that entire InterestsScreen should have access to the InterestsViewModel and use its event handlers as necessary. There may be many places when screen is willing to send the updates of the state and this can be done through InterestsViewModel event handlers only (which call into respective repository futher). As it stands now (when the developer is invited to avoid passing ViewModel as an argument to the Screen), the architecture requires to carefully and painfully isolate every smallest component that can require the call into ViewModel event handlers and assemble such components in respective remember... functions (e.g. rememberTabContent) (than can receive ViewModel safely). Such isolation seems to be counterintuitive and painful. E.g. if one wants to add another call to event handler (that changes state) then one needs to bring up such code from the Screen and isolate into remember function and completely regroup its code and screen composition tree. No freedom to work and use InterestsViewModel freely.

So - is there some explanation whether to pass or not ViewModel to the Screen as an argument. And such passing is not advisable then how to understand such guideline?

0 Answers
Related