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