把Fragment变成Composable踩坑

Why

在编写Compose时分如果遇到需求加载其他Fragment就比较费事,并且很多时分这种Fragment还是xml或许第三方SDK供给的。下面供给一些解决计划。

Option 1

google也意识到这个问题,所以供给了AndroidViewBinding,能够把Fragment经过包装成AndroidView,就能够在Composable中随意运用了。AndroidViewBinding在组合项退出组合时会移除 fragment。

官方文档:Compose 中的 fragment

//源码
@Composable
fun <T : ViewBinding> AndroidViewBinding(
    factory: (inflater: LayoutInflater, parent: ViewGroup, attachToParent: Boolean) -> T,
    modifier: Modifier = Modifier,
    update: T.() -> Unit = {} //view inflate 完结时分回调
) { ...
  • 首先需求增加ui-viewbinding依靠,并且开启viewBinding
// gradle
buildFeatures {
    ...
    viewBinding true
}
...
implementation("androidx.compose.ui:ui-viewbinding")
  • 创建xml布局,在android:name="MyFragment"增加Fragment的名字和包名路径
<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" />
  • 在Composable函数中如下调用,如果您需求在同一布局中运用多个 fragment,请确保您已为每个FragmentContainerView定义唯一 ID。
@Composable
fun FragmentInComposeExample() {
  AndroidViewBinding(MyFragmentLayoutBinding::inflate) {
    val myFragment = fragmentContainerView.getFragment<MyFragment>()
    // ...
  }
}

这种办法默许支持空构造函数的Fragment,如果是带有参数或许需求arguments传递数据的,需求改造成调用办法传递或许callbak办法,官方主张运用FragmentFactory。

class MyFragmentFactory extends FragmentFactory {
   @NonNull
   @Override
   public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) {
       Class extends Fragment> clazz = loadFragmentClass(classLoader, className);
       if (clazz == MainFragment.class) {
       //这次处理传递参数
          return new MainFragment(anyArg1, anyArg2);
       } else {
           return super.instantiate(classLoader, className);
       }
   }
}
//运用
getSupportFragmentManager().setFragmentFactory(fragmentFactory)

请参考此文:FragmentFactory :功用详解&运用场景

Option 2

如果咱们能够new Fragment或许有fragment实例,怎么加载到Composable中呢。

思路:fragmentManager把framgnt add之后,fragment自己getView,然后包装成AndroidView即可。修改下AndroidViewBinding源码就能够得到如下代码:

@Composable
fun FragmentComposable(
    fragment: Fragment,
    modifier: Modifier = Modifier,
    update: (Fragment) -> Unit = {}
) {
    val fragmentTag = remember { mutableStateOf(fragment.javaClass.name) }
    val localContext = LocalContext.current
    AndroidView(
        modifier = modifier,
        factory = { context ->
            require(!fragment.isAdded) { "fragment must not attach to any host" }
            (localContext as? FragmentActivity)?.supportFragmentManager
                ?.beginTransaction()
                ?.setReorderingAllowed(true)
                ?.add(fragment, fragmentTag.value)
                ?.commitNowAllowingStateLoss()
            fragment.requireView()
        },
        update = { update(fragment) }
    )
    DisposableEffect(localContext) {
        val fragmentManager = (localContext as? FragmentActivity)?.supportFragmentManager
        val existingFragment = fragmentManager?.findFragmentByTag(fragmentTag.value)
        onDispose {
            if (existingFragment != null && !fragmentManager.isStateSaved) {
                // If the state isn't saved, that means that some state change
                // has removed this Composable from the hierarchy
                fragmentManager
                    .beginTransaction()
                    .remove(existingFragment)
                    .commitAllowingStateLoss()
            }
        }
    }
}

Issue Note

其实里面有个巨坑。如果你的Fragment中还经过fragmentManager进行了navigation的完成,你会发现你的其他Fragment生命周期会反常,回来了却onDestoryView,onDestory不回调。

  • 计划1中 官方主张把一切的子Fragment经过childFragmentManager来加载,这姿态Fragment依靠与父目标,当父亲被回退出去后,子类Fragment悉数自动毁掉了,会正常被childFragmentManager处理生命周期。

  • 计划1中 Fragment嵌套需求用FragmentContainerView来包装持有。下面是源码解析,只保留了核心处理的地方

