Converting Android MVP to MVVM with presenter constructor arguments
With the Model–View–ViewModel (MVVM) architectural pattern being implemented in many apps today, I decided to convert one using the Model-View-Presenter (MVP) to one using MVVM. The steps are straightforward and very easy to do, but if you have presenter constructor arguments, you run into a problem until you find the solution, and then it becomes really easy to do again.
On to the original MVP code. I am using Dagger for presenter constructor injection, but you will see that Dagger plays well, behind the scenes. The issue surrounds having arguments in your presenter constructor. The presenter declaration with an argument in the constructor:
class Presenter @Inject constructor(private val service: BitcoinService): Contract.Presenter {The contract is typical MVP stuff and doesn’t care about any presenter constructor argument nor Dagger:
class Contract { interface View {
...
}
interface Presenter {
fun attach(view: View)
...
}
}
With an MVP pattern you attach an implementation of your Contract.Presenter interface in your implementation of your Contract.View interface. In this case, the Contract.View interface is implemented in my MainActivity class. Since I am using Dagger, I’ll also inject the presenter in my activity. The original activity code:
class MainActivity : AppCompatActivity(), Contract.View {
@Inject
lateinit var presenter: Contract.Presenter override fun onCreate(savedInstanceState: Bundle?) {
...
presenter.attach(this)
So there’s the setup. Now to the conversion.
First up is converting the Presenter class to our view-model class.
class MainViewModel @Inject constructor(private val service: BitcoinService): ViewModel() {Easy! Second, let’s create the ViewModel object via a ViewModelProviders object. This is copy-paste from documentation and the code goes in your activity, replacing the presenter code. The result is model , which is a ViewModel object that has awareness of a newly created MainViewModel object. The one-liner is here:
val model = ViewModelProviders.of(this).get(MainViewModel::class.java)or, equivalently:
val model = ViewModelProviders.of(this, factory)[MainViewModel::class.java]Compile. Yes! Run. Argh.
The problem is that MainViewModel doesn’t have a no-arg constructor, and in the depths of creating model we get to its constructor code in the framework’s androidx.lifecycle.ViewModelProvider class:
public static class NewInstanceFactory implements Factory {
...
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
...
return modelClass.newInstance();All it knows is how to create the object with a no-arg constructor, so an exception is thrown since our class doesn’t have one.
How do we do this since MainViewModel has only a constructor with an argument? The lifecycle framework has the solution. We provide a factory that instantiates the object rather than letting the framework’s default no-arg instantiation code instantiate the object.
The one-liner above changes ever so slightly to become:
val model = ViewModelProviders.of(this, factory)[MainViewModel::class.java]We will be able to get the framework to return our view-model object no matter how complex the constructor, through a factory class. The factory’s purpose is to override the framework’s default create method above, which again only knows how to instantiate objects from no-arg constructor classes. The framework allows us to pass this factory class, which will override the factory’screate method. So now to the factory class:
@Suppress("UNCHECKED_CAST")
class ViewModelFactory @Inject constructor(private val service: BitcoinService): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T = MainViewModel(service) as T
}Instead of having the framework instantiate from our MainViewModel class directly, it will use the ViewModelFactory class, which in turn will instantiate the object without requiring a no-arg constructor, the framework’s default requirement.
Notice that this class has the same signature in the right places as the framework’s NewInstanceFactory above, so look at this as a replacement for create, because it is. We can do whatever we want in this factory, it is under our control, as long as we return what create requires, a ViewModel.
And we can do constructor injection by injecting our arguments at the ViewModelFactory level, which our code can do, and passing them as simple arguments to our MainViewModel class. Again, all under our control.
The changes to our MainActivity result in (including injecting the factory in place of the presenter at the activity level):
class MainActivity : AppCompatActivity() {
//@Inject
//lateinit var presenter: Contract.Presenter
@Inject
lateinit var factory: ViewModelProvider.Factory override fun onCreate(savedInstanceState: Bundle?) {
...
//presenter.attach(this)
val model = ViewModelProviders.of(this, factory)[MainViewModel::class.java]
That’s it. Converting an MVP pattern to MVVM when the presenter has construction arguments. Thank you for reading and implementing!
