In single-activity apps, page functions are often hosted by fragments, and there are two ways to communicate between fragments: through the Fragment Result API or ViewModel; The Fragment Result API has the following format:
class FragmentA : Fragment() {
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
// This function is in fragment-ktx
setFragmentResultListener("requestKey") { requestKey, bundle ->
val result = bundle.getString("bundleKey")}}}class FragmentB : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
button.setOnClickListener {
val result = "result"
// This function is in fragment-ktx
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}
}
}
Copy the code
FragmentA receives results only when it is in the STARTED state. This approach is suitable for simple parameter communication, but for larger parameters (such as bitmaps) or more complex communication logic, the ViewModel is a better choice. It is also easy to communicate with a Fragment through a ViewModel. When creating a ViewModel, specify the ViewModelStore as the host Activity of the Fragment. In this way, the same ViewModel can be obtained from multiple fragments. The Fragment-Ktx library also provides the activityViewModels() function to retrieve the ViewModel held by the host Activity directly.
The ViewModel is stored in the Activity’s ViewModelStore. If any Fragment that uses the ViewModel is destroyed, the ViewModel will not be released. In addition to the fact that the ViewModel is already leaked, when we create a Fragment to use the ViewModel again, the ViewModel will still contain the data from the last time we used it, which can cause some unexpected problems.
To solve the above two problems, a common solution is to manually reset the data in the ViewModel during Fragment destruction, as follows:
class TestViewModel : ViewModel() {
private val dataList: MutableList<String> = mutableListOf()
fun addData(string: String) {
dataList.add(string)
}
// onCleared() is not used because this function cannot be called externally
fun tearDown(a) {
dataList.clear()
}
}
Copy the code
Then call TestViewModel#tearDown() in the Fragment’s onDestory().
LifecycleEventObserver and the ViewModel onCleared() function can be used to automatically remove the ViewModel:
open class AutoTearDownViewModel(
private val lifecycleOwner: LifecycleOwner
) : ViewModel(), LifecycleEventObserver {
init {
if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) {
onCleared()
} else {
lifecycleOwner.lifecycle.addObserver(this)}}override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
onCleared()
lifecycleOwner.lifecycle.removeObserver(this)}}}class TestViewModel(val lifecycleOwner: LifecycleOwner) : AutoTearDownViewModel(lifecycleOwner) {
private val dataList: MutableList<String> = mutableListOf()
override fun onCleared(a) {
dataList.clear()
}
}
class AutoTearDownViewModelFactory(
private val lifecycleOwner: LifecycleOwner
) : ViewModelProvider.Factory {
override fun
create(modelClass: Class<T>): T {
return modelClass.getConstructor(LifecycleOwner::class.java).newInstance(lifecycleOwner)
}
}
Copy the code
In fragments can be achieved by using ViewModelProvider (requireActivity (), AutoTearDownViewModelFactory (this)) create ViewModel, when used in multiple fragments, The created ViewModel follows the life cycle of the first Fragment that created it.
This approach does not require us to manually call the cleanup function, but still does not solve the ViewModel leak problem, and still relies on the developer to manually clear or reset the data in the onCleared(), which is easy to forget during development.
How do you automatically remove an Activity’s ViewModel from its ViewModelStore when it is no longer in use? The ViewModel is automatically cleaned up when the Fragment is destroyed by lifecycleEvening Server. All that remains is to remove the ViewModel from the Activity’s ViewModelStore when the Fragment is destroyed. ViewModel is stored in HashMap
. This field is private, so we need to use reflection to get the Map and delete the ViewModel. So we’ll modify the AutoTearDownViewModel as follows:
// AutoTearDownViewModel.kt
const val VIEW_MODEL_KEY = "auto_tear_down_view_model"
fun <T> getVmKey(clazz: Class<T>): String {
return VIEW_MODEL_KEY + ":" + clazz.canonicalName
}
open class AutoTearDownViewModel(
private val fragment: Fragment
) : ViewModel(), LifecycleEventObserver {
init {
if (fragment.lifecycle.currentState == Lifecycle.State.DESTROYED) {
onCleared()
} else {
fragment.lifecycle.addObserver(this)}}override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
val mapField = ViewModelStore::class.java.getDeclaredField("mMap")
mapField.isAccessible = true
val viewModelMap = mapField.get(fragment.requireActivity().viewModelStore) as HashMap<String, ViewModel>
viewModelMap.remove(getVmKey(this: :class.java))? .let {val method = ViewModel::class.java.getDeclaredMethod("clear")
method.isAccessible = true
method.invoke(it)
}
fragment.lifecycle.removeObserver(this)}}}class AutoTearDownViewModelFactory(
private val fragment: Fragment,
) : ViewModelProvider.Factory {
override fun
create(modelClass: Class<T>): T {
return modelClass.getConstructor(Fragment::class.java).newInstance(fragment) as T
}
}
Copy the code
After the above modification, the ViewModel will follow the life cycle of the Fragment that first created it. When the Fragment is destroyed, the ViewModel will be cleaned up, and the next time you enter the Fragment, a new ViewModel will be created.
Note that the above code calls the ViewModel clear() method once after removing the ViewModel, And this method is in androidx. Lifecycle: lifecycle – viewmodel: version 2.1.0 – alpha01 after added, if the previous version, you need to call the onCleared () method.
For ease of use, we can implement a function like activityViewModels() with the help of the Kotlin extension function and the delegate:
// AutoTearDownViewModelProvider.kt
inline fun <reified VM : ViewModel> Fragment.autoTearDownViewModel(a): Lazy<VM> {
return AutoTearDownViewModelLazy(VM::class.this)
}
class AutoTearDownViewModelLazy<VM : ViewModel>(
private val viewModelClass: KClass<VM>,
private val fragment: Fragment
) : Lazy<VM> {
private var cached: VM? = null
override val value: VM
get() {
returncached ? : ViewModelProvider( fragment.requireActivity(), AutoTearDownViewModelFactory(fragment) ).get(getVmKey(viewModelClass.java), viewModelClass.java).also {
cached = it
}
}
override fun isInitialized(a)= cached ! =null
}
Copy the code
The solution mentioned above is not perfect either. Since reflection is used and the reflected fields and methods are not public, changes to the method declarations in the ViewModel or ViewModelStore may result in invalidation in later versions; In case of configuration changes (such as rotating the screen), the ViewModel can still be rebuilt, which is actually the opposite of what the ViewModel was intended to be, but if the App is limited to portrait screens, this can be a better option.