注册

把Fragment变成Composable踩坑

把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: ViewGr0up, 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: ViewGr0up, 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 rootGr0up = viewBinding.root as? ViewGr0up
if (rootGr0up != null) {
//递归找到 并且加入集合
findFragmentContainerViews(rootGr0up, 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的导航方式成本很高,如何无缝兼容呢。于是有了如下思考



  • 加载这个Composable Fragment之前可能还有Fragment加载和导航,需要单独的FragmentManager
    val parentFragment = remember(localView) {
    try {
    // 需要依赖 implementation "androidx.fragment:fragment-ktx:1.6.2"
    localView.findFragment<Fragment>().takeIf { it.isAdded }
    } catch (e: IllegalStateException) {
    // findFragment throws if no parent fragment is found
    null
    }
    }
    val localContext = LocalContext.current
    //如果有还有父Fragment就使用childFragmentManager,
    //如果没有说明是第一个Fragment用supportFragmentManager
    val fragmentManager = parentFragment?.childFragmentManager
    ?: (localContext as? FragmentActivity)?.supportFragmentManager
    //加载Composable Fragment
    val fragment = ...
    fragmentManager
    ?.beginTransaction()
    ?.setReorderingAllowed(true)
    ?.add(id, fragment, fragment.javaClass.name)
    ?.commitAllowingStateLoss()


  • 子Fragment若用parentFragment childFragmentManager管理,不需要额外处理
  • 子Fragment若用parentFragment fragmentManager管理,需要监听的出入堆栈,在Composable销毁时候处理所有堆栈中的子fragment
    val attachListener = remember {
    FragmentOnAttachListener { _, fragment ->
    Log.d("FragmentComposable", "fragment: $fragment")
    }
    }
    fragmentManager?.addFragmentOnAttachListener(attachListener)


  • 实际操作中parentFragmentManager实现的子Fragment导航,中间会发生popback,如何防止出栈的Fragment出现内存泄露问题
    val fragments = remember { mutableListOf<WeakReference<Fragment>>() }
    FragmentOnAttachListener { _, fragment ->
    Log.d("FragmentComposable", "fragment: $fragment")
    fragments += WeakReference(fragment)
    }


  • 实际操作中 beginTransaction().remove(childFragment)只会执行子fragment的onDestoryView方法,onDestory不触发,原来是加载子fragment用了addToBackStack,需要调用popBackStack
    DisposableEffect(localContext) {
    val fragmentManager = ...
    onDispose {
    //回退栈到AndroidView的Fragment
    fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
    }
    }



Final Option


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) }
)
}

注意事项



  • 使用上面的代码加载的Fragment(父),若里面导航子Fragment,必须使用parentFragment一样fragmentManager 或者 parentFragment的childFragmentManager
  • 如果子Fragment使用了FragmentActivity?.supportFragmentManager,而parentFragment.fragmentManager不是这个,就会导致子Fragment的生命周期异常。

转载声明


未授权禁止转载和二次修改发布(最近发现有人搬运我的文章,并且改为自己原创,脸都不要了。)如果上面的代码有Bug,请在评论区留言。


作者:forJrking
来源:juejin.cn/post/7312266765123272744

0 个评论

要回复文章请先登录注册