'Android ViewModel - "by activityViewModels" called before "by viewModels"
After some time away from android development I'm trying to start again with a simple project.
I've created a new project picking the "basic activity" option which resulted in a MainActivity and two fragments. Starting from this, since the main functionality requires a database, I've followed the "Room with a view" codelab, which however has a single activity. In my project I set an observer in the activity and all worked fine but, as soon as I moved the observer in the first fragment and "retrieved" the ViewModel with "by activityViewModels", the app started throwing an Instantiation exception. Reason: MyViewModel has no zero argument constructor.
After some debugging, I've noticed that the "by activityViewModel" property in the fragment is called before the "by viewModel" in the activity. The ViewModel has a factory and I would like it scoped to the activity and later would be accessed from the second fragment.
ViewModel:
class MyViewModelFactory(private val repository: MyRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return MyViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
class MyViewModel(private val repository: MyRepository): ViewModel() {
val list: LiveData<List<Item>> = repository.allItems.asLiveData()
}
Activity
...more imports
import androidx.activity.viewModels
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMainBinding
val myViewModel: MyViewModel by viewModels {
MyViewModelFactory((application as MyApplication).repository)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
val navController = findNavController(R.id.nav_host_fragment_content_main)
appBarConfiguration = AppBarConfiguration(navController.graph)
setupActionBarWithNavController(navController, appBarConfiguration)
myViewModel.list.observe(this) { list ->
print(list.size)
}
}
}
Fragment
...more imports
import androidx.fragment.app.activityViewModels
class ListFragment : Fragment() {
private var _binding: FragmentListBinding? = null
private val binding get() = _binding!!
val sharedViewModel: MyViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedViewModel.list.observe(viewLifecycleOwner) { list ->
print(list.size)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Dependencies
def room_version = "2.4.2"
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.4.1'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
// Room components
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
//same result enabling these dependencies
//implementation 'androidx.activity:activity-ktx:1.4.0'
//implementation 'androidx.fragment:fragment-ktx:1.4.1'
//implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
For what I understand, the "by viewModels { //factory method }" property in the activity should instantiate the viewModel using the factory, then the ":viewModelType by activityViewModel" property in the fragment (which has no factory option) retrieve a ViewModel of the defined type, if already instantiated by the parent activity.
If I have understood correctly, why "by activityViewModels" is called before "by viewModels"? Shouldn't be the other way around? How can I fix it?
Solution 1:[1]
You should modify your viewmodel, activity and fragment.
First, for your ViewModel, the ViewModelProvider.Factory is deprecated, so use this instead :
class MyViewModel(application: Application): AndroidViewModel(application) {
private val repository by lazy { MyRepository.newInstance(application) }
val list: LiveData<List<Item>> = repository.allItems.asLiveData()
}
Then, in your activity class:
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMainBinding
val myViewModel by viewModels<MyViewModel>()
And for your fragment:
class ListFragment : Fragment() {
private var _binding: FragmentListBinding? = null
private val binding get() = _binding!!
val sharedViewModel by activityViewModels<MyViewModel>()
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | ladytoky0 |