Skip to content

Instantly share code, notes, and snippets.

@pbk20191
Last active May 4, 2024 04:13
Show Gist options
  • Save pbk20191/ffc66221bb72c3d3d68fbde81126ee79 to your computer and use it in GitHub Desktop.
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)
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)
}
@pbk20191
Copy link
Author

pbk20191 commented Jan 31, 2024

Can use this code like below. (Migrating Fragment to compose but with large restriction)

  • can not use navGraphViewModel since Fragment.navGraphViewModel looks for NavHostFragment
  • call Navigation.setNavController manually before using findNavController()
  • can not use Fragment'ssavedStateHandle or fragmentResult to communicate with different route => should use NavBackStackEntry's SavedStateHandle
  • can use Hilt Fragment
  • Fragment's state and viewModel are eventually saved to NavBackStackEntry not Root FragmentManager

@Composable
fun NavigationSample(fragmentManager: FragmentManager) {
    val navController = rememberNavController()
    NavHost(
        navController = navController, 
        startDestination = "start", 
        builder = {
            composable("start") {
                fragmentManager.FragmentBridge(
                    modifier = Modifier.wrapContentSize(), 
                    onUpdate = { fragmentManager, container ->  
                        if (fragmentManager.findFragmentByTag(it.id) == null) {
                            fragmentManager.commitNow { 
                                replace<FeatureFragment>(container, it.id, it.arguments)
                            }
                        }
                        val feature = fragmentManager.findFragmentByTag(it.id)!!
                    }
                )
            }
            composable("compose") {
                fragmentManager.FragmentBridge(
                    modifier = Modifier.wrapContentSize(),
                    onUpdate = { fragmentManager, container ->
                        if (fragmentManager.findFragmentByTag(it.id) == null) {
                            fragmentManager.commitNow {
                                replace<ComposeFragment>(container, it.id, it.arguments)
                            }
                        }
                        
                        val composeView = fragmentManager.findFragmentByTag(it.id)?.requireView() as ComposeView
                        composeView.setContent { 
                            
                        }
                        
                    }
                )
            }
            
        }
    )
}

class ComposeFragment: Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View = ComposeView(inflater.context).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) }
    
}

class FeatureFragment: Fragment()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment