Reputation: 670
I'm playing around with compose and tried to include a fragment inside a compose AndroidView.
So in my situation, we have AFragment with a ComposeView and inside the ComposeView there is an AndroidView which create a FragmentContainerView and add a PIFragment.
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setContent {
AppTheme {
GalleryScreen(
factory = viewModelFactory,
remoteConfig = remoteConfig,
id = id,
currentPosition = currentPositionState,
onBack = { router.back(requireActivity()) },
) {
AndroidView(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
factory = {
FragmentContainerView(context).apply {
id = R.id.pFIC
}
},
update = {
childFragmentManager.beginTransaction().replace(
R.id.pFIC,
PIFragment::class.java,
buildArguments(
id = id,
origin = origin,
), null
).commitAllowingStateLoss()
},
)
}
}
}
}
}
All was working fine, but when we publish this code in production, we saw crash in firebase:
java.lang.IllegalArgumentException: No view found for id 0x7f0b072b (...:id/pFIC) for fragment PIFragment{ef1f89b} (bdbe15f0-679d-41bb-8a27-367655f73545 id=0x7f0b072b)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:513)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282)
at androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:112)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1647)
at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3128)
at androidx.fragment.app.FragmentManager.dispatchViewCreated(FragmentManager.java:3065)
at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:2988)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:546)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2180)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2100)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2002)
at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:524)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
Upvotes: 7
Views: 3879
Reputation: 4083
Since version 1.8.1 there is an AndroidFragment
composable similar to the AndroidView
composable, to do this kind of thing.
https://developer.android.com/jetpack/androidx/releases/fragment#1.8.0
Example:
AndroidFragment(clazz = PIFragment::class.java)
As simple as the above, and you have other parameters in there to pass whatever bundle arguments you need or if you need to do something when it updates through the FragmentManager.
Here you have a few more examples:
https://github.com/search?q=AndroidFragment++language%3AKotlin&type=code
Upvotes: 5
Reputation: 173
I faced this exception too. I modified the sagix's answer to more convenient way for me. Check it out below. Don't forget use a childFragmentManager
@Composable
fun ReusableFragmentComponent(
screen: SupportAppScreen,
fragmentManager: FragmentManager,
modifier: Modifier = Modifier
) {
val containerId by rememberSaveable { mutableStateOf(View.generateViewId()) }
AndroidView(
modifier = modifier,
factory = { context ->
FragmentContainerView(ContextThemeWrapper(context, context.currentStyle())).apply {
id = containerId
}
},
update = {
val fragmentAlreadyAdded = fragmentManager.findFragmentByTag(screen.screenKey) != null
if (!fragmentAlreadyAdded) {
fragmentManager
.beginTransaction()
.replace(containerId, screen.getFragment(), screen.screenKey)
.commit()
}
})
DisposableEffect(LocalLifecycleOwner.current) {
onDispose {
fragmentManager.findFragmentById(containerId)?.let { fragment ->
fragmentManager
.beginTransaction()
.remove(fragment)
.commitAllowingStateLoss()
}
}
}
}
Upvotes: 1
Reputation: 1774
You should also be using AndroidViewBinding
instead of AndroidView
for adding a fragment in Compose. AndroidViewBinding
has fragment-specific handling that will ensure that fragments are removed when the containing composable is disposed of. Note that this requires view binding to be enabled.
Define your FragmentContainerView in XML like so:
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container_view"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:name="com.example.MyFragment" />
...and then in your composable function:
@Composable
fun FragmentInComposeExample() {
AndroidViewBinding(MyFragmentLayoutBinding::inflate) {
val myFragment = fragmentContainerView.getFragment<MyFragment>()
// ...
}
}
You can read Fragment in Compose for more info.
Upvotes: 4
Reputation: 670
In fact, the crash was happening after the parent fragment was recreated.
In my case because of navigation:
Crash, because the fragment manager is trying to recreate AFragment and PIFragment, but the compose pass is not done so pFIC does not exist yet.
The solution was to remove the PIFragment when the parent fragment view is destroyed.
override fun onDestroyView() {
childFragmentManager.findFragmentById(R.id.pFIC)?.let { fragment ->
childFragmentManager.beginTransaction().remove(fragment).commitAllowingStateLoss()
}
super.onDestroyView()
}
Upvotes: 8