Search code examples
androidkotlinfragmentviewmodellazy-evaluation

ViewModel initialisation when the factory is no longer there


I am creating a minimal ViewModel in my MainActivity, using the by viewModels mechanism.

It currently just maintains a repository, by Dependency Injection. This requires construction parameters, so I provided a factory class for doing this.

I instantiate a Factory object within MainActivity and pass same using by viewModels to initialise the first and only instance of that ViewModel.

private val Context.dataStore by preferencesDataStore("settings")

class MainActivity : AppCompatActivity() {

//..
    private val viewModel : AutomationViewModel by viewModels {
        AutomationViewModelFactory(
        this.application, SettingsRepository(dataStore))
    }
//...

The ViewModel looks like this:

class AutomationViewModel(
    private val mApplication: Application, // not really needed.
    private val repository: SettingsRepository
): ViewModel() {

    // example property to setup an observable state
    val loginParametersState: StateFlow<LoginParameters> =
        repository.LoginParametersFlow.stateIn(
            viewModelScope,
            SharingStarted.Eagerly, LoginParameters()
        )

    // This is the dummy method, that seems to be necessary to 
    //    preheat.
    var dummy: Int = 0; private set
    fun setDummy(x: Int) {
        dummy = x
    }

}

And here is the associated Factory class

class AutomationViewModelFactory(
    private val mApplication: Application, // not really needed
    private val repository: SettingsRepository
) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return when(modelClass) {
            AutomationViewModel::class.java -> AutomationViewModel(mApplication, repository)
            else -> throw IllegalArgumentException("Unknown ViewModel class")
        } as T
    }

// Not sure what this is about
    override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
        return super.create(modelClass, extras)
    }

That AutomationViewModel is then used in one of my Fragments, also using the by viewModels mechanism. The idea is, of course, that I take advantage of the previously cached instance of the already-instantiated AutomationViewModel.

On my Fragments, I get hold of the ViewModel object using the same 'by' mechanism. However, I don't pass the Factory object in at this point, because it's out of scope now, being scoped to the Activity.

class LoginFragment : Fragment() {
    private val connector: AutomationViewModel by activityViewModels()

    //.. Usual boilerplate code for onCreate, onCreateView, onViewCreated. Nothing particular to see there.


    }

In any case, I don't want to use the factory object in the Fragment because I don't have access to it, and don't want to end up coupling into the Fragment all the stuff that is managed by MainActivity, and I am expecting to use an already cached VM instance.

However, this doesn't work. My Factory object never gets instantiated; the default factory is used instead, and throws an exception because it knows not how to instantiate my particular ViewModel class.

I discovered that the VM was actually never getting instantiated in MainActivity because it is, currently, not touched by it.

It appears that the VM's instantiation is being deferred until the Fragment where, as I said above, the Factory is no longer available.

Is my understanding of what is going on here correct?

Is my structure of passing the Factory into the by statement in the Activity only, just using the naked by to instantiate(retrieve cached) in the Fragments, correct ?

I have noticed that I can make it all work by simply having a dummy function on my VM, which does nothing but causes it to be 'preheated' by forcing instantion at that point in MyActivity.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //.. boilerplate from Wizard

        // Seems to work, only if I add this below, which does 
        //   nothing
        viewModel.setDummy(1)

    }
}

All of the other posts I have seen say the the by and get mechanisms are interchangeable.

This is my main question:

Is there truly no way to instantiate a VM eagerly ? Should I be using a dummy preheat method as a permanent solution.

This is the post from which I am mainly working


Solution

  • by viewModels() returns Lazy so it will only be initialized when it is first used, in your case the fragment. You can manually initialize the ViewModel in your MainActivity.onCreate() using ViewModelProvider.

    MainActivity.kt

    private lateinit viewModel: AutomationViewModel
    
    override fun onCreate(...) {
        super.onCreate(...)
        viewModel = ViewModelProvider(
            this,
            AutomationViewModelFactory(this.application, SettingsRepository(dataStore))
        )
    

    by viewModels() also uses ViewModelProvider, but since it is lazy it is not initialized until you first use it.