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.
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.