Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds ViewModel support to the Arch. Comp. library #405

Closed
wants to merge 1 commit into from
Closed

Adds ViewModel support to the Arch. Comp. library #405

wants to merge 1 commit into from

Conversation

miquelbeltran
Copy link

@miquelbeltran miquelbeltran commented Jan 26, 2018

  • A ViewModelController that has its own ViewModelStore
  • The ViewModelController extends LifecycleController so can be used to subscribe to LiveData
  • Also provides a ViewModelProvider factory method
  • The Architecture Components library version is updated to 1.1.0
  • Includes demo usage in ViewModelDemoController

Based on the discussion in #395

This solution is also available as standalone library: https://github.com/miquelbeltran/conductor-viewmodel/tree/master which also fixes the Lifecycle events to handle the LiveData observer subscription properly.

Currently this PR depends on #383 as well, because the current implementation of the LifecycleController can't be used to observe LiveData as the LiveData observer is not being removed in onDestroyView.

@@ -43,6 +43,7 @@ ext {
autodispose = "com.uber.autodispose:autodispose:$autodisposeVersion"

archComopnentsLifecycle = "android.arch.lifecycle:runtime:$archComponentsVersion"
archComopnentsExtensions = "android.arch.lifecycle:extensions:$archComponentsVersion"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comopnents -> Components

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks @jshvarts I fixed the typo and removed the runtime import, as the extensions already depend on it so it's not required to explicitly add it


private ViewModelStore viewModelStore = new ViewModelStore();

protected ViewModelProvider viewModelProvider() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new ViewModelProviders.DefaultFactory() is deprecated. Can it be dropped here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I did not realize 1.1.0 deprecated the method. It was replaced with https://developer.android.com/reference/android/arch/lifecycle/ViewModelProvider.AndroidViewModelFactory.html

@jshvarts
Copy link

jshvarts commented Jan 26, 2018

I just tried running the Arch Components View Model demo and got a crash:

FATAL EXCEPTION: main
Process: com.bluelinelabs.conductor.demo, PID: 11519
java.lang.RuntimeException: Cannot create an instance of class com.bluelinelabs.conductor.demo.controllers.viewmodel.ViewModelDemo
at android.arch.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.java:154)
at android.arch.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.java:208)
at android.arch.lifecycle.ViewModelProvider.get(ViewModelProvider.java:133)
at android.arch.lifecycle.ViewModelProvider.get(ViewModelProvider.java:101)
at com.bluelinelabs.conductor.demo.controllers.viewmodel.ViewModelDemoController.onCreateView(ViewModelDemoController.java:49)
at com.bluelinelabs.conductor.Controller.inflate(Controller.java:986)
at com.bluelinelabs.conductor.ControllerChangeHandler.executeChange(ControllerChangeHandler.java:190)
at com.bluelinelabs.conductor.ControllerChangeHandler.executeChange(ControllerChangeHandler.java:154)
at com.bluelinelabs.conductor.Router.performControllerChange(Router.java:792)
at com.bluelinelabs.conductor.Router.performControllerChange(Router.java:762)
at com.bluelinelabs.conductor.Router.performControllerChange(Router.java:744)
at com.bluelinelabs.conductor.Router.pushController(Router.java:179)
at com.bluelinelabs.conductor.demo.controllers.ExternalModulesController.onModelRowClick(ExternalModulesController.java:97)
at com.bluelinelabs.conductor.demo.controllers.ExternalModulesController$AdditionalModulesAdapter$ViewHolder.onRowClick(ExternalModulesController.java:148)
at com.bluelinelabs.conductor.demo.controllers.ExternalModulesController$AdditionalModulesAdapter$ViewHolder_ViewBinding$1.doClick(ExternalModulesController$AdditionalModulesAdapter$ViewHolder_ViewBinding.java:33)
at butterknife.internal.DebouncingOnClickListener.onClick(DebouncingOnClickListener.java:22)
at android.view.View.performClick(View.java:6205)
at android.view.View$PerformClick.run(View.java:23653)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6682)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1520)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1410)
Caused by: java.lang.IllegalAccessException: java.lang.Class<com.bluelinelabs.conductor.demo.controllers.viewmodel.ViewModelDemo> is not accessible from java.lang.Class<android.arch.lifecycle.ViewModelProvider$NewInstanceFactory>
at java.lang.Class.newInstance(Native Method)
at android.arch.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.java:150)
at android.arch.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.java:208) 
at android.arch.lifecycle.ViewModelProvider.get(ViewModelProvider.java:133) 
at android.arch.lifecycle.ViewModelProvider.get(ViewModelProvider.java:101) 
at com.bluelinelabs.conductor.demo.controllers.viewmodel.ViewModelDemoController.onCreateView(ViewModelDemoController.java:49) 
at com.bluelinelabs.conductor.Controller.inflate(Controller.java:986) 
at com.bluelinelabs.conductor.ControllerChangeHandler.executeChange(ControllerChangeHandler.java:190) 
at com.bluelinelabs.conductor.ControllerChangeHandler.executeChange(ControllerChangeHandler.java:154) 
at com.bluelinelabs.conductor.Router.performControllerChange(Router.java:792) 
at com.bluelinelabs.conductor.Router.performControllerChange(Router.java:762) 
at com.bluelinelabs.conductor.Router.performControllerChange(Router.java:744) 
at com.bluelinelabs.conductor.Router.pushController(Router.java:179) 
at com.bluelinelabs.conductor.demo.controllers.ExternalModulesController.onModelRowClick(ExternalModulesController.java:97) 
at com.bluelinelabs.conductor.demo.controllers.ExternalModulesController$AdditionalModulesAdapter$ViewHolder.onRowClick(ExternalModulesController.java:148) 
at com.bluelinelabs.conductor.demo.controllers.ExternalModulesController$AdditionalModulesAdapter$ViewHolder_ViewBinding$1.doClick(ExternalModulesController$AdditionalModulesAdapter$ViewHolder_ViewBinding.java:33) 
at butterknife.internal.DebouncingOnClickListener.onClick(DebouncingOnClickListener.java:22) 
at android.view.View.performClick(View.java:6205) 
at android.view.View$PerformClick.run(View.java:23653) 
at android.os.Handler.handleCallback(Handler.java:751) 
at android.os.Handler.dispatchMessage(Handler.java:95) 
at android.os.Looper.loop(Looper.java:154) 
at android.app.ActivityThread.main(ActivityThread.java:6682) 
at java.lang.reflect.Method.invoke(Native Method) 
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1520) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1410) 

