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

MutableSharedFlow重复收集引发Android导航无限循环问题求助

解决SharedFlow重放导致的无限导航循环问题

问题根源

你设置internalUiAction = MutableSharedFlow<UiAction>(1),意味着这个流会缓存最后1个事件。当Fragment从详情页返回时,observe(viewLifecycleOwner)会重新订阅流,SharedFlow会自动重放缓存的NavigateToHeroesDetails事件,导致再次触发导航,陷入循环。

导航属于一次性消费事件,不需要被重放;而GetSuggestedList是初始化加载事件,仅需在Fragment首次创建时触发一次。


方案一:拆分Action流(语义更清晰)

将需要重放的初始化动作和不需要重放的一次性动作拆分为两个独立的流:

修改ViewModel代码

class HeroesViewModel(private val heroesRepository: HeroesRepository) : ViewModel() {
    // 保留原有uiState、uiEvent相关代码
    private val internalUiState = MutableStateFlow<UiState>(UiState.Initial)
    val uiState = internalUiState.asLiveData()

    private val externalUiEvent = MutableSharedFlow<UiEvent>(1)
    private val uiEvent = externalUiEvent.asSharedFlow()

    // 初始化动作流:保留replay=1,用于首次加载触发
    private val internalInitActions = MutableSharedFlow<InitUiAction>(1).apply {
        tryEmit(InitUiAction.GetSuggestedList)
    }
    val initActions = internalInitActions.asLiveData()

    // 一次性动作流:replay=0,不缓存事件,用于导航等一次性操作
    private val internalOneTimeActions = MutableSharedFlow<OneTimeUiAction>(replay = 0, extraBufferCapacity = 1)
    val oneTimeActions = internalOneTimeActions.asLiveData()

    init {
        observeUiEvents()
    }

    private fun observeUiEvents() = viewModelScope.launch {
        uiEvent.collect { event -> 
            when (event) {
                is UiEvent.ListItemClicked -> navigateToHeroDetails(event.heroModel)
                is UiEvent.SearchTextChanged -> getHeroesByName(event.searchText)
            }
        }
    }

    private fun navigateToHeroDetails(heroModel: HeroesListModel) =
        internalOneTimeActions.tryEmit(OneTimeUiAction.NavigateToHeroesDetails(heroModel))

    // 保留原有getHeroesByName、getSuggestedHeroesList等方法
    private fun getHeroesByName(name: String) = viewModelScope.launch(Dispatchers.IO) {
        when (val response = heroesRepository.getHeroesByNameWithSuggestions(name)) {
            is NetworkResponse.Success -> internalUiState.emit(UiState.Data(response.body as List<HeroesListModel>))
            is NetworkResponse.Error -> response.error.message?.let { internalUiState.emit(UiState.Error(it)) }
            else -> {}
        }
    }

    fun getSuggestedHeroesList() = viewModelScope.launch(Dispatchers.IO) {
        when (val response = heroesRepository.getSuggestedHeroesList(true)) {
            is NetworkResponse.Success -> submitState(UiState.Data(response.body as List<HeroesListModel>))
            is NetworkResponse.Error -> response.error.message?.let { submitState(UiState.Error(it)) }
            else -> {}
        }
    }

    private fun submitState(uiState: UiState) = viewModelScope.launch {
        internalUiState.emit(uiState)
    }

    fun submitEvent(uiEvent: UiEvent) = externalUiEvent.tryEmit(uiEvent)

    // 拆分Action类
    sealed class InitUiAction {
        object GetSuggestedList : InitUiAction()
    }

    sealed class OneTimeUiAction {
        data class NavigateToHeroesDetails(val heroModel: HeroesListModel) : OneTimeUiAction()
    }

    // 保留原有UiEvent、UiState类
    sealed class UiEvent {
        data class SearchTextChanged(val searchText: String) : UiEvent()
        data class ListItemClicked(val heroModel: HeroesListModel) : UiEvent()
    }

    sealed class UiState {
        data class Data(val modelsListResponse: List<BaseHeroListModel>) : UiState()
        data class Error(val errorMessage: String) : UiState()
        object Initial : UiState()
    }
}

修改Fragment代码

