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

从详情页返回时恢复Jetpack Compose前一个Composable的状态

Jetpack Compose 导航返回后页面状态丢失问题

我在Jetpack Compose中遇到了状态与导航相关的问题:带搜索栏的页面A,点击搜索结果跳转到详情页面,返回页面A时会回到初始状态(搜索栏清空、无搜索结果)。尝试过saveState和restoreState但无效,不确定是导航配置还是其他问题。

提供的代码

SharedTransitionLayout {
    NavHost(
        modifier = Modifier.fillMaxSize().padding(padding).background(MaterialTheme.colorScheme.background),
        navController = navController,
        startDestination = CardSearchRoute
    ) {

        composable<CardSearchRoute> {
            val viewModel = koinViewModel<CardSearchViewModel>()
            val state by viewModel.state.collectAsStateWithLifecycle()
            CardSearchScreen(
                state = state,
                onEvent = viewModel::onEvent,
                animatedVisibilityScope = this,
                onCardClicked = {
                    navController.navigate(CardDetailRoute(cardID = it.id))
                }
            )

        }

        composable<CardDetailRoute> { backStackEntry ->
            val cardID = backStackEntry.toRoute<CardDetailRoute>().cardID
            val viewmodel = koinViewModel<CardDetailViewModel>(parameters = { parametersOf(cardID) })
            val state by viewmodel.state.collectAsStateWithLifecycle()
            CardDetailScreen(
                state = state, animatedVisibilityScope = this,
                onEvent = {}
            )
        }

        composable<ComingSoonRoute> {
            todoScreen("Collection")
        }

        composable<ScanRoute> {
            todoScreen("Scanning")
        }
    }
}

搜索页面代码

@OptIn(ExperimentalSharedTransitionApi::class, ExperimentalMaterial3Api::class)
@Composable
fun SharedTransitionScope.CardSearchScreen(
    state: UIState,
    onEvent: (UIEvent) -> Unit,
    onCardClicked: (Card) -> Unit,
    animatedVisibilityScope: AnimatedVisibilityScope
) {


    LaunchedEffect(Unit) {
        onEvent(UIEvent.OnSearchBarTextChanged(""))
    }


    Column(
        modifier = Modifier
            .fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {

        SearchBar(
            windowInsets = WindowInsets(top = 0),
            modifier = Modifier.padding(16.dp).fillMaxWidth(),
            shape = RoundedCornerShape(corner = CornerSize(8.dp)),
            inputField = {
                SearchBarDefaults.InputField(
                    query = state.searchBarText ?: "",
                    onQueryChange = {
                        onEvent(UIEvent.OnSearchBarTextChanged(it))
                    },
                    onSearch = {  },
                    expanded = false,
                    onExpandedChange = {  },
                    placeholder = { Text("Search by name") },
                    colors = TextFieldDefaults.colors(
                        focusedIndicatorColor = Color.White,
                        cursorColor = Color.White,
                        focusedLabelColor = Color.White, unfocusedPlaceholderColor = Color.White.copy(alpha = 0.5f), focusedPlaceholderColor = Color.White.copy(alpha = 0.5f)
                    ),
                    leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
                )
            },
            expanded = false,
            onExpandedChange = {  },
        ) { }

        Box(
            modifier = Modifier.fillMaxHeight()
        ) {
            if (state.isLoading) {
                CircularProgressIndicator(
                    modifier = Modifier.align(Alignment.Center)
                )
            } else {
                val cards = state.foundCards.map { it }.toSet()
                LazyColumn (
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    item{
                        CardCarrousel(
                            title = "LEADERS",
                            cards = cards,
                            type = CardType.LEADER,
                            onCardClicked = onCardClicked,
                            animatedVisibilityScope = animatedVisibilityScope
                        )
                    }

                    item{
                        CardCarrousel(
                            title = "BASES",
                            cards = cards,
                            type = CardType.BASE,
                            onCardClicked = onCardClicked,
                            animatedVisibilityScope = animatedVisibilityScope

                        )
                    }

                    item{
                        CardCarrousel(
                            title = "UNITS",
                            cards = cards,
                            type = CardType.UNIT,
                            onCardClicked = onCardClicked,
                            animatedVisibilityScope = animatedVisibilityScope

                        )
                    }

                    item{
                        CardCarrousel(
                            title = "EVENTS",
                            cards = cards,
                            type = CardType.EVENT,
                            onCardClicked = onCardClicked,
                            animatedVisibilityScope = animatedVisibilityScope

                        )
                    }

                    item{
                        CardCarrousel(
                            title = "UPGRADES",
                            cards = cards,
                            type = CardType.UPGRADE,
                            onCardClicked = onCardClicked,
                            animatedVisibilityScope = animatedVisibilityScope

                        )
                    }
                }

            }
        }

    }

}

ViewModel 代码

class CardSearchViewModel(
    private val searchCardUseCase: SearchCardUseCase
) : ViewModel() {

    private val _state = MutableStateFlow(UIState())
    val state = _state.asStateFlow()


    fun onEvent(event: UIEvent) {
        when (event) {
            is UIEvent.OnSearchBarTextChanged ->{
                _state.update {
                    it.copy(
                        searchBarText = event.text
                    )
                }
                searchJob?.cancel()
                search()
            }

        }
    }

    var searchJob: Job? = null

    private fun search() {
        searchJob = viewModelScope.launch(Dispatchers.IO) {
            searchCardUseCase.invoke(_state.value.searchBarText!!).collect { result ->
                result.onLoading { _, _  ->
                    _state.update {
                        it.copy(
                            isLoading = true,
                            foundCards = emptySet()
                        )
                    }
                }.onSuccess { foundCards ->
                    _state.update {
                        it.copy(
                            isLoading = false,
                            foundCards = foundCards.sortedBy { it.name }.toSet()
                        )
                    }
                }.onFailure {
                    _state.update {
                        it.copy(
                            isLoading = false
                        )
                    }
                }
            }
        }
    }

}

data class UIState(
    val searchBarText: String? = null,
    val isLoading: Boolean = false,
    val foundCards: Set<Card> = emptySet(),
    val sortType: SortType = SortType.NAME
)
sealed class UIEvent {
    data class OnSearchBarTextChanged(val text: String) : UIEvent()
}

问题根源与修复方案

问题根源

  1. ViewModel生命周期不匹配:当前使用koinViewModel()获取ViewModel时,默认会在页面进入后台(被压入导航栈)时销毁,返回时重建,导致状态丢失。
  2. 强制初始化清空状态:搜索页面中的LaunchedEffect(Unit)会在每次Composable重组(包括返回时)触发,强制清空搜索栏文本,重置状态。

修复步骤

1. 确保ViewModel在导航栈中保留

修改NavHost中CardSearchRoute的ViewModel获取方式,让ViewModel与当前导航栈条目绑定,确保返回时复用原有实例:

composable<CardSearchRoute> { backStackEntry ->
    val viewModel = koinViewModel<CardSearchViewModel>(scope = backStackEntry)
    val state by viewModel.state.collectAsStateWithLifecycle()
    CardSearchScreen(
        state = state,
        onEvent = viewModel::onEvent,
        animatedVisibilityScope = this,
        onCardClicked = {
            navController.navigate(CardDetailRoute(cardID = it.id))
        }
    )
}

2. 移除强制清空状态的代码

删除搜索页面中的LaunchedEffect(Unit)代码块,避免返回时重置搜索状态:

// 移除这段代码
LaunchedEffect(Unit) {
    onEvent(UIEvent.OnSearchBarTextChanged(""))
}

如果需要页面首次加载时初始化为空搜索,将逻辑移至ViewModel的init块:

class CardSearchViewModel(
    private val searchCardUseCase: SearchCardUseCase
) : ViewModel() {
    init {
        onEvent(UIEvent.OnSearchBarTextChanged(""))
    }

    // 其余代码不变
}

3. 可选:配置变更时的状态保存

如果需要处理屏幕旋转等配置变更场景,可以在ViewModel中使用SavedStateHandle来持久化关键状态(如搜索栏文本):

class CardSearchViewModel(
    private val searchCardUseCase: SearchCardUseCase,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    private val _state = MutableStateFlow(UIState(
        searchBarText = savedStateHandle.get<String>("searchBarText") ?: ""
    ))
    val state = _state.asStateFlow()

    fun onEvent(event: UIEvent) {
        when (event) {
            is UIEvent.OnSearchBarTextChanged ->{
                _state.update {
                    it.copy(
                        searchBarText = event.text
                    )
                }
                savedStateHandle.set("searchBarText", event.text)
                searchJob?.cancel()
                search()
            }
        }
    }

    // 其余代码不变
}

验证效果

完成上述修改后,从详情页返回页面A时,ViewModel会保留之前的状态,搜索栏文本和搜索结果都不会被重置,符合预期行为。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 08:40:53