@miquelbeltran
Copy link
Author

I submitted a change, I suspect the issue is that ViewModelDemo was not public but package-private. Surprisingly it worked for me all the time.

@jshvarts
Copy link

Making ViewModelDemo public fixed the crash.

@esafirm
Copy link

esafirm commented Jan 28, 2018

How one can share the ViewModel between parent Controller to its children?

@miquelbeltran
Copy link
Author

@esafirm I'd not share the same instance of a ViewModel across views (doesn't matter if they are Controllers, Activities or Fragments).

You could of course have different instances of the same ViewModel class on as much views as you need. (Example: you have a ViewModel for a user profile, you can use this ViewModel on different screens where you need to display the user profile, but each one will have its own instance).

The question is also frequently asked regarding sharing VM across activities: android/architecture-components-samples#29

As well, the best way to ensure that the same data is used on all instances of the same ViewModel is by having a data repository that can store any state or api data that you need to display, and the ViewModels use that data repository.


public abstract class ViewModelController extends LifecycleController {

private ViewModelStore viewModelStore = new ViewModelStore();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think viewModelStore.clear() should be called on Controller onDestroy

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, as the docs say: If an owner of this ViewModelStore is destroyed and is not going to be recreated, then it should call clear() on this ViewModelStore, so ViewModels would be notified that they are no longer used. https://developer.android.com/reference/android/arch/lifecycle/ViewModelStore.html#clear() I'd look to add that to the ViewModelController onDestroy method.

@esafirm
Copy link

esafirm commented Jan 28, 2018

The question is also frequently asked regarding sharing VM across activities: android/architecture-components-samples#29

The issue is about sharing ViewModel between activities, which in comparison would be sharing a ViewModel between Controller on the same level.

What i'm asking is sharing a ViewModel between a Controller and their children (added via getChildRouter) and i think it is a common/valid use case. It's the same as sharing between Activity and their Fragments.

AFAIK it is even mentioned on ViewModel introduction. (Can communicate between Activity and their Fragments in the decouple way)

@miquelbeltran
Copy link
Author

miquelbeltran commented Jan 28, 2018

OK, I never use child Controllers so I cannot go into details. As far as they can access the parent Controller instance, they should be able to use the same ViewModelProvider/ViewModelStore, then get the same ViewModel instance. Then I guess we need to make the viewModelProvider method public not protected. If you give it a try let me know!

@miquelbeltran
Copy link
Author

miquelbeltran commented Feb 13, 2018

As I needed this for another project I made a small library: https://github.com/miquelbeltran/conductor-viewmodel which also provides the Lifecycle events to handle the LiveData observer subscription properly to unsubscribe then the view is destroyed.

@tomblenz
Copy link

I'd make viewModelProvider() public. With activities and fragments, a fragment can pass its activity as a provider to get the activity ViewModel for ease of sharing data between fragments and activities (google have an example of this use case in their docs).

eg. this is currently not possible because viewModelProvider() is protected.

val vm = (parentController as ViewModelController)
      .viewModelProvider()
      .get(HomeViewModel::class.java)

@miquelbeltran
Copy link
Author

I agree. I'll make the change!

miquelbeltran added a commit to miquelbeltran/conductor-viewmodel that referenced this pull request Apr 11, 2018
@tomblenz
Copy link

I've been having a play with this PR and I have to say, this appears to tick all of the boxes for AC+Conductor interop (with the above exception). Good job mate.

I also was not able to reproduce the duplicated observer thing in either Fragment or Controller, it may have been fixed in AC 1.1.1 or something?

@miquelbeltran
Copy link
Author

miquelbeltran commented Apr 11, 2018

Thanks! I am thinking about rebasing this and make it 1:1 to what I have in https://github.com/miquelbeltran/conductor-viewmodel

I added my own LifecycleProvider. See in here: https://github.com/bluelinelabs/Conductor/pull/405/files#diff-b7e5adc69ad34fd475e7fad2af3275a7

Btw I am using the conductor-viewmodel library on a couple of production projects, it's working great :)

* A ViewModelController that has its own ViewModelStore
* Also provides a ViewModelProvider factory method
* The Architecture Components library is bumped to 1.1.0
* Includes demo usage in ViewModelDemoController
* Includes its own LifecycleOwner that provides Lifecycle events
required for LiveData
* It's located in the com.bluelines.conductor.viewmodel package
* Added updated documentation on README.md
* Add viewModelStore.clear in onDestroy

As the docs say:

```
If an owner of this ViewModelStore is destroyed and is not going to be
recreated, then it should call clear() on this ViewModelStore, so ViewModels
would be notified that they are no longer used.
```

https://developer.android.com/reference/android/arch/lifecycle/ViewModelStore.html#clear()

* viewModelProvider method is public so it can be called by child
controlers
@miquelbeltran
Copy link
Author

I have rebased the work, and as well moved the code to the com.bluelinelabs.conductor.viewmodel package.

As said, I have added my own ViewModelLifecycleOwner because the one found in the library is not handling the required Lifecycle events correctly. It's important that ON_DESTROY happens in the onDestroyView and not in onDestroy because you want to stop updating the UI (i.e. stop observing LiveData) when the view is destroyed, not when the Controller is destroyed.

@miquelbeltran
Copy link
Author

Hello all, since there has been no movement for months and I have a working solution as an independent library: https://github.com/miquelbeltran/conductor-viewmodel I've decided to close this PR.

If you are interested in the included functionality please feel free to use my library in conjunction with Conductor!

@miquelbeltran miquelbeltran deleted the feature/view-model-arch-comp branch August 30, 2018 07:50
@johnjeremih
Copy link

@miquelbeltran I currently working on a 5 years old project that uses a conductor. Why should I use Conductor over fragment? Why not replace it with fragments? I have been asking myself this question, but I need an expert to tell me what they think about it.

@PaulWoitaschek
Copy link
Collaborator

The question is: Why would you want to use fragments?
Imo the arch view models don't give any real benefit as controllers survive config changed anyways and thats the main selling point for the arch view models.

@johnjeremih
Copy link

@PaulWoitaschek I'm trying to follow what Google recommends and try to have an MVVM architecture, but with conductors, you don't have that. I like how you can manage the data in the view model and retrieve it in any fragment. Now, I would like to know your perspective and how you see the new trend of MVVM architecture.

@EricKuck
Copy link
Member

It's important to note that MVVM existed far before Google released the component that they call a ViewModel. It's also very important to note that the thing they call a ViewModel is not an MVVM view model. It's a thing that survives configuration changes and nothing more. You can use it as an MVC controller, an MVP presenter, or a handy place to store random state fields.

The ViewModel class is very badly named and has mislead countless developers into thinking it's needed for MVVM. It's not. Its sole purpose is to wallpaper over the fact that activities and fragments recreate on configuration change.

Given that Conductor Controllers do not have this issue, the class provides no value. You don't need a base class called ViewModel to have a ViewModel. You can roll your own with no real effort. What I've done in the past is create a base class with a CoroutineScope (rx works too, or livedata if you really had to) that cancels on Controller lifecycle events. That's really all you need.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants