You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Compose TabRow中Activity重启/配置变更时Fragment空白问题

Compose TabRow结合FragmentContainerView重启后Fragment无法显示

问题描述

在Compose的TabRow中使用FragmentContainerView展示现有Fragment,首次加载时一切正常,但Activity重启后,Fragment虽存在于FragmentManager中却无法显示。受架构限制,只能使用add方式添加Fragment,不能用replace。Fragment A、B、C仅包含一个显示自身名称的TextView。

问题代码

Activity.kt

@Composable
fun MainScreen() {
        var selectedTabIndex by remember { mutableStateOf(0) }
        Column(Modifier.fillMaxSize()) {
            TabRow(
                selectedTabIndex = selectedTabIndex,
                backgroundColor = Color.Gray,
            ) {
                tabs.forEachIndexed { index, title ->
                    Tab(
                        text = {
                            Text(
                                text = title,
                                maxLines = 1,
                                overflow = TextOverflow.Ellipsis
                            )
                        },
                        selected = selectedTabIndex == index,
                        onClick = {
                            selectedTabIndex = index
                        },
                        selectedContentColor = Color.Black,
                        unselectedContentColor = Color.White
                    )
                }
            }
            FragmentContainer(
                modifier = Modifier.fillMaxSize(),
                commit = getCommitFunction(
                    index = selectedTabIndex
                )
            )
        }
    }

    private fun getFragment(index: Int): Fragment {
        return when (index) {
            0 -> FragmentA.newInstance()
            1 -> FragmentB.newInstance()
            else -> FragmentC.newInstance()
        }
    }

    private fun getCommitFunction(
        index: Int,
    ): FragmentTransaction.(containerId: Int) -> Unit =
        { containerId ->
            showPage(this, index, containerId)
        }

    private fun showPage(transaction: FragmentTransaction, position: Int, containerId: Int) {

        // hide current showing fragment
        tabs.forEach { fragmentTag ->
            val fragment: Fragment? = supportFragmentManager.findFragmentByTag(fragmentTag)
            if (fragment != null && fragment.isVisible) {
                transaction.hide(fragment)
            }
        }

        val fragmentTag = tabs[position]
        var fragment: Fragment? = supportFragmentManager.findFragmentByTag(fragmentTag)

        if (fragment == null) {
            // we don't have the fragment yet, need to create it
            fragment = getFragment(position)
            println("containerId $containerId")
            transaction.add(containerId, fragment, fragmentTag)
        } else {
            println("containerId else $containerId")
            // we already created the fragment, just need to show it
            transaction.show(fragment)
        }

        transaction.setPrimaryNavigationFragment(fragment)
    }

FragmentContainer.kt

@Composable
fun FragmentContainer(
    modifier: Modifier = Modifier,
    commit: FragmentTransaction.(containerId: Int) -> 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>()
        } catch (e: IllegalStateException) {
            // findFragment throws if no parent fragment is found
            null
        }
    }
    val containerId by rememberSaveable { mutableStateOf(View.generateViewId()) }
    val container = remember { mutableStateOf<FragmentContainerView?>(null) }
    val viewBlock: (Context) -> View = remember(localView) {
        { context ->
            FragmentContainerView(context).apply { id = containerId }
        }
    }
    AndroidView(
        modifier = modifier,
        factory = viewBlock,
        update = {view ->
            val fragmentManager = parentFragment?.childFragmentManager
                ?: (view.context as? FragmentActivity)?.supportFragmentManager
            fragmentManager?.commit { commit(view.id) }
            container.value = view as FragmentContainerView
        }
    )

    // Set up a DisposableEffect that will clean up fragments when the FragmentContainer is disposed
    val localContext = LocalContext.current
    DisposableEffect(localView, localContext, container) {
        onDispose {
            val fragmentManager = parentFragment?.childFragmentManager
                ?: (localContext as? FragmentActivity)?.supportFragmentManager
            // Now find the fragment inflated via the FragmentContainerView
            val existingFragment = fragmentManager?.findFragmentById(container.value?.id ?: 0)
            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(true) {
                    remove(existingFragment)
                }
            }
        }
    }
}

问题根源

  1. Activity重启后,FragmentManager自动恢复之前的Fragment实例,但这些Fragment的旧宿主容器已销毁,新创建的FragmentContainerView虽ID相同,Fragment却未重新关联到新容器,导致无法显示。
  2. 原代码仅隐藏可见的Fragment,重启后Fragment的isVisible状态异常,导致部分Fragment未被正确隐藏,覆盖目标Fragment。
  3. FragmentContainer的update块在Compose重组时重复执行commit,引发Fragment状态混乱。

修复方案

方案一:修正Fragment显示/隐藏逻辑

修改showPage方法,确保所有已添加的Fragment都被隐藏,同时检查已存在的Fragment是否关联到当前容器,异常时重新关联:

private fun showPage(transaction: FragmentTransaction, position: Int, containerId: Int) {
    // 隐藏所有已添加的Fragment,避免重启后状态异常
    tabs.forEach { fragmentTag ->
        supportFragmentManager.findFragmentByTag(fragmentTag)?.let {
            if (it.isAdded) {
                transaction.hide(it)
            }
        }
    }

    val fragmentTag = tabs[position]
    var fragment: Fragment? = supportFragmentManager.findFragmentByTag(fragmentTag)

    if (fragment == null) {
        fragment = getFragment(position)
        transaction.add(containerId, fragment, fragmentTag)
    } else {
        // 检查Fragment是否关联到当前容器,异常时重新关联
        if (fragment.containerId != containerId) {
            transaction.remove(fragment)
            transaction.add(containerId, fragment, fragmentTag)
        }
        transaction.show(fragment)
    }

    transaction.setPrimaryNavigationFragment(fragment)
}

方案二:简化FragmentContainer逻辑,直接管理Fragment切换

将Fragment的初始化和切换逻辑从FragmentContainer中移出,直接在Tab点击事件中操作FragmentManager,避免Compose重组带来的重复commit:

修改后的Activity.kt

@Composable
fun MainScreen() {
    var selectedTabIndex by rememberSaveable { mutableStateOf(0) }
    val fragmentManager = (LocalContext.current as FragmentActivity).supportFragmentManager
    val containerId by rememberSaveable { mutableStateOf(View.generateViewId()) }

    // 初始化Fragment,仅执行一次
    LaunchedEffect(Unit) {
        tabs.forEachIndexed { index, tag ->
            if (fragmentManager.findFragmentByTag(tag) == null) {
                fragmentManager.commit {
                    add(containerId, getFragment(index), tag)
                    // 默认隐藏非初始Fragment
                    if (index != selectedTabIndex) hide(fragmentManager.findFragmentByTag(tag)!!)
                }
            }
        }
        // 设置初始显示的Fragment
        fragmentManager.commit {
            show(fragmentManager.findFragmentByTag(tabs[selectedTabIndex])!!)
            setPrimaryNavigationFragment(fragmentManager.findFragmentByTag(tabs[selectedTabIndex])!!)
        }
    }

    Column(Modifier.fillMaxSize()) {
        TabRow(
            selectedTabIndex = selectedTabIndex,
            backgroundColor = Color.Gray,
        ) {
            tabs.forEachIndexed { index, title ->
                Tab(
                    text = {
                        Text(
                            text = title,
                            maxLines = 1,
                            overflow = TextOverflow.Ellipsis
                        )
                    },
                    selected = selectedTabIndex == index,
                    onClick = {
                        if (selectedTabIndex != index) {
                            fragmentManager.commit {
                                // 隐藏当前显示的Fragment
                                hide(fragmentManager.findFragmentByTag(tabs[selectedTabIndex])!!)
                                // 显示目标Fragment
                                show(fragmentManager.findFragmentByTag(tabs[index])!!)
                                setPrimaryNavigationFragment(fragmentManager.findFragmentByTag(tabs[index])!!)
                            }
                            selectedTabIndex = index
                        }
                    },
                    selectedContentColor = Color.Black,
                    unselectedContentColor = Color.White
                )
            }
        }
        // 简化后的FragmentContainer仅负责创建容器
        FragmentContainer(
            modifier = Modifier.fillMaxSize(),
            containerId = containerId
        )
    }
}

private fun getFragment(index: Int): Fragment {
    return when (index) {
        0 -> FragmentA.newInstance()
        1 -> FragmentB.newInstance()
        else -> FragmentC.newInstance()
    }
}

修改后的FragmentContainer.kt

@Composable
fun FragmentContainer(
    modifier: Modifier = Modifier,
    containerId: Int
) {
    AndroidView(
        modifier = modifier,
        factory = { context ->
            FragmentContainerView(context).apply { id = containerId }
        }
    )
}

该方案将Fragment管理逻辑从Compose重组中完全剥离,仅在初始化时创建Fragment,后续切换直接操作FragmentManager,彻底避免了重启后的状态异常问题。


内容的提问来源于stack exchange,提问作者coolswap11

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 23:45:54