class DashboardFragment : Fragment() {
    private lateinit var binding: FragmentDashboardBinding
    private val heroesViewModel = get<HeroesViewModel>()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        binding = FragmentDashboardBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        init()
        observeUiState()
        observeInitActions()
        observeOneTimeActions()
    }

    // 观察初始化动作
    private fun observeInitActions() = heroesViewModel.initActions.observe(viewLifecycleOwner) { action ->
        when(action){
            is HeroesViewModel.InitUiAction.GetSuggestedList -> getSuggestedHeroesList()
        }
    }

    // 观察一次性动作(导航)
    private fun observeOneTimeActions() = heroesViewModel.oneTimeActions.observe(viewLifecycleOwner) { action ->
        when(action){
            is HeroesViewModel.OneTimeUiAction.NavigateToHeroesDetails -> navigateToHeroesDetails(action.heroModel)
        }
    }

    // 保留原有init、observeUiState等方法
    private fun init() {
        binding.heroesSearchView.setOnQueryTextListener(object : OnSearchViewOnlyTextChangedListener() {
            override fun onQueryTextChange(newText: String?): Boolean {
                if (newText.isNullOrEmpty()) return false
                heroesViewModel.submitEvent(HeroesViewModel.UiEvent.SearchTextChanged(newText))
                binding.progressBar.setVisiblyAsVisible()
                return false
            }
        })
    }

    private fun observeUiState() = heroesViewModel.uiState.observe(viewLifecycleOwner) { uiState -> 
        when (uiState) {
            is HeroesViewModel.UiState.Data -> showHeroesList(uiState)
            is HeroesViewModel.UiState.Error -> showGeneralError(uiState)
            HeroesViewModel.UiState.Initial -> Unit
        }
    }

    private fun navigateToHeroesDetails(heroModel: HeroesListModel) =
        findNavController().navigate(DashboardFragmentDirections.actionMainFragmentToHeroesDetailsFragment(heroModel))

    private fun showHeroesList(result: HeroesViewModel.UiState.Data) {
        binding.heroesList.setContent {
            LazyColumn {
                items(result.modelsListResponse.toList()) { model -> 
                    if (model is HeroListSeparatorModel)
                        HeroesListSeparatorItem(model)
                    else if (model is HeroesListModel)
                        HeroesListItem(model) {
                            heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ListItemClicked(model))
                        }
                }
            }
        }
        binding.progressBar.setVisiblyAsGone()
    }

    private fun showGeneralError(result: HeroesViewModel.UiState.Error) {
        Toast.makeText(requireContext(), result.errorMessage, Toast.LENGTH_LONG).show()
        binding.progressBar.setVisiblyAsGone()
    }

    private fun getSuggestedHeroesList() {
        heroesViewModel.getSuggestedHeroesList()
        binding.progressBar.setVisiblyAsVisible()
    }
}

方案二:使用Event包装类(通用事件消费保护)

创建一个Event类,确保每个事件仅被消费一次,即使流重放也不会重复处理:

新增Event工具类

open class Event<out T>(private val content: T) {
    var hasBeenHandled = false
        private set

    // 获取未处理的事件内容,标记为已处理
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    // 仅查看内容,不标记为已处理
    fun peekContent(): T = content
}

修改ViewModel代码

class HeroesViewModel(private val heroesRepository: HeroesRepository) : ViewModel() {
    private val internalUiState = MutableStateFlow<UiState>(UiState.Initial)
    val uiState = internalUiState.asLiveData()

    // 用Event包装UiAction,保留replay=1用于初始化动作
    private val internalUiAction = MutableSharedFlow<Event<UiAction>>(1).apply {
        tryEmit(Event(UiAction.GetSuggestedList))
    }
    val uiAction = internalUiAction.asLiveData()

    private val externalUiEvent = MutableSharedFlow<UiEvent>(1)
    private val uiEvent = externalUiEvent.asSharedFlow()

    init {
        observeUiEvents()
    }

    private fun observeUiEvents() = viewModelScope.launch {
        uiEvent.collect { event -> 
            when (event) {
                is UiEvent.ListItemClicked -> navigateToHeroDetails(event.heroModel)
                is UiEvent.SearchTextChanged -> getHeroesByName(event.searchText)
            }
        }
    }

    private fun navigateToHeroDetails(heroModel: HeroesListModel) =
        internalUiAction.tryEmit(Event(UiAction.NavigateToHeroesDetails(heroModel)))

