This is the fourth day of my participation in the August More text Challenge. For details, see:August is more challenging
Jetpack’s MVVM is not at fault per se, but in some misuse by the developer. This series will share some common AAC misuses to help you build healthier application architectures
When is ViewModel data first loaded?
In MVVM, the ViewModel’s important responsibility is to decouple the View from the Model.
- The View sends a command to the ViewModel to request data
- Views subscribe to changes in the ViewModel via DataBinding or LiveData
When you subscribe to the ViewModel, people usually put onViewCreated, which is fine. But a common mistake is to load the first data in the ViewModel into onViewCreated:
//DetailTaskViewModel.kt
class DetailTaskViewModel : ViewModel() {
private val _task = MutableLiveData<Task>()
val task: LiveData<Task> = _task
fun fetchTaskData(taskId: Int) {
viewModelScope.launch {
_task.value = withContext(Dispatchers.IO){
TaskRepository.getTask(taskId)
}
}
}
}
//DetailTaskFragment.kt
class DetailTaskFragment : Fragment(R.layout.fragment_detailed_task){
private val viewModel : DetailTaskViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
super.onViewCreated(view, savedInstanceState)
/ / to subscribe to the ViewModel
viewMode.uiState.observe(viewLifecycleOwner) {
//update ui
}
// Request data
viewModel.fetchTaskData(requireArguments().getInt(TASK_ID))
}
}
Copy the code
So, if the ViewModel asks for data in onViewCreated, then it will ask again when the View is rebuilt for portrait or horizontal reasons, and we know that the ViewModel has a longer lifetime than the View, so the data can exist across the life of the View, So there is no need to repeat requests as the View is rebuilt.
Correct loading timing
The initial data loading of ViewModel is recommended to be carried out in init{} to ensure that the ViewModelScope is only loaded once
//TasksViewModel.kt
class TasksViewModel: ViewModel() {
private val _tasks = MutableLiveData<List<Task>>()
val tasks: LiveData<List<Task>> = _uiState
init {
viewModelScope.launch {
_tasks.value = withContext(Dispatchers.IO){
TasksRepository.fetchTasks()
}
}
}
}
Copy the code
LiveData KTX Builder
In addition, lifecycle- LiveData – KTX provides a LiveData KTX Builder that allows data requests to be made at the same time as liveData is created. There is no need to create MutableLiveData.
implementation “androidx.lifecycle:lifecycle-livedata-ktx:$latest_version”
val tasks: LiveData<Result> = liveData {
emit(Result.loading())
try {
emit(Result.success(repo.fetchData()))
} catch(ioException: Exception) {
emit(Result.error(ioException))
}
}
Copy the code
Note: This KTX Builder is only suitable for data loading only once, if there is a user dynamically triggered data request, then also need to use MutableLiveData to achieve.
Set initialization parameters for the ViewModel
If you request data in the ViewModel constructor, how do you pass it in when you need arguments? Let’s say we need to pass in a TaskId in our first example.
1. Construct parameters
The easiest way to think about it is by passing in a construct parameter.
class DetailTaskViewModel(private val taskId: Int) : ViewModel() {
/ /...
init {
viewModelScope.launch {
_tasks.value = TasksRepository.fetchTask(taskId)
}
}
}
Copy the code
It is important to note that you cannot call the constructor of the ViewModel directly, so you cannot store the ViewModel in the ViewModelStore.
At this point you need to define a ViewModelProvider. Factory:
class TaskViewModelFactory(val taskId: Int) : ViewModelProvider.Factory {
override fun
create(modelClass: Class<T>): T =
modelClass.getConstructor(Int: :class.java)
.newInstance(taskId)
}
Copy the code
Then in the Fragment, use this Factory to create a ViewModel
class DetailTaskFragment : Fragment(R.layout.fragment_detailed_task){
private val viewModel : DetailTaskViewModel by viewModels {
TaskViewModelFactory(requireArguments().getInt(TASK_ID))
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
super.onViewCreated(view, savedInstanceState)
/ /...}}Copy the code
2. Use SavedStateHandler
Since Fragment 1.2.0 or Activity 1.1.0, you can use SavedStateHandle as the ViewModel parameter. SavedStateHandle can help the ViewModel achieve data persistence and can pass arguments for fragments to the ViewModel.
How to use SavedStateHandle to persist data is not the focus of this article, but how to use SavedStateHandle to get arguments
implementation “androidx.lifecycle:lifecycle-viewmodel-savestate:$latest_version”
The SavedStateHandle version of the ViewModel is defined as follows:
class TaskViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
/ /...
init {
viewModelScope.launch {
_tasks.value = TasksRepository.fetchTask(
savedStateHandle.get<Int>(TASK_ID)
)
}
}
}
Copy the code
Create a ViewModel in the Fragment as follows:
class DetailTaskFragment : Fragment(R.layout.fragment_detailed_task){
private val viewModel: TaskViewModel by viewModels {
SavedStateViewModelFactory(
requireActivity().application,
requireActivity(),
arguments// Pass arguments to SavedStateHandler as the default argument)}override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
super.onViewCreated(view, savedInstanceState)
/ /...}}Copy the code
SavedStateViewModelFactory is key, it will be constructed the ViewModel, incoming SavedStateHandler
3. Customize extension methods
The first two methods have a lot of template code, but a custom extension method, viewModelByFactory, is recommended to simplify the code even further
typealias CreateViewModel = (handle: SavedStateHandle) -> ViewModel
inline fun <reified VM : ViewModel> Fragment.viewModelByFactory(
defaultArgs: Bundle? = null.noinline create: CreateViewModel = {
val constructor = findMatchingConstructor(VM::class.java, arrayOf(SavedStateHandle::class.java))
constructor!!!!! .newInstance(it) } ): Lazy<VM> {return viewModels {
createViewModelFactoryFactory(this, defaultArgs, create)
}
}
inline fun <reified VM : ViewModel> Fragment.activityViewModelByFactory(
defaultArgs: Bundle? = null.noinline create: CreateViewModel
): Lazy<VM> {
return activityViewModels {
createViewModelFactoryFactory(this, defaultArgs, create)
}
}
fun createViewModelFactoryFactory(
owner: SavedStateRegistryOwner,
defaultArgs: Bundle? , create:CreateViewModel
): ViewModelProvider.Factory {
return object : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
override fun
create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
@Suppress("UNCHECKED_CAST")
return create(handle) as? T ? :throw IllegalArgumentException("Unknown viewmodel class!")}}}@PublishedApi
internal fun <T> findMatchingConstructor(
modelClass: Class<T>,
signature: Array<Class< * > >): Constructor<T>? {
for (constructor in modelClass.constructors) {
val parameterTypes = constructor.parameterTypes
if (Arrays.equals(signature, parameterTypes)) {
return constructor as Constructor<T>
}
}
return null
}
Copy the code
The effects when used are as follows:
class DetailTaskFragment : Fragment(R.layout.fragment_detailed_task){
private val viewModel by viewModelByFactory(arguments)
override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
super.onViewCreated(view, savedInstanceState)
/ /...}}Copy the code
In addition to SavedStateHandler, you can customize the CreateViewModel if you want to add more parameters
4. Dependency injection
Finally, take a look at how to use dependency injection to pass parameters. Hilt, for example, naturally supports dependency injection for viewModels and is also based on SavedStateHandler in nature
@HiltViewModel
class DetailedTaskViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
/ /...
}
Copy the code
Add the @hiltViewModel annotation and use the @Inject annotation constructor. In addition to SavedStateHandle, you can also inject more parameters
To use ViewModel, don’t forget to add @AndroidEntryPoint
@AndroidEntryPoint
class DetailedTaskFragment : Fragment(R.layout.fragment_detailed_task){
private val viewModel : DetailedTaskViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
super.onViewCreated(view, savedInstanceState)
/ /...}}Copy the code
Before three ways more or less to use ViewModelProvider. Factory to construct the ViewModel, and Hilt to avoid the use of the Factory, the most simple in writing.
(after)
series
Jetpack MVVM seven Deadly SINS: Launch coroutines in launchWhenX
One of Jetpack MVVM’s seven deadly SINS: Using fragments as LifecycleOwner