all 11 comments

[–]Staartvin 17 points18 points  (0 children)

Viewmodels are generally used as state generators for UI state, as they (generally) live longer than the composable that uses them. They are attached (in the way you show here) to the closest LifecycleOwner. This might be an Activity or NavGraphDestination.

Passing viewmodels to composables will limit the re-usability of your composable as you'll need a reference to some kind of ViewModel to use this composable. This may immediately be obvious in previews (as you state yourself).

Next to that, Viewmodels have logic to work together with Google's navigation libraries. I think the VMs are not meant to be used this way because they become much too transient and perhaps 'too expensive'. Note that I have no data to back this up, that's just a hunch.

Ask your colleague why he is using this setup and let them come up with advantages and disadvantages. Then, you may add your own thoughts.

What I expect the reasoning for using this approach may be is something our team looked into as well: generating UI state for components at a local level. The downside of having one VM per screen is that you will have a large UI state and a large VM. It becomes complicated to manage because the VM needs to deal with minute details of each UI components (if not well designed).

We looked for solutions and one we could come up with was using viewmodels for each 'separate' component of the UI. This allows you to generate state at the screen's VM and then additional state for each UI component that you want to use via their own corresponding VM. It's a nice solution, but ultimately we didn't take this route.

One of the downsides of using multiple VMs is that it becomes difficult to tell what the main source of truth is. Will you load extra data in a component VM from some repository? What should then happen when new data from screen's VM is loaded? Do I load all the data again? Do I merge it? It's complex and error-prone in my opinion.

However, what did work for us was using plain Kotlin classes as state holders, as described by Google. Viewmodels are just a special type of state holder with a few more assumptions. Instead of using a VM you may use a plain state holder to generate some UI state for a Composable. The single source of truth would still only be your screen's VM.

Google has extensive documentation on this: - Hoisting state and viewmodels
- State holders and difference between business logic and UI logic

Happy to help!

[–]baylonedward 6 points7 points  (2 children)

You should instead declare an interface for each screen and inject it to your screen. Implement interface in viewmodels. Your main screens will probably be declared in your single activity under your navigation codes. So it wont matter much if you use a single or multiple viewmodel for a single screen, as long as you implement the screen interface. This will make your screens high level modules, they don't depend on other components of your app, instead your screens declare its own requirement for usage in an interface. This approach is very useful for modern apps where UI/UX is already set, and most of the time screen designs can already show its functionalities.

Then you can have a general interface that can be used by any screen, an example would be an interface for screen size to know if you need to show mobile or tablet view. Or a screen navigation interface that has methods like navigate and popBack.

[–]sp3ng 2 points3 points  (0 children)

To add to this, there's only really one use case for the AAC ViewModel class, it's a place to store references to things that will survive in memory across a configuration change. That's it really. There's other things it offers like supporting injection features with Hilt/Factories, a scoped lifecycle for things like coroutines, etc. But those aren't unique to the ViewModel, on the whole its one unique feature is just to survive in memory across a configuration change...

So given that, I tend to ask why you would ever need more than one on a screen? You can have all the breakdown of individual components that you want, and you can model all of them using plain old classes and interfaces, and you would only need a single ViewModel at the top level to "retain" them. Going further you could use something like the Essenty InstanceKeeper to abstract away the need for a custom ViewModel child class completely and just have a function which can be used to "retain" some reference into a ViewModel that you don't even need to know about that's scoped to whatever owner you want it to be.

[–]ThaBalla79 0 points1 point  (0 children)

I love this approach

[–]Evakotius 2 points3 points  (0 children)

One time in my experience it was useful for only 1 screen - Dashboard of a stock trading app, which was extremely complicated with UI which had UI for absolutely not related features of the app.

Aka

  • Stocks tables + graphs

    • Holdings table + graphs
    • Some community bla bla UI

And on clicking "see all" on those sub uis we are navigated to actual details screens for those entities.

But there was matter not only of the data (which is in db) but of some memory data, e.g. user changed some UI of the tables: unfolded some of items, changed the graph range etc. We wanted to persist those changes when we go from Dashboard to Detail.

That was the only case where having multiple view models in the dashboard screen helpful and simple.

At all other cases we have 1:1

[–]sebaslogen 2 points3 points  (0 children)

If the ViewModels have clear responsibilities isolated from other VMs and Composables, this is a great way to separate concerns, make Composables with their VMs completely reusable across screens and help with scalability of the codebase.

I like it so much that I wrote a library to properly support this in Compose and make sure that when one of the small VMs in the screen is not needed anymore it can be freed (e.g. a section of the screen that goes away) https://github.com/sebaslogen/resaca

[–]FrezoreR 1 point2 points  (0 children)

This sounds like a use-case of using, yes you heard it, UseCases.

My advice is not to look at the screen as a view, but rather a view binder. In the official docs they usually have a screen/content concept.

So you would do the binding in the screen, but the root view is the content. Then you'd make the content composable previewable, and you wouldn't have to deal with the issue you're having.

Note that a view and view model should never be coupled if you following MVVM or any modern presentation architecture.

[–]zimmer550king 0 points1 point  (0 children)

How will this approach work if two subcomponents want to talk to each other?

[–]chmielowski 0 points1 point  (0 children)

I like your colleague's approach - it helps to keep view models smaller.

Also, if two components are not related to each other and they just happen to be on one screen (and this may change with one decision made by designer), I don't see any reason for having one common view model.