    // 保留原有其他方法
    private fun getHeroesByName(name: String) = viewModelScope.launch(Dispatchers.IO) {
        when (val response = heroesRepository.getHeroesByNameWithSuggestions(name)) {
            is NetworkResponse.Success -> internalUiState.emit(UiState.Data(response.body as List<HeroesListModel>))
            is NetworkResponse.Error -> response.error.message?.let { internalUiState.emit(UiState.Error(it)) }
            else -> {}
        }
    }

    fun getSuggestedHeroesList() = viewModelScope.launch(Dispatchers.IO) {
        when (val response = heroesRepository.getSuggestedHeroesList(true)) {
            is NetworkResponse.Success -> submitState(UiState.Data(response.body as List<HeroesListModel>))
            is NetworkResponse.Error -> response.error.message?.let { submitState(UiState.Error(it)) }
            else -> {}
        }
    }

    private fun submitState(uiState: UiState) = viewModelScope.launch {
        internalUiState.emit(uiState)
    }

    fun submitEvent(uiEvent: UiEvent) = externalUiEvent.tryEmit(uiEvent)

    // 保留原有UiEvent、UiState、UiAction类
    sealed class UiEvent {
        data class SearchTextChanged(val searchText: String) : UiEvent()
        data class ListItemClicked(val heroModel: HeroesListModel) : UiEvent()
    }

    sealed class UiState {
        data class Data(val modelsListResponse: List<BaseHeroListModel>) : UiState()
        data class Error(val errorMessage: String) : UiState()
        object Initial : UiState()
    }

    sealed class UiAction {
        data class NavigateToHeroesDetails(val heroModel: HeroesListModel) : UiAction()
        object GetSuggestedList : UiAction()
    }
}

修改Fragment代码

class DashboardFragment : Fragment() {
    private lateinit var binding: FragmentDashboardBinding
    private val heroesViewModel = get<HeroesViewModel>()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        binding = FragmentDashboardBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        init()
        observeUiState()
        observeUiAction()
    }

    private fun observeUiAction() = heroesViewModel.uiAction.observe(viewLifecycleOwner) { event ->
        // 仅处理未被消费的事件
        event.getContentIfNotHandled()?.let { action ->
            when(action){
                is HeroesViewModel.UiAction.GetSuggestedList -> getSuggestedHeroesList()
                is HeroesViewModel.UiAction.NavigateToHeroesDetails -> navigateToHeroesDetails(action.heroModel)
            }
        }
    }

    // 保留原有其他方法
    private fun init() {
        binding.heroesSearchView.setOnQueryTextListener(object : OnSearchViewOnlyTextChangedListener() {
            override fun onQueryTextChange(newText: String?): Boolean {
                if (newText.isNullOrEmpty()) return false
                heroesViewModel.submitEvent(HeroesViewModel.UiEvent.SearchTextChanged(newText))
                binding.progressBar.setVisiblyAsVisible()
                return false
            }
        })
    }

    private fun observeUiState() = heroesViewModel.uiState.observe(viewLifecycleOwner) { uiState -> 
        when (uiState) {
            is HeroesViewModel.UiState.Data -> showHeroesList(uiState)
            is HeroesViewModel.UiState.Error -> showGeneralError(uiState)
            HeroesViewModel.UiState.Initial -> Unit
        }
    }

    private fun navigateToHeroesDetails(heroModel: HeroesListModel) =
        findNavController().navigate(DashboardFragmentDirections.actionMainFragmentToHeroesDetailsFragment(heroModel))

    private fun showHeroesList(result: HeroesViewModel.UiState.Data) {
        binding.heroesList.setContent {
            LazyColumn {
                items(result.modelsListResponse.toList()) { model -> 
                    if (model is HeroListSeparatorModel)
                        HeroesListSeparatorItem(model)
                    else if (model is HeroesListModel)
                        HeroesListItem(model) {
                            heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ListItemClicked(model))
                        }
                }
            }
        }
        binding.progressBar.setVisiblyAsGone()
    }

    private fun showGeneralError(result: HeroesViewModel.UiState.Error) {
        Toast.makeText(requireContext(), result.errorMessage, Toast.LENGTH_LONG).show()
        binding.progressBar.setVisiblyAsGone()
    }

    private fun getSuggestedHeroesList() {
        heroesViewModel.getSuggestedHeroesList()
        binding.progressBar.setVisiblyAsVisible()
    }
}

方案选择

  • 方案一更符合语义,明确区分初始化状态类动作和一次性事件,推荐用于架构清晰的场景。
  • 方案二是通用解决方案,适合快速修复现有代码,无需大量重构。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 21:54:15