I ran into the issue of a ViewModel
not maintaining the apps UI state when I first started working with ViewModels
and it was due to a something that I simply overlooked at the time. So if you are just starting out and running into the same issue maybe this post will help you out.
Before diving in lets review why we want to make use of ViewModels
. First, it’s a way to encapsulate business logic and keep that logic out of the UI code. Second, and this is a big one for me, it maintains UI state through configuration changes. (You can simulate a configuration change by just rotating your phone from vertical to horizontal or vice versa). There are other reasons as well but those are the big ones. So I was quite surprised when running the code below and all the information I added to my UI was reset. It was after a bit of research that I realized I missed a glaring detail about how to instantiate ViewModels
.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var viewModel = mainViewModel()
setContent {
BloggingAppTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
mainActivityScreen(vm = viewModel)
}
}
}
}
}
During my research I also saw some examples like the one below where the ViewModel was instantiated in the methods arguments. Both the example above and below are incorrect and will result in your ViewModel being re-created during every configuration change.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BloggingAppTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
mainActivityScreen(vm = mainViewModel())
}
}
}
}
What is the Correct Way to Instantiate a ViewModel?
When you instantiate your ViewModel
class you need to do it by calling the viewModel()
or viewModles()
methods. These are methods that live in AndroidX libraries, each method lives in a different library but fundamentally they perform very similar tasks. What these methods do is add your ViewModel
into a ViewModelStore
and it is the ViewModelStore
that persist through configuration changes. (If you want to go off the deep end on how ViewModelStores persist through configuration changes click here.)
These methods are defined in two different packages:
androidx.activity.viewModels
androidx.lifecycle.viewmodel.compose.viewModel
They have slightly different signatures as well, if you are using the activity.viewModels method you will need to create a factory that returns a ViewModelProvider.Factory
object. In my opinion it is a bit more work to setup the factory – Example:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel: mainViewModel by viewModels { mainViewModel.Factory }
setContent {
BloggingAppTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
mainActivityScreen(vm = viewModel(initializer = {mainViewModel()}))
mainActivityScreen(vm = viewModel)
}
}
}
}
ViewModel Code:
class mainViewModel : ViewModel() {
private var _counter = mutableStateOf(0)
var counter: Int
get(){return _counter.value}
set(value) {
_counter.value = value
}
// the factory that will be called when the ViewModel is created.
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return mainViewModel() as T
}
}
}
}
The lifecycle.viewmodel.compose.viewModel
just takes a initializer lambda that you can return your ViewModel in. However, you do have to call the viewModel
method from within a composable function. Its pretty easy to setup especially if your model does not have any dependencies. Example:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BloggingAppTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
mainActivityScreen(vm = viewModel(initializer = {mainViewModel()}))
}
}
}
}
Now that we are using one or both of the above methods our UI state will persist through configuration changes. I’m also assuming you are not using Dagger or Hilt those libraries have their own way of doing things like this.
What was happening with the First Examples?
When there is a configuration change like when the phone is rotated or when a different language is selected in settings the top activity is destroyed and recreated along with the views that are anchored to that Activity. In the examples above every time there is a configuration change the constructor for the ViewModel
is called again and you get a whole new object.