Why a viewmodel factory is needed in Android?
Asked Answered
T

4

107

We have been discussing about this but we don't know the reason of creating a viewmodel factory to create a viewmodel instead of instantiate the viewmodel directly. What is the gain of creating a factory that just creates the viewmodel?

I just put a simple example of how I did it without Factory

here is the kodein module:

val heroesRepositoryModel = Kodein {
    bind<HeroesRepository>() with singleton {
        HeroesRepository()
    }

    bind<ApiDataSource>() with singleton {
        DataModule.create()
    }

    bind<MainViewModel>() with provider {
        MainViewModel()
    }
}

The piece of the Activity where I instantiate the viewmodel without using the factory

class MainActivity : AppCompatActivity() {
    private lateinit var heroesAdapter: HeroAdapter
    private lateinit var viewModel: MainViewModel
    private val heroesList = mutableListOf<Heroes.MapHero>()
    private var page = 0
    private var progressBarUpdated = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewModel = ViewModelProviders.of(this)
                .get(MainViewModel::class.java)
        initAdapter()
        initObserver()
        findHeroes()
    }

The ViewModel where I instantiate the usecase directly without having it in the constructor

class MainViewModel : ViewModel(), CoroutineScope {

    private val heroesRepository: HeroesRepository = heroesRepositoryModel.instance()
    val data = MutableLiveData<List<Heroes.MapHero>>()

    private var job: Job = Job()
    override val coroutineContext: CoroutineContext
        get() = uiContext + job

    fun getHeroesFromRepository(page: Int) {
        launch {
            try {
                val response = heroesRepository.getHeroes(page).await()
                data.value = response.data.results.map { it.convertToMapHero() }
            } catch (e: HttpException) {
                data.value = null
            } catch (e: Throwable) {
                data.value = null
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        job.cancel()
    }
}

So here a example using factory

class ListFragment : Fragment(), KodeinAware, ContactsAdapter.OnContactListener {

    override val kodein by closestKodein()

    private lateinit var adapterContacts: ContactsAdapter

    private val mainViewModelFactory: MainViewModelFactory by instance()
    private val mainViewModel: MainViewModel by lazy {
        activity?.run {
            ViewModelProviders.of(this, mainViewModelFactory)
                .get(MainViewModel::class.java)
        } ?: throw Exception("Invalid Activity")
    }

    override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_list, container, false)
    }

The viewmodelfactory:

class MainViewModelFactory (private val getContacts: GetContacts) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
            return MainViewModel(getContacts) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

And the viewmodel:

class MainViewModel(private val getContacts: GetContacts) : BaseViewModel() {
    lateinit var gamesList: LiveData<PagedList<Contact>>
    var contactsSelectedData: MutableLiveData<List<Contact>> = MutableLiveData()
    var contactsSelected: ArrayList<Contact> = ArrayList()
    private val pagedListConfig by lazy {
        PagedList.Config.Builder()
                .setEnablePlaceholders(false)
                .setInitialLoadSizeHint(PAGES_CONTACTS_SIZE)
                .setPageSize(PAGES_CONTACTS_SIZE)
                .setPrefetchDistance(PAGES_CONTACTS_SIZE*2)
                .build()
    }

Here is the complete first example:

https://github.com/ibanarriolaIT/Marvel/tree/mvvm

And the complete second example:

https://github.com/AdrianMeizoso/Payment-App

Tickler answered 29/1, 2019 at 10:43 Comment(2)
so then why to create it yourself if it is used anyway?Tickler
On a side note, why were you explicitly extending from CoroutineScope in your ViewModel?Ruttish
L
120

We can not create ViewModel on our own. We need ViewModelProviders utility provided by Android to create ViewModels.

But ViewModelProviders can only instantiate ViewModels with no arg constructor.

So if I have a ViewModel with multiple arguments, then I need to use a Factory that I can pass to ViewModelProviders to use when an instance of MyViewModel is required.

For example -

public class MyViewModel extends ViewModel {
    private final MyRepo myrepo;
    public MyViewModel(MyRepo myrepo) {
         this.myrepo = myrepo;
    }
}

To instantiate this ViewModel, I need to have a factory which ViewModelProviders can use to create its instance.

ViewModelProviders Utility can not create instance of a ViewModel with argument constructor because it does not know how and what objects to pass in the constructor.

Lauritz answered 29/1, 2019 at 11:27 Comment(10)
But I am instantiating the repository in the viewmodel itself so the constructor doesn't have any parameter. Then the question here is if there is an advantage on instantiating the repository in the activity, create the factory to pass the repository to the viewmodel or just instantiate the repository in the viewmodel and use ViewModelProviders without creating your own factory.Tickler
I am instantiating the repository in the viewmodel itself then you are violating DI principles and makes me wonder why you use Kodein.Langan
For dependency inversion, a class should not create the dependencies it needs. Those dependencies should be supplied to it. Here we have an example of a Constructor Injection where we are injecting our repo through object constructor.Lauritz
Read this for more reference and clarity - en.wikipedia.org/wiki/Dependency_inversion_principleLauritz
@Langan I am instatiating the repository using kodein.Tickler
"ViewModelProviders Utility can not create instance of a ViewModel with argument constructor because it does not know how and what objects to pass in the constructor." Could you explain this? Why is ViewModelProviders designed specifically that way to use only factories and not plain constructors for ViewModel creation?Undershoot
do we have to create ViewModelFactory for each viewModel class ?Antimonyl
@SanketPatel - You can use a single ViewModelFactory for ViewModels with no constructor parameters.Cayes
@Undershoot I'm not quite sure about the reason it's designed that way, but thinking about it, I think it makes sense, if ViewModelProviders will instantiate any ViewModel with any arguments, it means the ViewModelProviders will have to accept unknown and unlimited arguments, which might not be a good design, so a Factory is used so that the ViewModel doesn't have to care how the ViewModel is instantiated, but just use the instance directly. Maybe reading about "Factory Method Pattern" could explain that in more detail. (that's my own interpretation)Desai
These days it is easier to just use Jetpack ViewModel: developer.android.com/topic/libraries/architecture/viewmodelRuttish
P
55

In short,

if we need to pass some input data to the constructor of the viewModel , we need to create a factory class for viewModel.

Like example :-

class MyViewModelFactory constructor(private val repository: DataRepository): ViewModelProvider.Factory {