@Composable
fun <T : ViewBinding> AndroidViewBinding(
    factory: (inflater: LayoutInflater, parent: ViewGroup, attachToParent: Boolean) -> T,
    modifier: Modifier = Modifier,
    update: T.() -> Unit = {}
) {
    // fragmentContainerView的调集
    val fragmentContainerViews = remember { mutableStateListOf<FragmentContainerView>() }
    val viewBlock: (Context) -> View = remember(localView) {
        { context ->
            ...
            val viewBinding = ...
            fragmentContainerViews.clear()
            val rootGroup = viewBinding.root as? ViewGroup
            if (rootGroup != null) {
            //递归找到 并且参加调集
                findFragmentContainerViews(rootGroup, fragmentContainerViews)
            }
            viewBinding.root
        }
    }
    ...
    //遍历一切找到View每个都注册一个 DisposableEffect用来处理毁掉
    fragmentContainerViews.fastForEach { container ->
        DisposableEffect(localContext, container) {
            // Find the right FragmentManager
            val fragmentManager = parentFragment?.childFragmentManager
                ?: (localContext as? FragmentActivity)?.supportFragmentManager
            // Now find the fragment inflated via the FragmentContainerView
            val existingFragment = fragmentManager?.findFragmentById(container.id)
            onDispose {
                if (existingFragment != null && !fragmentManager.isStateSaved) {
                    // If the state isn't saved, that means that some state change
                    // has removed this Composable from the hierarchy
                    fragmentManager.commit {
                        remove(existingFragment)
                    }
                }
            }
        }
    }
}

考虑和完善

很多时分咱们的业务很杂乱改动Fragment的导航办法本钱很高,怎么无缝兼容呢。于是有了如下考虑

  • 监听Fragment的出入仓库,在Composable毁掉时分处理一切仓库中的fragment
  • 子Fragment是经过childFragmentManager处理不需求而外处理,只需求管理parentFragment
  • 实际操作中parentFragmentManager完成的导航,中心会产生popback,怎么防止出栈的Fragment呈现内存走漏问题
  • 实际操作中 fragmentManager.beginTransaction().remove(existingFragment)只会履行fragment的onDestoryView办法,onDestory不触发,原来是用了addToBackStack

终究完成如下

import android.widget.FrameLayout
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.viewinterop.AndroidView
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentOnAttachListener
import androidx.fragment.app.findFragment
import java.lang.ref.WeakReference
/**
 * Make fragment as Composable by AndroidView
 *
 * @param fragment fragment
 * @param fm add fragment by FragmentManager, can be childFragmentManager
 * @param update The callback to be invoked after the layout is inflated.
 */
@Composable
fun <T : Fragment> FragmentComposable(
    modifier: Modifier = Modifier,
    fragment: T,
    update: (T) -> Unit = {}
) {
    val localView = LocalView.current
    // Find the parent fragment, if one exists. This will let us ensure that
    // fragments inflated via a FragmentContainerView are properly nested
    // (which, in turn, allows the fragments to properly save/restore their state)
    val parentFragment = remember(localView) {
        try {
            localView.findFragment<Fragment>().takeIf { it.isAdded }
        } catch (e: IllegalStateException) {
            // findFragment throws if no parent fragment is found
            null
        }
    }
    val fragments = remember { mutableListOf<WeakReference<Fragment>>() }
    val attachListener = remember {
        FragmentOnAttachListener { _, fragment ->
            Log.d("FragmentComposable", "fragment: $fragment")
            fragments += WeakReference(fragment)
        }
    }
    val localContext = LocalContext.current
    DisposableEffect(localContext) {
        val fragmentManager = parentFragment?.childFragmentManager
            ?: (localContext as? FragmentActivity)?.supportFragmentManager
        fragmentManager?.addFragmentOnAttachListener(attachListener)
        onDispose {
            fragmentManager?.removeFragmentOnAttachListener(attachListener)
            if (fragmentManager?.isStateSaved == false) {
                fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
                fragments
                    .filter { it.get()?.isRemoving == false }
                    .reversed()
                    .forEach { existingFragment ->
                        Log.d("FragmentComposable", "remove:${existingFragment.get()}")
                        fragmentManager
                            .beginTransaction()
                            .remove(existingFragment.get()!!)
                            .commitAllowingStateLoss()
                    }
            }
        }
    }
    AndroidView(
        modifier = modifier,
        factory = { context ->
            FrameLayout(context).apply {
                id = System.currentTimeMillis().toInt()
                require(!fragment.isAdded) { "$fragment must not attach to any host" }
                val fragmentManager = parentFragment?.childFragmentManager
                    ?: (localContext as? FragmentActivity)?.supportFragmentManager
                fragmentManager
                    ?.beginTransaction()
                    ?.setReorderingAllowed(true)
                    ?.replace(this.id, fragment, fragment.javaClass.name)
                    ?.commitAllowingStateLoss()
                fragments.clear()
            }
        },
        update = { update(fragment) }
    )
}