把Fragment变成Composable的踩坑

把Fragment变成Composable的踩坑

Why

在编写Compose时候如果遇到需要加载其他Fragment就比较麻烦,而且很多时候这种Fragment还是xml或者第三方SDK提供的。下面提供一些解决方案。

Option 1

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

官方文档:Compose 中的 fragment

kotlin 复制代码
//源码
@Composable
fun <T : ViewBinding> AndroidViewBinding(
    factory: (inflater: LayoutInflater, parent: ViewGroup, attachToParent: Boolean) -> T,
    modifier: Modifier = Modifier,
    update: T.() -> Unit = {} //view inflate 完成时候回调
) { ...
  • 首先需要添加ui-viewbinding依赖,并且开启viewBinding
kotlin 复制代码
// gradle
buildFeatures {
    ...
    viewBinding true
}
...
implementation("androidx.compose.ui:ui-viewbinding")
  • 创建xml布局,在android:name="MyFragment"添加Fragment的名字和包名路径
xml 复制代码
<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。
kotlin 复制代码
@Composable
fun FragmentInComposeExample() {
    AndroidViewBinding(MyFragmentLayoutBinding::inflate) {
        val myFragment = fragmentContainerView.getFragment<MyFragment>()
        // ...
    }
}

这种方式默认支持空构造函数的Fragment,如果是带有参数或者需要arguments传递数据的,需要改造成调用方法传递或者callbak方式,官方建议使用FragmentFactory。

java 复制代码
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源码就可以得到如下代码:

kotlin 复制代码
@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来包装持有。下面是源码解析,只保留了核心处理的地方

kotlin 复制代码
@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

最终实现如下

kotlin 复制代码
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) }
    )
}
相关推荐
石小石Orz1 分钟前
因为没有使用路由懒加载,产生了一个难以寻找的bug
前端
Mintopia1 分钟前
Three.js 力导向图:让数据跳起优雅的华尔兹
前端·javascript·three.js
墨渊君16 分钟前
React Native 跨平台组件库实践: GlueStack UI 上手指南
前端
晓得迷路了24 分钟前
栗子前端技术周刊第 84 期 - Vite v7.0 beta、Vitest 3.2、Astro 5.9...
前端·javascript·vite
独立开阀者_FwtCoder27 分钟前
最全301/302重定向指南:从SEO到实战,一篇就够了
前端·javascript·vue.js
wu_android29 分钟前
Android 线性布局中常见的冲突属性总结
android
Moment36 分钟前
给大家推荐一个超好用的 Marsview 低代码平台 🤩🤩🤩
前端·javascript·github
小满zs40 分钟前
Zustand 第三章(状态简化)
前端·react.js
普宁彭于晏42 分钟前
元素水平垂直居中的方法
前端·css·笔记·css3
恋猫de小郭1 小时前
为什么跨平台框架可以适配鸿蒙,它们的技术原理是什么?
android·前端·flutter