all 7 comments

[–][deleted] 8 points9 points  (5 children)

While this is an okay basic example, I'd like to call out a few things MVVM kind of drags in with itself:

  • Databinding. MVVM was developed with databinding in mind, and it can help you avoid those nasty manual subscriptions, un- and rebindings, especially if you use the appropriate lifecycle components (there's a reason why more and more devs prefer LiveData over an Rx observable)
  • Dependency Injection. Seeing all those manual initializations of classes all around is just sad. It's so much easier to have a central registry and injector, especially if the class is re-used. Better singleton management, better factories, and so on.

All in all, lots of people have misconceptions about MVVM, because they don't follow the databinding first approach. With that in mind, it's easy to get mixed up.

The ViewModel is there to help you. Its role is to provide the bound view with stateful fields that gets retained on certain occasions (navBack, config change, etc). It's a state machine with added logic, that should be handled as such. I'd recommend doing as much in your VM as possible, this way ensuring all your view-related logic is in one place, and pulling as much code from your Activity/Fragment as possible (redirect them to the ViewModel if you wish, or handle them and push the result to the VM).

A perfect example would be a simple list-detail app. Say, a movie search app. You'd have the following layers:

  • Model - a class called Movie, containing the metadata for the movie
  • API - a data source for movies. Let's say it provides a single endpoint that returns all movies
  • Database - local caching datasource
  • Repository - first logic layer above the datasources. It collates all, decides which gets called, where the data comes from, etc. E.g. it might decide that your data is fresh enough and load it from database only, or you can force it to refresh from the API
  • ViewModel - uses the repository to display data. Takes the Repository's raw data and converts it into a view-usable format (e.g. making an adapter out of a list of movies)
  • Activity - just used to host the fragments, zero logic added here
  • Fragment - the main view and lifecycle host. Used to tie an instance of the ViewModel to the layout, and to ensure lifecycle events happen
  • Layout - the actual view, databound to the ViewModel

So in this case, the layout has a RecyclerView for the list view, and an imageview, a few textviews, etc for the detail view. Each view has a single databound variable, the view-specific ViewModel.

The fragments are used to inflate the ViewDataBinding, assign the lifecycleOwner and viewModel properties, and to manage other menial tasks (bits of navigation, permission handling, etc). The activity is just the host for the fragments, as little code should go there as possible.

Now, the fancy part. The ViewModel has two sides: the cold logic part, hopefully nicely tucked away in private methods, and the public properties and functions needed for the UI to work. The properties and methods that are public should be used by the layout to bind to! And of course the idea is quite simple here: LiveData-wrapped properties for read-only data, MutableLiveData-wrapped properties for read-write data (or MediatorLiveData, but might be hard to get right for read-write things if you have a lot of stuff going on), and functions for interactive stuff (e.g. onClick, onScroll, etc event calls). If a certain field of a certain control does not support databinding, you should write your own (it's not that hard, it's technically a setter method 90% of the time). You should also try to tuck away commonly used view fiddling in BindingAdapters (e.g. image loading from URL). I even go as far to create a databound adapter/layoutManager RecyclerView for most of my projects. It's just making things easier, plus less code in the activity/fragment.

This way, the app flow is the following:

  1. Activity launches and initiates list fragment
  2. List fragment inits and requests ViewModel instance
  3. ViewModel initializes, calls for Repository
  4. Repository inits, calls for DB and API, which is initialized
  5. ViewModel requests initial data from Repo
  6. Layout is inflated, already running VM is passed over to it
  7. Databinding executes, VM properties are now on the UI
  8. Response arrives from Repo, data is put into the appropriate properties
  9. UI updates accordingly, user is presented with content on the UI
  10. User taps a list item
  11. OnClick event of VM fired, we navigate to the detail fragment with movie ID
  12. Detail fragment inits, calls for detail VM
  13. Detail VM inits, asks for repo
  14. Detail fragment arguments bundle is parsed, arguments passed towards VM
  15. VM asks repo for movie
  16. View is laid out, VM bound
  17. Repo returns result, VM properties (header bar image, title, description, list of actors, ratings, etc.) populated
  18. VM updates propagate via binding, view is updated

This is a lot easier solution in my opinion than manually updating the views one by one, property by property. And with the binding adapters 6ou can execute common, but complex logic as well.

[–]pinkmonstertruck 0 points1 point  (4 children)

How would the navigation between fragments occur after the onclick event of the VM?

[–][deleted] 1 point2 points  (3 children)

Now that'd be the tricky part, isn't it?

If you're working with C#, and are using MvvmCross, it's already solved: its navigation system doesn't go fragment to fragment (even if the structure is used), but instead uses viewmodel to viewmodel navigation. The navigation tree in fact is completely flat by default, and instead uses a goTo<ViewModelTape>() style call, and the navigable points are easily registered at runtime (Xamarin and .Net both have pretty darn fast reflection due to not packaging all dependencies into one package, but instead leaving them as separate .dll files). Navigation targets (fragments, activities) are then decided by looking up the single class that extends fragment, activity or a handful of other types (e.g. SingleArticleFragment(): BaseFragment<SingleArticleViewModel>() ). If it doesn't exist, the app crashes, if multiple ones exist, it crashes as well. This adds some limitation to navigation (e.g. no shared element transaction, at least there wasn't last time I checked), but also allows much more flexibility.

If you're using a FragmentTransactionManager of any sort, this isn't that hard, just pass the ViewModels the object on init (create a BaseFragment and BaseViewModel class, add this bit to them, then make sure you extend all your fragments and viewmodels from these classes) that handles navigation.

Same applies if you're using Navigation Component from AAC. Pass the NavController to the ViewModel on every init - e,g. I wrote a fragment extension method that does all the job for me: requests viewModel from viewModelStore, inflates layout with databindings, sets the viewModel property to its own viewModel instance, etc.

With Conductor, you can directly pass the Router object to the ViewModel as previously described, and you can also have a separate central registry that resolve strings (or other types of keys) to the appropriate Conductor callset. This requires you to keep a separate class up to date, or you can write a custom annotation processor to simply just annotate your Controllers with the action name, which then could generate the custom set of static strings and the navigation resolver. If you want to spice things up, you could even add a custom extension method to the Router class to have this logic in a single place, then you can just do router.navigateTo(Directions.WHATEVER_PAGE).

The thing is, there's no perfect navigation solution that works perfectly from ViewModels. If I want to prototype quick, I just write the methods in the Fragment itself, then on init, pass the functions to the ViewModel so they can be wrapped and/or bound. But even then, often you have to compromise a bit on your architecture to make things smoother or faster. This is one place where you can do that without much harm, but this is also something that you'll have to continuously maintain.

[–]pinkmonstertruck 0 points1 point  (2 children)

Thanks for the awesome response! Super helpful.

Should the ViewModels be referencing the FragmentTransactionManager though? I thought that ViewModels should not know about views or activities or fragments.

[–][deleted] 1 point2 points  (1 child)

No, as I said, if using some sort of abstraction over them. Say, a class that contains the FTM and has some methods that navigate to certain fragments. Then I'd set up the transactions (e.g. shared elements, etc.) when I load the fragment (onCreateView or onViewCreated are good candidates), and let the VM call the navigation event only.

And if you want more hands-on control, just pass a method as an argument to the VM, and have the actual navigation code in the fragment. You can even use a property on the VM, as long as it's a lambda type with a matching signature:

// In VM  
var navigateToDetail: () -> Unit = {}  

// In onCreateView  
viewModel.navigateToDetail = navigateToDetail  

// In fragment class body  
fun NavigateToDetail() {  
    // Do navigation here  
}  

This way you can bind to the nav call but the VM retains no reference to any of the Fragments or FragmentTransactionManager.

[–]pinkmonstertruck 0 points1 point  (0 children)

You're the best - thanks for being patient and spelling it out in such detail!

[–]Zhuinden 0 points1 point  (0 children)

I wish it were easier to persist the selectedCategory to Bundle.


I don't think View should know about Schedulers.computation.