How to model parent-child relationship in Android MVVM VMs?


In a multiple architecture activity if you wan't PianoViews to have ViewModels and your ActivityViewModel to know about them - don't use Dagger injection with them but create PianoViewModels inside an ActivityViewModel and assign some callback to them on the stage of creation - thus you will have an access to them and will be able to listen to their events and influence their behaviour as well as save their state, from inside the ActivityViewModel. It is not an uncommon and in some cases even a correct approach. Dagger - is a mere instrument that is not intended to be used everywhere, but only there were it is needed. It is not needed to create PianoViewModels - you can inject all the needed stuff into the ActivityViewModel and pass all the needed elements to PianoViewModels constructors.

Also you don't need to wrap your Views into Fragments if you don't want to.

Edit end

You are making wrong assumptions based on a flawed architectural approach.

I am curious why do you need ActivityViewModel at all. View model should exist only for the elements that have some View. Current android development suggests that the Activity should not have a view representation and serve as a mere container of other views(Single activity principle). Depending on your architecture Activity may handle showing the loading state(progress bar) and some errors, but once again it should not contain anything that is being handled by other views. Thus PianoView should be a PianoFragment with its own ViewModel that handles access to its repository on the data layer via interactor on the domain layer.

The shared view model would work in case you would need one, and you would be using the Single activity principle with multiple fragments. Because Jetpack Navigation has the support of the shared view model out of the box. In the case of a shared view model - each fragment would have its own view model along with a shared one for communication. Each navigation graph could have a separate shared view model only for the fragments it contains.

Also regarding KeyStateRepository - you need only one of those(or a Dagger @Scoped multiple copies - but I do not recommend it). The only change should be - the addition of an extra key for each separate PianoView - to distinguish them inside a KeyStateRepository. To easily achieve that you may be using Room or some other file/memory/database cache mechanism.

Hence the initial problem of your app is not an inverted dependency of ActivityViewModel on a PianoViewModel, but a flawed architecture of the app and its inner interactions. If you want to continue work with your current architecture - there is no easy answer to your question, and almost every chosen solution would not be 'clean' enough to justify its usage.

I think you got a decent answer from Pavlo up there, I'll just clarify what he meant with other words.

  1. KeyStateRepository is a storage for the state of piano keys. There's nothing stopping you from making it to support N number of Pianos at the same time, this would solve the scenario where you have NNN Pianos on Screen, each with different keys pressed.

  2. The PianoView should be contained in a Fragment, that should be your "unit". Why? Because you want a ViewModel to handle the state and events coming to/from the view. And a Fragment is the Android artifact provided for that regard. Think of it as an annoying piece of baggage you need. Android Devs used to call these things "Policy Delegates" because you delegate to these (a Fragment/Activity) some things you cannot do without "the framework" (the Android Framework, that is).

  3. With this in mind, you have an Activity whose viewModel/State is handled independently. What State/Events do this viewModel handle? Things that are not in the PianoFragment/View(s). E.g. if you wanted to handle the back navigation, or a "record" button at the top, this is the activity's domain. What happens inside the "PianoView/Fragment" is not this activity's problem.

  4. Now the Fragment that will contain the actual PianoView can be designed to contain "more than one" or just one. If you go for more than one, then the PianoContainerFragment will be designed with a ViewModel designed to handle more than one PianoView (so each view will have a "name/key") and the KeyStateRepo will be able to handle the "CRUD" operations of any Piano View you throw at. The ViewModel will sit in between, dispatching events for different "subscribed" views.

  5. If you elect to go for "one fragment contains one piano view", then it's a similar architecture, but now handling the multiple "fragments" in one "activity" is now responsibility of the Activity (and its view model). But remember, the PianoViews (via a Fragment either shared or not) talk to a ViewModel that can be shared among piano views, that talks to a common KeyState Repo. The activity coordinates the views and other Android things (navigation, etc.) but the views operate independently, even of each other.

  6. You don't really need a shared viewModel I think, in fact, I wouldn't do it until really needed, the more you separate things, the less the chances of "violating" one of the fancy patterns... but if you elect to use the PianoViewModel as a shared among all views, that's perfectly acceptable, you're going to have to include the Piano "Name" to differentiate whose events are for whom.

In other words (showing with ONE PianoViewModel for ASCII Simplicity),

// One QuizActivityViewModel, Multiple Fragments:

Activity -> PianoFragment (PianoView)| 
                                     | <-> PianoViewModel <-> KeyRepo
            PianoFragment (PianoView)|                       /
            -> QuizActivityViewModel <----------------------/

Here the QuizActivity creates N fragments (in a list maybe?). These fragments internally initialize their pianoView and connect to a PianoViewModel (can be shared like in the graph above) or each can have its own. They all talk to the same Repo. The repo is your "single source of truth about what each "piano". What keys are pressed, and anything else you can think of (including a name/key to make it unique). When QuizActivity needs to evaluate the state of these, it will ask (via its own viewModel) for the state of NN pianos.


// 1 Act. 1 Frag. N Views.
Activity -> PianoFragment (PianoView)| 
                          (PianoView)| <-> PianoViewModel <-> KeyRepo
         -> QuizActivityViewModel  <---------------------------/

With these, the QuizActivity (which created the pianos to begin with as well), also knows the keys of the pianos that will/are displayed. It can talk to its viewModel that talks to the same KeysRepo (you only have one of these and that's fine). So it can still handle the "nav" buttons and it can ask (via its QuizActVM) what the current state of the Keys are (for all involved pianos). When a Piano key event is fired in a PianoView, the PianoViewModel will receive the event (what key was touched, in what piano); the KeyStateRepo will record this, and perhaps update a flow {} with the events coming from the pianos...

The Flow will be expressed in a sealed class which will contain enough information for both QuizActivity + VM (to perhaps perform a real-time validation), and to the PianoViewModel to update the state and push a new state to the PianoFragment (Which will update the state of its view(s)).

This is all common to either method. I hope this clarifies the sequence.

Does this make sense to you?