sagix
sagix

Reputation: 670

Crash when using fragment in AndroidView using compose

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

Answers (4)

4gus71n
4gus71n

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

knapweed1
knapweed1

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

Chris Arriola
Chris Arriola

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

sagix
sagix

Reputation: 670

In fact, the crash was happening after the parent fragment was recreated.

In my case because of navigation:

  • AFragment (with PIFragment)
  • navigate to BFragment
  • back to AFragment

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

Related Questions