把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销毁时候处理所有堆栈中的子fragmentval 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
,需要调用popBackStackDisposableEffect(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,请在评论区留言。
来源:juejin.cn/post/7312266765123272744