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