0

I have the following ViewModel and Fragment -

class HeroesViewModel(private val heroesRepository: HeroesRepository) : ViewModel() {

    private val internalUiState = MutableStateFlow<UiState>(UiState.Initial)
    val uiState = internalUiState.asLiveData()

    private val internalUiAction = MutableSharedFlow<UiAction>(1).apply {
        tryEmit(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) =
        submitAction(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 { message ->
                    internalUiState.emit(UiState.Error(message))
                }
            }
            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 { message ->
                    submitState(UiState.Error(message))
                }
            }
            else -> {}
        }
    }

    private fun submitAction(uiAction: UiAction) = internalUiAction.tryEmit(uiAction)


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

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

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

class DashboardFragment : Fragment() {

    //Class Variables - UI
    private lateinit var binding: FragmentDashboardBinding

    //Class Variables - Dependency Injection
    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) { 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) { uiAction ->
        when (uiAction) {
            is HeroesViewModel.UiState.Data -> {
                showHeroesList(uiAction)
            }
            is HeroesViewModel.UiState.Error -> {
                showGeneralError(uiAction)
            }
            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()
    }

}

As you can see, I have the replayCache set to 1 in internalUiAction but the value keeps emitting itself. When I navigate using the navigateToHeroesDetails() method and go back using the navigation bar I immediately observe the last uiAction emitted value which is NavigateToHeroesDetails, causing me to navigate again and again to the heroes details screen. This is an endless loop of navigation.

As far as a hint for a solution, if I double tap the navigation 2 times quickly it does indeed go back to the first Fragment. Seems like I am missing something related to SharedFlow

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Alon Shlider
  • 1,187
  • 1
  • 16
  • 46

0 Answers0