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