Last active
May 4, 2024 04:13
-
-
Save pbk20191/ffc66221bb72c3d3d68fbde81126ee79 to your computer and use it in GitHub Desktop.
Composable to host fragment, its state is managed by Compose ( not by FragmentManager)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package androidx.fragment.app | |
import android.content.Context | |
import android.os.Bundle | |
import android.view.LayoutInflater | |
import android.view.View | |
import android.view.ViewGroup | |
import androidx.activity.ComponentActivity | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.DisposableEffect | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.rememberCompositionContext | |
import androidx.compose.runtime.rememberUpdatedState | |
import androidx.compose.runtime.saveable.Saver | |
import androidx.compose.runtime.saveable.rememberSaveable | |
import androidx.compose.runtime.setValue | |
import androidx.compose.runtime.currentCompositeKeyHash | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.platform.LocalContext | |
import androidx.compose.ui.platform.LocalLifecycleOwner | |
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner | |
import androidx.compose.ui.platform.compositionContext | |
import androidx.compose.ui.viewinterop.AndroidView | |
import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks | |
import androidx.lifecycle.Lifecycle | |
import androidx.lifecycle.ViewModel | |
import androidx.lifecycle.ViewModelProvider | |
import androidx.lifecycle.ViewModelStore | |
import androidx.lifecycle.viewmodel.CreationExtras | |
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner | |
import androidx.lifecycle.viewmodel.compose.viewModel | |
import java.util.Collections | |
import java.util.UUID | |
import java.util.WeakHashMap | |
/** | |
* installs Bridge for Compose & Fragment. [LocalViewModelStoreOwner], [LocalSavedStateRegistryOwner], [LocalLifecycleOwner] manages the Container. | |
* | |
* This method is designed to be use inside the compose navigation graph | |
* | |
* It's caller's responsibility to keep the caller fragmentManager consistent. | |
* | |
* Bridge's viewModel will be destroyed if [LocalViewModelStoreOwner] is destroyed or disposed while [LocalLifecycleOwner] is at least [Lifecycle.State.STARTED] | |
* | |
* [Fragment.setRetainInstance] is not supported and will operate like normal fragment lifecycle. | |
* | |
* [LocalViewModelStoreOwner], [LocalSavedStateRegistryOwner],[LocalLifecycleOwner] can be neither [android.app.Activity] nor [Fragment] nor [FragmentViewLifecycleOwner], | |
* [androidx.navigation.NavBackStackEntry] should be preferred. | |
* */ | |
@Composable | |
public fun FragmentManager.FragmentBridge( | |
modifier:Modifier, | |
onUpdate:(FragmentManager,Int) -> Unit, | |
onReuse:((FragmentManager,Int) -> Unit)? = null | |
) { | |
val context = LocalContext.current | |
val lifecycleHolder = rememberUpdatedState(newValue = LocalLifecycleOwner.current.lifecycle) | |
val viewModelOwner = LocalViewModelStoreOwner.current | |
var marked by remember{ | |
mutableStateOf(false) | |
} | |
val currentParent by rememberUpdatedState(newValue = this) | |
val subComposition = rememberCompositionContext() | |
val viewId = currentCompositeKeyHash | |
val tag = currentCompositeKeyHash.toString() | |
check( | |
LocalViewModelStoreOwner.current !is ComponentActivity | |
&& LocalViewModelStoreOwner.current !is Fragment | |
&& LocalViewModelStoreOwner.current !is FragmentViewLifecycleOwner | |
) { | |
"LocalViewModelStoreOwner can be neither Activity nor Fragment" | |
} | |
check(LocalSavedStateRegistryOwner.current !is ComponentActivity | |
&& LocalSavedStateRegistryOwner.current !is Fragment | |
&& LocalSavedStateRegistryOwner.current !is FragmentViewLifecycleOwner | |
) { | |
"LocalSavedStateRegistryOwner can be neither Activity nor Fragment" | |
} | |
check( | |
LocalLifecycleOwner.current !is ComponentActivity | |
&& LocalLifecycleOwner.current !is Fragment | |
&& LocalLifecycleOwner.current !is FragmentViewLifecycleOwner | |
) { | |
"LocalLifecycleOwner can be neither Activity nor Fragment" | |
} | |
val viewModel = viewModel( | |
key = tag, | |
initializer = { ReferenceViewModel() } | |
) | |
// save hosting fragment state to composable saveable | |
val stateHolder = rememberSaveable( | |
saver = Saver( | |
save = { | |
val ref = currentParent.findFragmentByTag(tag) | |
var stateHolder = Bundle() | |
if (ref != null) { | |
val fragmentManager = ref.parentFragmentManager | |
if (!ref.childFragmentManager.isStateSaved) { | |
val savedState = fragmentManager.saveFragmentInstanceState(ref)?.mState | |
if (savedState != null) { | |
fragmentManager.fragmentStore.setSavedState(ref.mWho, savedState) | |
} | |
stateHolder = Bundle(savedState) | |
} else { | |
val bundle = fragmentManager.fragmentStore.getSavedState(ref.mWho) | |
if (bundle != null) { | |
stateHolder = Bundle(bundle) | |
} | |
} | |
} | |
stateHolder.takeUnless { it.isEmpty } | |
}, | |
restore = { it } | |
) | |
){ | |
Bundle() | |
} | |
val countingCallback = remember{ | |
object: FragmentLifecycleCallbacks() { | |
override fun onFragmentAttached( | |
fm: FragmentManager, | |
f: Fragment, | |
context: Context | |
) { | |
viewModel.fragmentSet.add(f.mWho) | |
} | |
} | |
} | |
val lifecycleCallback = remember{ | |
object: FragmentLifecycleCallbacks() { | |
override fun onFragmentPreCreated( | |
fm: FragmentManager, | |
f: Fragment, | |
savedInstanceState: Bundle? | |
) { | |
if (f.tag == tag) { | |
savedInstanceState?.remove(keyId) | |
} | |
} | |
override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) { | |
// unlink the viewModel store graph from fragmentManager and inject the viewModel we own | |
if (f.tag == tag) { | |
viewModel.fragmentSet.clear() | |
f.childFragmentManager.registerFragmentLifecycleCallbacks(countingCallback, false) | |
f.childFragmentManager.fragmentStore.nonConfig = viewModel.fragmentVM | |
val field = (FragmentManager::class.java.declaredFields + FragmentManager::class.java.fields).first { | |
it.type == FragmentManagerViewModel::class.java && it.name == "mNonConfig" | |
} | |
field.isAccessible = true | |
field[f.childFragmentManager] = viewModel.fragmentVM | |
// f.childFragmentManager.mNonConfig = viewModel | |
field.isAccessible = false | |
viewModel.fragmentVM.setIsStateSaved(true) | |
} | |
} | |
override fun onFragmentViewCreated( | |
fm: FragmentManager, | |
f: Fragment, | |
v: View, | |
savedInstanceState: Bundle? | |
) { | |
if (f.tag == tag) { | |
v.id = viewId | |
v.compositionContext = subComposition | |
if (v is FragmentContainerView) { | |
f.childFragmentManager.onContainerAvailable(v) | |
} | |
marked = true | |
} | |
} | |
override fun onFragmentViewDestroyed(fm: FragmentManager, f: Fragment) { | |
if (f.tag == tag) { | |
marked = false | |
} | |
} | |
override fun onFragmentSaveInstanceState( | |
fm: FragmentManager, | |
f: Fragment, | |
outState: Bundle | |
) { | |
if (f.tag == tag) { | |
if (fm.isStateSaved) { | |
outState.putBoolean(keyId, true) | |
} else { | |
outState.remove(keyId) | |
} | |
} | |
} | |
override fun onFragmentDetached(fm: FragmentManager, f: Fragment) { | |
if (f.tag == tag) { | |
fm.unregisterFragmentLifecycleCallbacks(this) | |
} | |
} | |
} | |
} | |
// check if Dipose is happening due to the lifecycle event or layout changing event | |
// lifecycle event => navigating, state saving and finishing | |
DisposableEffect(Unit) { | |
onDispose { | |
if (lifecycleHolder.value.currentState.isAtLeast(Lifecycle.State.STARTED)) { | |
removeAndClearViewModel(viewModelOwner!!.viewModelStore, tag) | |
} | |
} | |
} | |
DisposableEffect(currentParent) { | |
val fragmentManager = currentParent | |
fragmentManager.registerFragmentLifecycleCallbacks(lifecycleCallback, false) | |
val previous = fragmentManager.findFragmentByTag(tag) | |
if (previous == null) { | |
val fragment = fragmentManager.fragmentFactory.instantiate(context.classLoader, HostingFragment::class.java.name) | |
if (!stateHolder.isEmpty) { | |
fragment.setInitialSavedState(Fragment.SavedState(stateHolder)) | |
} | |
fragmentManager.commit { | |
add(0, fragment, tag) | |
setReorderingAllowed(true) | |
disallowAddToBackStack() | |
} | |
} | |
onDispose { | |
val ref = fragmentManager.findFragmentByTag(tag) | |
if (ref != null) { | |
val keySet = ref.childFragmentManager.fragmentStore.allSavedState.keys.toSet() | |
viewModel.fragmentSet.removeIf{ | |
!keySet.contains(it) && ref.childFragmentManager.findFragmentByWho(it) == null | |
} | |
viewModel.fragmentSet.forEach { | |
val child = ref.childFragmentManager.findFragmentByWho(it) | |
if (child != null) { | |
val childConfig = viewModel.fragmentVM | |
if (!childConfig.isCleared) { | |
viewModel.fragmentVM.getChildNonConfig(child).onCleared() | |
} | |
} | |
} | |
if (!fragmentManager.isStateSaved) { | |
fragmentManager.commitNow { | |
remove(ref) | |
setReorderingAllowed(true) | |
disallowAddToBackStack() | |
} | |
} | |
} | |
} | |
} | |
if (marked) { | |
AndroidView( | |
modifier = modifier, | |
factory = { currentParent.findFragmentByTag(tag)!!.requireView() }, | |
update = { | |
val fragmentManager = it.findFragment<Fragment>().childFragmentManager | |
onUpdate(fragmentManager, viewId) | |
}, | |
onReset = if (onReuse == null) null else { | |
{ | |
it.compositionContext = subComposition | |
val host = it.findFragment<Fragment>() | |
onUpdate(host.childFragmentManager, viewId) | |
} | |
} | |
) | |
} | |
} | |
private final class ReferenceViewModel public constructor():ViewModel() { | |
val fragmentSet = Collections.newSetFromMap(WeakHashMap<String,Boolean>()) as MutableSet<String> | |
val fragmentVM = FragmentManagerViewModel(true) | |
override fun onCleared() { | |
super.onCleared() | |
fragmentVM.onCleared() | |
fragmentSet.forEach { | |
fragmentVM.clearNonConfigState(it, true) | |
} | |
fragmentSet.clear() | |
} | |
} | |
private const val keyId = "androidx.app.fragment.Hosting::destroy" | |
public final class HostingFragment public constructor(): Fragment() { | |
// we can not prevent the fragment from being saved, so we destory it immediately during restoratin | |
// this is safe because ower viewModel and saved state is not managed by fragment graph | |
override fun onCreate(savedInstanceState: Bundle?) { | |
if (savedInstanceState?.getBoolean(keyId) == true) { | |
mSavedFragmentState = Bundle() | |
parentFragmentManager.commit { | |
remove(this@HostingFragment) | |
disallowAddToBackStack() | |
setReorderingAllowed(true) | |
} | |
} | |
super.onCreate(savedInstanceState) | |
} | |
override fun onSaveInstanceState(outState: Bundle) { | |
super.onSaveInstanceState(outState) | |
if (parentFragmentManager.isStateSaved) { | |
outState.putBoolean(keyId, true) | |
} | |
} | |
override fun onCreateView( | |
inflater: LayoutInflater, | |
container: ViewGroup?, | |
savedInstanceState: Bundle? | |
): View { | |
return FragmentContainerView(inflater.context) | |
} | |
} | |
private fun removeAndClearViewModel(viewModelStore: ViewModelStore, key:String) { | |
val viewModel = ViewModelProvider(viewModelStore, ViewModelProvider.NewInstanceFactory())[key, ViewModel::class.java] | |
val trash = ViewModelStore() | |
ViewModelProvider( | |
trash, | |
object: ViewModelProvider.Factory { | |
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T { | |
return viewModel as T | |
} | |
}, | |
)[key, ViewModel::class.java] | |
trash.clear() | |
val mapField = ViewModelStore::class.java.let { | |
it.declaredFields + it.fields | |
}.first { | |
it.type == MutableMap::class.java | |
} | |
mapField.isAccessible = true | |
val map = mapField[viewModelStore] as MutableMap<*, *> | |
mapField.isAccessible = false | |
map.remove(key) | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Can use this code like below. (Migrating Fragment to compose but with large restriction)
navGraphViewModel
sinceFragment.navGraphViewModel
looks forNavHostFragment
Navigation.setNavController
manually before usingfindNavController()
Fragment
'ssavedStateHandle
or fragmentResult to communicate with different route => should useNavBackStackEntry
'sSavedStateHandle
Hilt
FragmentNavBackStackEntry
not RootFragmentManager