     override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return if (modelClass.isAssignableFrom(MyViewModel::class.java!!)) {
            MyViewModel(this.repository) as T
        } else {
            throw IllegalArgumentException("ViewModel Not Found")
        }
    }
}

Reason

We cannot directly create the object of the ViewModel as it would not be aware of the lifecyclerOwner. So we use :-

ViewModelProviders.of(this, MyViewModelFactory(repository)).get(MyViewModel::class.java)
Paratyphoid answered 21/1, 2020 at 7:1 Comment(0)
L
46

We have been discussing about this but we don't know the reason of creating a viewmodel factory to create a viewmodel instead of instantiate the viewmodel directly. What is the gain of creating a factory that just creates the viewmodel?

Because Android will only give you a new instance if it's not yet created for that specific given ViewModelStoreOwner.

Let's also not forget that ViewModels are kept alive across configuration changes, so if you rotate the phone, you're not supposed to create a new ViewModel.

If you are going back to a previous Activity and you re-open this Activity, then the previous ViewModel should receive onCleared() and the new Activity should have a new ViewModel.

Unless you're doing that yourself, you should probably just trust the ViewModelProviders.Factory to do its job.

(And you need the factory because you typically don't just have a no-arg constructor, your ViewModel has constructor arguments, and the ViewModelProvider must know how to fill out the constructor arguments when you're using a non-default constructor).

Langan answered 29/1, 2019 at 11:49 Comment(3)
The thing is that I inject to the viewmodel the repository it needs so I don't need to create a constructor with arguments, and it respects the lifecycle because it does't load again everytime I rotate the phone. So it works as if I had a factory but without having to create it.Tickler
That is no injection, you're doing a lookup from a global variable. You're supposed to use constructor argument. In case of Kodein, I would think you're supposed to have provider { MainViewModel(instance()) } and then invoke the provider inside the ViewModelProviders.Factory.Langan
This is the only answer that highlights the importance of setting "initial state" only once, and not on every rotation of the device. That is the essence of why creating a custom factory is important.Thankless
M
12

When we are simply using ViewModel, we cannot pass arguments to that ViewModel

class GameViewModel() : ViewModel() {

    init {
        Log.d(TAG, "GameViewModel created")
    }
}

However, in some cases, we need to pass our own arguments to ViewModel. This can be done using ViewModelFactory.

class ScoreViewModel(finalScore: Int) : ViewModel() {

    val score = finalScore

    init {
        Log.d(TAG, "Final score: $finalScore")
    }
}

And to instantiate this ViewModel, we need a ViewModelProvider.Factory as simple ViewModel cannot instantiate it.

class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
            return ScoreViewModel(finalScore) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

When it comes to instantiating object of this ViewModel i.e with ViewModelProvider, we pass ViewModelFactory as an argument which contains information about our custom arguments which we want to pass. It goes like:

viewModelFactory = ScoreViewModelFactory(score)
viewModel = ViewModelProvider(this,viewModelFactory).get(ScoreViewModel::class.java)

That is why factory methods are there.

Mitchellmitchem answered 19/7, 2021 at 18:56 Comment(3)
But why do i need to pass argument through a constructor instead of creating a function to be called by the class in viewModel?Hereof
@Hereof Because this way you would be able to access that parameter immediately (as in init {}). Also, this way is more clearner.Mitchellmitchem
In most of my cases, I just use a function instead of viewmodel ProviderHereof

© 2022 - 2024 — McMap. All rights reserved.