MutableSharedFlow is being re collected again and again - android

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

Related

MainActivity cannot be cast to interface error when implementing interface from child fragment in parent fragment

I have this app in which I have a parent fragment, which has 2 child fragments in a ViewPager2 object.
One of the child fragments has an interface to communicate changes on its menu to the parent fragment.
The child fragment in question is TasksListFragment.kt and the parent fragment is TodayFragment.kt
When I try to initialize the interface in the child fragment onAttach() function, I get
FATAL EXCEPTION: main Process: com.rajchenbergstudios.hoygenda, PID:
java.lang.ClassCastException: com.rajchenbergstudios.hoygenda.ui.activity.MainActivity cannot be cast to com.rajchenbergstudios.hoygenda.ui.todaylists.taskslist.TasksListFragment$ChildFragmentListener
I don't understand why I get this error, and it says MainActivity, when the parent is a fragment which is the one implementing the interface in the first place, not the MainActivity.
I have everything set up correctly:
I have an interface in the child fragment
The interface is used on the child fragment onCreateMenu to pass the menu object to its interface function onFragmentMenuChanged(menu: Menu)
I override the child fragment's onAttach() and initialize the interface:
override fun onAttach(context: Context) {
super.onAttach(context)
childFragmentListener = context as ChildFragmentListener
}
I write a function called setListener() which is called from the parent fragment to pass its context this to the function parameter which assigns it to the childFragment listener
fun setListener(listener: ChildFragmentListener) {
this.childFragmentListener = listener
}
The parent fragment implements the child fragment listener as seen in the TodayFragment.kt file
Can you tell me what am I doing wrong or how to implement an interface to effectively communicate from child fragment back to its parent fragment?
TasksListFragment.kt
#ExperimentalCoroutinesApi
#AndroidEntryPoint
class TasksListFragment : Fragment(R.layout.fragment_child_tasks_list), TasksListAdapter.OnItemClickListener {
private val viewModel: TasksListViewModel by viewModels()
private lateinit var searchView: SearchView
private lateinit var childFragmentListener: ChildFragmentListener
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentChildTasksListBinding.bind(view)
val tasksListAdapter = TasksListAdapter(this)
binding.apply {
tasksListRecyclerview.layoutTasksListRecyclerview.apply {
adapter = tasksListAdapter
layoutManager = LinearLayoutManager(requireContext())
setHasFixedSize(true)
}
ItemTouchHelper(object: ItemTouchHelper.SimpleCallback(0,
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT){
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val task = tasksListAdapter.currentList[viewHolder.adapterPosition]
viewModel.onTaskSwiped(task)
}
}).attachToRecyclerView(tasksListRecyclerview.layoutTasksListRecyclerview)
}
loadObservable(binding, tasksListAdapter)
loadTasksEventCollector()
loadMenu()
}
private fun loadObservable(binding: FragmentChildTasksListBinding, tasksListAdapter: TasksListAdapter) {
viewModel.tasks.observe(viewLifecycleOwner){ tasksList ->
binding.apply {
HGDAViewStateUtils.apply {
if (tasksList.isEmpty()) {
setViewVisibility(tasksListRecyclerview.layoutTasksListRecyclerview, visibility = View.INVISIBLE)
setViewVisibility(tasksListLayoutNoData.layoutNoDataLinearlayout, visibility = View.VISIBLE)
} else {
setViewVisibility(tasksListRecyclerview.layoutTasksListRecyclerview, visibility = View.VISIBLE)
setViewVisibility(tasksListLayoutNoData.layoutNoDataLinearlayout, visibility = View.INVISIBLE)
tasksListAdapter.submitList(tasksList)
}
}
}
}
}
/**
* TasksListViewModel.TaskEvent.ShowUndoDeleteTaskMessage: Stays in this class. It asks for components relevant to this class.
* TasksListViewModel.TaskEvent.NavigateToEditTaskScreen: Stays in this class. The method it overrides comes from task list adapter.
* TasksListViewModel.TaskEvent.NavigateToDeleteAllCompletedScreen: Stays in this class. Relevant to menu which is in this class.
* TasksListViewModel.TaskEvent.NavigateToDeleteAllScreen: Stays in this class. Relevant to menu which is in this class.
*/
private fun loadTasksEventCollector() {
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.tasksEvent.collect { event ->
when (event) {
is TasksListViewModel.TaskEvent.ShowUndoDeleteTaskMessage -> {
Snackbar
.make(requireView(), "Task deleted", Snackbar.LENGTH_LONG)
.setAction("UNDO"){
viewModel.onUndoDeleteClick(event.task)
}
.show()
}
is TasksListViewModel.TaskEvent.NavigateToEditTaskScreen -> {
val action = TodayFragmentDirections
.actionTodayFragmentToTaskAddEditFragment(task = event.task, title = "Edit task", taskinset = null, origin = 1)
findNavController().navigate(action)
}
is TasksListViewModel.TaskEvent.NavigateToAddTaskToSetBottomSheet -> {
val action = TasksListFragmentDirections.actionGlobalSetBottomSheetDialogFragment(task = event.task, origin = 1)
findNavController().navigate(action)
}
is TasksListViewModel.TaskEvent.NavigateToDeleteAllCompletedScreen -> {
val action = TasksListFragmentDirections
.actionGlobalTasksDeleteAllDialogFragment(origin = 1)
findNavController().navigate(action)
}
is TasksListViewModel.TaskEvent.NavigateToDeleteAllScreen -> {
val action = TasksListFragmentDirections
.actionGlobalTasksDeleteAllDialogFragment(origin = 3)
findNavController().navigate(action)
}
}.exhaustive
}
}
}
private fun loadMenu(){
val menuHost: MenuHost = requireActivity()
menuHost.addMenuProvider(object: MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
childFragmentListener.onFragmentChanged(menu)
menuInflater.inflate(R.menu.menu_tasks_list_fragment, menu)
val searchItem = menu.findItem(R.id.tasks_list_menu_search)
searchView = searchItem.actionView as SearchView
val pendingQuery = viewModel.searchQuery.value
if (pendingQuery != null && pendingQuery.isNotEmpty()) {
searchItem.expandActionView()
searchView.setQuery(pendingQuery, false)
}
searchView.OnQueryTextChanged{ searchQuery ->
viewModel.searchQuery.value = searchQuery
}
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
menu.findItem(R.id.tasks_list_menu_hide_completed).isChecked =
viewModel.preferencesFlow.first().hideCompleted
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.tasks_list_menu_sort_by_date -> {
viewModel.onSortOrderSelected(SortOrder.BY_DATE)
true
}
R.id.tasks_list_menu_sort_by_name -> {
viewModel.onSortOrderSelected(SortOrder.BY_NAME)
true
}
R.id.tasks_list_menu_hide_completed -> {
menuItem.isChecked = !menuItem.isChecked
viewModel.onHideCompletedSelected(menuItem.isChecked)
true
}
R.id.tasks_list_menu_delete_completed -> {
viewModel.onDeleteAllCompletedClick()
true
}
R.id.tasks_list_menu_delete_all -> {
viewModel.onDeleteAllClick()
true
}
else -> false
}
}
}, viewLifecycleOwner, Lifecycle.State.RESUMED)
}
interface ChildFragmentListener {
fun onFragmentChanged(menu: Menu)
}
fun setListener(listener: ChildFragmentListener) {
this.childFragmentListener = listener
}
override fun onItemClick(task: Task) {
viewModel.onTaskSelected(task)
}
override fun onItemLongClick(task: Task) {
viewModel.onTaskLongSelected(task)
}
override fun onCheckboxClick(task: Task, isChecked: Boolean) {
viewModel.onTaskCheckedChanged(task, isChecked)
}
override fun onAttach(context: Context) {
super.onAttach(context)
childFragmentListener = context as ChildFragmentListener
}
override fun onPause() {
super.onPause()
Logger.i(TAG, "onPause", "TasksListFragment paused")
}
override fun onDestroyView() {
super.onDestroyView()
searchView.setOnQueryTextListener(null)
}
}
TodayFragment.kt
#ExperimentalCoroutinesApi
#AndroidEntryPoint
class TodayFragment : Fragment(R.layout.fragment_parent_today), TasksListFragment.ChildFragmentListener {
private val viewModel: TodayViewModel by viewModels()
private lateinit var binding: FragmentParentTodayBinding
private var fabClicked: Boolean = false
private lateinit var tasksListMenu: Menu
private lateinit var viewPager: ViewPager2
private val rotateOpen: Animation by lazy { AnimationUtils.loadAnimation(requireContext(), R.anim.rotate_open_anim) }
private val rotateClose: Animation by lazy { AnimationUtils.loadAnimation(requireContext(), R.anim.rotate_close_anim) }
private val fromBottom: Animation by lazy { AnimationUtils.loadAnimation(requireContext(), R.anim.from_bottom_anim) }
private val toBottom: Animation by lazy { AnimationUtils.loadAnimation(requireContext(), R.anim.to_bottom_anim) }
private val fadeIn: Animation by lazy { AnimationUtils.loadAnimation(requireContext(), R.anim.fade_in) }
private val fadeOut: Animation by lazy { AnimationUtils.loadAnimation(requireContext(), R.anim.fade_out) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentParentTodayBinding.bind(view)
binding.apply {
tasksListTransparentWhiteScreen.setOnClickListener {
fabAnimationsRollBack(binding)
fabClicked = !fabClicked
}
}
setChildFragmentMenus()
initViewPagerWithTabLayout(binding)
todayDateDisplay(binding)
initFabs(binding)
loadTodayEventCollector()
getFragmentResultListeners()
}
private fun setChildFragmentMenus(){
val tasksListFragment = TasksListFragment()
tasksListFragment.setListener(this)
Logger.i(TAG, "setChildFragmentMenus", "TasksListFragment menu set")
}
private fun getFragmentResultListeners() {
setFragmentResultListener("add_edit_request"){_, bundle ->
val result = bundle.getInt("add_edit_result")
onFragmentResult(result)
}
setFragmentResultListener("create_set_request_2"){_, bundle ->
val result = bundle.getInt("create_set_result_2")
onFragmentResult(result)
}
setFragmentResultListener("task_added_to_set_request"){_, bundle ->
val result = bundle.getInt("task_added_to_set_result")
val message = bundle.getString("task_added_to_set_message")
onFragmentResult(result, message)
}
setFragmentResultListener("task_added_from_set_request"){_, bundle ->
val result = bundle.getInt("task_added_from_set_result")
val message = bundle.getString("task_added_from_set_message")
onFragmentResult(result, message)
}
}
private fun onFragmentResult(result: Int, message: String? = ""){
viewModel.onFragmentResult(result, message)
}
/**
* TodayViewModel.TodayEvent.NavigateToAddTaskScreen: Relevant to this class. Belongs to Fab which are all in this class.
* TodayViewModel.TodayEvent.ShowTaskSavedConfirmationMessage: Relevant to this class. Belongs to onFragmentResultListener which is here.
* TodayViewModel.TodayEvent.ShowTaskSavedInNewOrOldSetConfirmationMessage: Relevant to this class. Belongs to onFragmentResultListener which is here.
* TodayViewModel.TodayEvent.ShowTaskAddedFromSetConfirmationMessage: Relevant to this class. Belongs to onFragmentResultListener which is here.
* TodayViewModel.TodayEvent.NavigateToAddTasksFromSetBottomSheet: Relevant to this class. Belongs to Fab which are all in this class.
*/
private fun loadTodayEventCollector() {
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.todayEvent.collect { event ->
when (event) {
is TodayViewModel.TodayEvent.NavigateToAddTaskScreen -> {
val action = TodayFragmentDirections
.actionTodayFragmentToTaskAddEditFragment(task = null, title = "Add task"
, taskinset = null, origin = 1)
findNavController().navigate(action)
}
is TodayViewModel.TodayEvent.ShowTaskSavedConfirmationMessage -> {
Snackbar.make(requireView(), event.msg, Snackbar.LENGTH_LONG).show()
setViewPagerPage(0)
}
is TodayViewModel.TodayEvent.ShowTaskSavedInNewOrOldSetConfirmationMessage -> {
Snackbar.make(requireView(), event.msg.toString(), Snackbar.LENGTH_LONG).show()
}
is TodayViewModel.TodayEvent.ShowTaskAddedFromSetConfirmationMessage -> {
Snackbar.make(requireView(), event.msg.toString(), Snackbar.LENGTH_LONG).show()
fabClicked = true
setFabAnimationsAndViewStates(binding)
setViewPagerPage(0)
}
is TodayViewModel.TodayEvent.NavigateToAddTasksFromSetBottomSheet -> {
val action = TasksListFragmentDirections
.actionGlobalSetBottomSheetDialogFragment(task = null, origin = 2)
findNavController().navigate(action)
}
}.exhaustive
}
}
}
// This will soon be used to be 1
private fun setViewPagerPage(index: Int){
viewModel.postActionWithDelay(300, object: TodayViewModel.PostActionListener{
override fun onDelayFinished() {
viewPager.setCurrentItem(index, true)
}
})
}
private fun todayDateDisplay(binding: FragmentParentTodayBinding) {
binding.apply {
tasksListDateheader.apply {
dateHeaderDayofmonth.text = viewModel.getCurrentDayOfMonth()
dateHeaderMonth.text = viewModel.getCurrentMonth()
dateHeaderYear.text = viewModel.getCurrentYear()
dateHeaderDayofweek.text = viewModel.getCurrentDayOfWeek()
}
}
}
private fun initViewPagerWithTabLayout(binding: FragmentParentTodayBinding) {
viewPager = binding.todayViewpager
val tabLayout: TabLayout = binding.todayTablayout
viewPager.adapter = activity?.let { TodayPagerAdapter(it) }
Logger.i(TAG, "initViewPagerWithTabLayout", "viewPager is not null")
TabLayoutMediator(tabLayout, viewPager) { tab, index ->
tab.text = when (index) {
0 -> "Tasks"
1 -> "Journal"
else -> throw Resources.NotFoundException("Tab not found at position")
}.exhaustive
when (index) {
0 -> {
}
1 -> {
fabClicked = false
}
}
}.attach()
}
private fun initFabs(binding: FragmentParentTodayBinding) {
binding.apply {
tasksListFab.setOnClickListener {
onMainFabClick(binding)
}
tasksListSubFab1.setOnClickListener {
Logger.i(TAG, "initFabs", "Coming soon")
}
tasksListSubFab2.setOnClickListener {
viewModel.onAddTasksFromSetClick()
}
tasksListSubFab3.setOnClickListener {
viewModel.onAddNewTaskClick()
}
}
}
private fun onMainFabClick(binding: FragmentParentTodayBinding) {
setFabAnimationsAndViewStates(binding)
}
private fun setFabAnimationsAndViewStates(binding: FragmentParentTodayBinding) {
setFabAnimationVisibilityAndClickability(binding, fabClicked)
fabClicked = !fabClicked
}
private fun setFabAnimationVisibilityAndClickability(binding: FragmentParentTodayBinding, clicked: Boolean) {
if (!clicked) fabAnimationsRollIn(binding) else fabAnimationsRollBack(binding)
}
private fun fabAnimationsRollIn(binding: FragmentParentTodayBinding) {
binding.apply {
HGDAAnimationUtils.apply {
HGDAViewStateUtils.apply {
setViewAnimation(v1 = tasksListFab, a = rotateOpen)
setViewAnimation(v1 = tasksListSubFab1, v2 = tasksListSubFab2, v3 = tasksListSubFab3, a = fromBottom)
setViewAnimation(v1 = tasksListSubFab1Tv, v2 = tasksListSubFab2Tv, v3 = tasksListSubFab3Tv, a = fromBottom)
setViewAnimation(v1 = tasksListTransparentWhiteScreen, a = fadeIn)
setViewVisibility(v1 = tasksListSubFab1, v2 = tasksListSubFab2, v3 = tasksListSubFab3
, v4 = tasksListSubFab1Tv, v5 = tasksListSubFab2Tv, v6 = tasksListSubFab3Tv, visibility = View.VISIBLE)
setViewVisibility(v1 = tasksListTransparentWhiteScreen, visibility = View.VISIBLE)
setViewClickState(v1 = tasksListSubFab1, v2 = tasksListSubFab2, v3 = tasksListSubFab3, clickable = true)
setViewClickState(v1 = tasksListTransparentWhiteScreen, clickable = true)
}
}
}
}
private fun fabAnimationsRollBack(binding: FragmentParentTodayBinding) {
binding.apply {
HGDAAnimationUtils.apply {
HGDAViewStateUtils.apply {
setViewAnimation(v1 = tasksListFab, a = rotateClose)
setViewAnimation(v1 = tasksListSubFab1, v2 = tasksListSubFab2, v3 = tasksListSubFab3, a = toBottom)
setViewAnimation(v1 = tasksListSubFab1Tv, v2 = tasksListSubFab2Tv, v3 = tasksListSubFab3Tv, a = toBottom)
setViewAnimation(v1 = tasksListTransparentWhiteScreen, a = fadeOut)
setViewVisibility(v1 = tasksListSubFab1, v2 = tasksListSubFab2, v3 = tasksListSubFab3
, v4 = tasksListSubFab1Tv, v5 = tasksListSubFab2Tv, v6 = tasksListSubFab3Tv, visibility = View.INVISIBLE)
setViewVisibility(v1 = tasksListTransparentWhiteScreen, visibility = View.INVISIBLE)
setViewClickState(v1 = tasksListSubFab1, v2 = tasksListSubFab2, v3 = tasksListSubFab3, clickable = false)
setViewClickState(v1 = tasksListTransparentWhiteScreen, clickable = false)
}
}
}
}
override fun onFragmentChanged(menu: Menu) {
tasksListMenu = menu
}
override fun onPause() {
super.onPause()
tasksListMenu.clear()
}
}
Fragment is not a Context i.e fragment is not a child of context .
So when you try to cast context as ChildFragmentListener you are actually casting your Activity to ChildFragmentListener which is giving you this RuntimeException . to make it work you can use childFragmentListener = parentFragment as ChildFragmentListener
Also if your Doing this you do not need setListener method anymore.
On other hand i would suggest you do not use listeners to communicate b/w fragments . I see you already using viewModel so just use a shared one to communicate . You can get a shared ViewModel inside child by creating it with parentFragment.

Filtering a recyclerview list using map or switchmap

I've been trying to figure this out for 2 days now - I just can't seem to get it to work!
I'm using MVVM with a Repository pattern.
Could someone tell me what I'm doing wrong here?
I'm trying to filter the list to show characters who appeared in specific seasons
e.g any characters from season 2 would be displayed but characters who weren't in season 2 would be omitted from being displayed.
The season list is also from the api endpoint which is why I'm trying to filter it in the viewmodel.
Is this the right way to do this or is there a better/different way to do it ?
Here's my Fragment class
#AndroidEntryPoint
class CharactersFragment : Fragment(R.layout.fragment_character_list) {
private lateinit var binding: FragmentCharacterListBinding
private val recyclerViewAdapter = MyCharactersRecyclerViewAdapter()
private val viewModel: CharactersViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentCharacterListBinding.inflate(inflater, container, false)
setHasOptionsMenu(true)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
showError()
setupRecyclerView()
navigateToDetails()
}
override fun onOptionsItemSelected(item: MenuItem) =
when (item.itemId) {
R.id.menu_filter -> {
showFilteringPopUpMenu()
true
}
else -> {
false
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.characters_fragment_menu, menu)
val searchItem: MenuItem = menu.findItem(R.id.menu_item_search)
val searchView = searchItem.actionView as SearchView
searchView.apply {
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(queryText: String): Boolean {
Log.d("MainActivity", "QueryTextSubmit: $queryText")
return false
}
override fun onQueryTextChange(queryText: String): Boolean {
Log.d("MainActivity", "QueryTextChange: $queryText")
recyclerViewAdapter.filter.filter(queryText)
return true
}
})
}
}
private fun navigateToDetails() {
recyclerViewAdapter.setOnItemClickListener {
val action =
CharactersFragmentDirections.actionCharactersFragmentToCharacterDetailsFragment(it)
findNavController().navigate(action)
}
}
private fun setupRecyclerView() {
binding.list.apply {
layoutManager =
StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
adapter = recyclerViewAdapter
viewModel.characters.observe(viewLifecycleOwner) {
it?.let {
recyclerViewAdapter.updateList(it)
Log.d("TAG", "onViewCreated: ${it.size}")
}
}
}
}
private fun showError() {
viewModel.spinner.observe(viewLifecycleOwner) { value ->
value.let { show ->
binding.spinner.visibility = if (show) View.VISIBLE else View.GONE
}
}
viewModel.errorText.observe(viewLifecycleOwner) { text ->
text?.let {
binding.errorTextView.apply {
this.text = text
visibility = View.VISIBLE
}
binding.list.visibility = View.GONE
viewModel.onErrorTextShown()
}
}
}
private fun showFilteringPopUpMenu() {
val view = activity?.findViewById<View>(R.id.menu_filter) ?: return
PopupMenu(requireContext(), view).run {
menuInflater.inflate(R.menu.filter_seasons, menu)
setOnMenuItemClickListener {
when (it.itemId) {
R.id.one -> {
viewModel.season.value = FilterSeasons.SEASON_ONE
Toast.makeText(requireContext(), "Season One", Toast.LENGTH_SHORT)
.show()
}
R.id.two -> {
viewModel.season.value = FilterSeasons.SEASON_TWO
Toast.makeText(requireContext(), "Season Two", Toast.LENGTH_SHORT)
.show()
}
R.id.three -> {
viewModel.season.value = FilterSeasons.SEASON_THREE
Toast.makeText(requireContext(), "Season Three", Toast.LENGTH_SHORT)
.show()
}
R.id.four -> {
viewModel.season.value = FilterSeasons.SEASON_FOUR
Toast.makeText(requireContext(), "Season Four", Toast.LENGTH_SHORT)
.show()
}
R.id.five -> {
viewModel.season.value = FilterSeasons.SEASON_FIVE
Toast.makeText(requireContext(), "Season Five", Toast.LENGTH_SHORT)
.show()
}
else -> {
viewModel.season.value = FilterSeasons.ALL_SEASONS
Toast.makeText(requireContext(), "All Seasons", Toast.LENGTH_SHORT)
.show()
}
}
true
}
show()
}
}
}
And here's my ViewModel
#HiltViewModel
class CharactersViewModel #Inject constructor(
private val repository: Repository
) : ViewModel() {
private val _spinner = MutableLiveData<Boolean>(false)
val spinner: LiveData<Boolean> = _spinner
val season = MutableLiveData<FilterSeasons>()
private val _errorText = MutableLiveData<String?>()
val errorText: LiveData<String?> = _errorText
private val _characters = MutableLiveData<List<BreakingBadCharacterItem>?>()
val characters: LiveData<List<BreakingBadCharacterItem>?> =
_characters.map { seasons ->
when (season.value) {
ALL_SEASONS -> {
seasons
}
SEASON_ONE -> seasons?.filter {
it.appearance.any { it == 1 }
}
SEASON_TWO -> seasons?.filter {
it.appearance.any { it == 2 }
}
SEASON_THREE -> seasons?.filter {
it.appearance.any { it == 3 }
}
SEASON_FOUR -> seasons?.filter {
it.appearance.any { it == 4 }
}
SEASON_FIVE -> seasons?.filter {
it.appearance.any { it == 5 }
}
else -> {
seasons
}
}
}
init {
getAllCharacters()
season.value = ALL_SEASONS
}
private fun getAllCharacters() =
viewModelScope.launch {
try {
_spinner.postValue(true)
val response = repository.loadBBCharacters()
_characters.postValue(response)
} catch (error: BreakingError) {
_errorText.postValue(error.message)
} finally {
_spinner.postValue(false)
}
}
fun onErrorTextShown() {
_errorText.value = null
}
}
I also have an Enum class
enum class FilterSeasons {
ALL_SEASONS,
SEASON_ONE,
SEASON_TWO,
SEASON_THREE,
SEASON_FOUR,
SEASON_FIVE
}
In your menuItemListener you are modifying the viewmodel.season bit you're not actually listening/observing to any changes to this value.
I would recommend a custom setter here (not sure it needs to be liveData as it's not being observed) :
val season: FilterSeasons = FilterSeasons.All
set(value) {
field = value
filterCharacterBySeason(value)
}
And then abstract the filtering you are doing in the character LiveData into itls own method filterCharacterBySeason(season: FilterSeason)

Handle search results after configuration change MVVM

I am building a movie app based on "The Movie DB". My app has only one activity with a recyclerview and one search action button (SearchView).
As soon as the app starts,the recyclerview filled with popularMovies list.
When I search for a movie I get the results and everything is fine,but when I rotate the screen I get the popularMovies list and not my search results.
Now I know the reason,its happening because in onCreate I have initMoviesRecyclerView() that fill the recyclerview with popularMovies.
My question is how can I keep the search results without filling the recyclerview with popularMovies ? How can I do this the correct way ?
This is the Repository Class:
class MainRepository {
//MutableLiveData
private val popularMoviesMutableLiveData = MutableLiveData<List<Movie>>()
private val searchAfterMutableLiveData = MutableLiveData<List<Movie>>()
// API
private val apiService : GetFromApi = APIService.retrofitClientRequest
private val apiKey = "NOT"
fun getPopularMoviesList() : MutableLiveData<List<Movie>>{
apiService.getPopularMovies(apiKey,1)?.enqueue(object : Callback<MovieListResult?> {
override fun onResponse(
call: Call<MovieListResult?>,
response: Response<MovieListResult?>
) {
if (response.isSuccessful){
popularMoviesMutableLiveData.value = response.body()?.moviesResults
Log.e("MovieListResults","Result: ${popularMoviesMutableLiveData.value}")
}
}
override fun onFailure(call: Call<MovieListResult?>, t: Throwable) {
Log.e("MovieListResults","Failed: ${t.message}")
}
})
return popularMoviesMutableLiveData
}
fun searchAfter(searchAfter : String) : MutableLiveData<List<Movie>>{
apiService.searchAfter(apiKey,searchAfter)?.enqueue(object : Callback<MovieListResult?> {
override fun onResponse(
call: Call<MovieListResult?>,
response: Response<MovieListResult?>
) {
if (response.isSuccessful){
searchAfterMutableLiveData.value = response.body()?.moviesResults
Log.e("SearchMovieListResults","Result: ${searchAfterMutableLiveData.value}")
}
}
override fun onFailure(call: Call<MovieListResult?>, t: Throwable) {
Log.e("SearchMovieListResults","Failed: ${t.message}")
}
})
return searchAfterMutableLiveData
}
}
This is the viewModel class:
class MainViewModel : ViewModel(){
//Repository
private val mainRepository = MainRepository()
//MutableLiveData
var popularMoviesMutableLiveData = MutableLiveData<List<Movie>>()
var searchAfterMutableLiveData = MutableLiveData<List<Movie>>()
//The Main Movie List
var mainMovieList = listOf<Movie>()
fun getPopularMovies() : LiveData<List<Movie>>{
popularMoviesMutableLiveData = mainRepository.getPopularMoviesList()
popularMoviesMutableLiveData.value = mainMovieList
return popularMoviesMutableLiveData
}
fun getMovieBySearch(searchAfter : String) : LiveData<List<Movie>>{
searchAfterMutableLiveData = mainRepository.searchAfter(searchAfter)
searchAfterMutableLiveData.value = mainMovieList
return searchAfterMutableLiveData
}
}
This is the MainActivity class:
class MainActivity : AppCompatActivity() {
//ViewModel
private val mainViewModel : MainViewModel by viewModels()
// Views
private lateinit var mainRecyclerView : RecyclerView
private lateinit var mainAdapter : MainRecyclerViewAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initMoviesRecyclerView()
}
private fun initMoviesRecyclerView() {
mainRecyclerView = findViewById(R.id.mainRecyclerView)
mainRecyclerView.setHasFixedSize(true)
mainRecyclerView.layoutManager = GridLayoutManager(this,1)
mainViewModel.getPopularMovies().observe(this, object : Observer<List<Movie>?> {
override fun onChanged(newList: List<Movie>?) {
if (newList != null) {
mainViewModel.mainMovieList = newList
mainAdapter = MainRecyclerViewAdapter(mainViewModel.mainMovieList)
mainRecyclerView.adapter = mainAdapter
}
}
})
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_menu,menu)
val searchView = menu.findItem(R.id.menu_search_movie).actionView as androidx.appcompat.widget.SearchView
searchView.queryHint = "Search By Name,Actor .."
searchView.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(whileTextChange: String?): Boolean {
//Clear SearchView
searchView.isIconified = true
searchView.setQuery("", false)
searchView.onActionViewCollapsed()
mainViewModel.getMovieBySearch(whileTextChange.toString()).observe(this#MainActivity,object : Observer<List<Movie>?> {
override fun onChanged(newList: List<Movie>?) {
if (newList != null) {
mainViewModel.mainMovieList = newList
mainAdapter.changeCurrentList(mainViewModel.mainMovieList)
}
}
})
return false
}
override fun onQueryTextChange(whileTextChange: String?): Boolean {
Log.e("onQueryTextChange","Text: $whileTextChange")
return false
}
})
return true
}
}
Thank you!
I don't see where you actually populate the data inside the ViewModel. But it all looked good.
This line is a bit suspicious:
mainViewModel.mainMovieList = newList
You should not update the ViewModel from the View lifeCycle. Also, you should not update the ViewModel during the observe call.
So after sometime I manage to work around it with a simple solution:
I created a function in the viewModel,to check if the search list value is null:
fun startFromSearch(): Boolean {
return searchAfterMutableLiveData.value != null
}
After that I made an if statement in the initMoviesRecyclerView(), and that's it!
private fun initMoviesRecyclerView() {
mainRecyclerView = findViewById(R.id.mainRecyclerView)
mainRecyclerView.setHasFixedSize(true)
mainRecyclerView.layoutManager = GridLayoutManager(this,1)
mainAdapter = MainRecyclerViewAdapter(mainViewModel.mainMovieList,this)
mainRecyclerView.adapter = mainAdapter
if (!mainViewModel.startFromSearch()){
mainViewModel.getPopularMovies(1).observe(this, object : Observer<List<Movie>?> {
override fun onChanged(newList: List<Movie>?) {
if (newList != null) {
mainAdapter = MainRecyclerViewAdapter(newList,this#MainActivity)
mainRecyclerView.adapter = mainAdapter
}
}
})
}
}

delay init methods in ViewModel

I have following project in Github : https://github.com/AliRezaeiii/TMDb-Paging
I have to postDelay calling methods in my ViewModel since datasource is not initialized :
abstract class DetailViewModel(private val item: TmdbItem) : BaseViewModel() {
private val handler = Handler(Looper.getMainLooper())
val trailers: ObservableList<Video> = ObservableArrayList()
val isTrailersVisible = ObservableBoolean(false)
private val _cast = MutableLiveData<List<Cast>>()
val cast: LiveData<List<Cast>> = _cast
val isCastVisible = ObservableBoolean(false)
init {
handler.postDelayed({
showTrailers()
showCast()
}, 100)
}
protected abstract fun getTrailers(id: Int): Observable<List<Video>>
protected abstract fun getCast(id: Int): Observable<List<Cast>>
private fun showTrailers() {
EspressoIdlingResource.increment() // App is busy until further notice
compositeDisposable.add(getTrailers(item.id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally {
if (!EspressoIdlingResource.getIdlingResource().isIdleNow) {
EspressoIdlingResource.decrement() // Set app as idle.
}
}
.subscribe({ videos ->
if (videos.isNotEmpty()) {
isTrailersVisible.set(true)
}
with(trailers) {
clear()
addAll(videos)
}
}
) { throwable -> Timber.e(throwable) })
}
private fun showCast() {
EspressoIdlingResource.increment() // App is busy until further notice
compositeDisposable.add(getCast(item.id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally {
if (!EspressoIdlingResource.getIdlingResource().isIdleNow) {
EspressoIdlingResource.decrement() // Set app as idle.
}
}
.subscribe({ cast ->
if (cast.isNotEmpty()) {
isCastVisible.set(true)
}
this._cast.postValue(cast)
}
) { throwable -> Timber.e(throwable) })
}
}
And here is my Fragment :
abstract class DetailFragment<T : TmdbItem>
: BaseDaggerFragment(), CastClickCallback {
protected abstract fun getViewModel(): DetailViewModel
protected abstract fun getLayoutId(): Int
protected abstract fun initViewBinding(root: View): ViewDataBinding
protected abstract fun getTmdbItem(): T
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val viewModel = getViewModel()
val root = inflater.inflate(getLayoutId(), container, false)
initViewBinding(root).apply {
setVariable(BR.vm, viewModel)
lifecycleOwner = viewLifecycleOwner
}
with(root) {
with(activity as AppCompatActivity) {
setupActionBar(details_toolbar) {
setDisplayShowTitleEnabled(false)
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
}
summary_label.visibleGone(getTmdbItem().overview.trim().isNotEmpty())
// Make the MotionLayout draw behind the status bar
details_motion.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
summary.setOnClickListener {
val maxLine = resources.getInteger(R.integer.max_lines)
summary.maxLines = if (summary.maxLines > maxLine) maxLine else Int.MAX_VALUE
}
viewModel.cast.observe(viewLifecycleOwner, Observer {
it?.apply {
val adapter = CastAdapter(it, this#DetailFragment)
cast_list.apply {
setHasFixedSize(true)
cast_list.adapter = adapter
}
}
})
with(details_rv) {
postDelayed({ scrollTo(0, 0) }, 100)
}
}
return root
}
}
And BaseDaggerFragment :
open class BaseDaggerFragment : DaggerFragment() {
#Inject
lateinit var dataSource: RemoteDataSource
}
Could be any better solution than :
init {
handler.postDelayed({
showTrailers()
showCast()
}, 100)
}
You can lazy initialize like this way
private val users:MutableLiveData<List<Cast>> by lazy {
MutableLiveData().also {
showTrailers()
showCast()
}
}
more details refer ViewModel

Initialization of a fragment causes the animation lag

While writing an Android app, I encountered a problem with a stuttering animation. I use AHBottomNavigation for navigation, FragNav is for swapping fragments and FlexibleAdapter for RecyclerView.
The application is built from one activity and five fragments. When I try to switch to the first fragment in the application, the BottomNavigation animation freez for a moment. It looks very unsightly. The second time I choose the same fragment, everything works smoothly. It seems to me that it is the fault to initialize the views in the fragment, but I have no idea how to do it differently.
AHBottomNavigation https://github.com/aurelhubert/ahbottomnavigation
FragNav https://github.com/ncapdevi/FragNav
FlexibleAdapter https://github.com/davideas/FlexibleAdapter
Fragment
class GradeFragment : BaseFragment(), GradeView {
#Inject
lateinit var presenter: GradePresenter
private val gradeAdapter = FlexibleAdapter<AbstractFlexibleItem<*>>(null, null, true)
companion object {
fun newInstance() = GradeFragment()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_grade, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.run {
attachView(this#GradeFragment)
loadData()
}
}
override fun initView() {
gradeAdapter.run {
isAutoCollapseOnExpand = true
isAutoScrollOnExpand = true
setOnUpdateListener { presenter.onUpdateDataList(it) }
setOnItemClickListener { position ->
getItem(position).let {
if (it is GradeItem) {
GradeDialog.newInstance(it.grade).show(fragmentManager, it.toString())
}
}
}
}
gradeRecycler.run {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = gradeAdapter
}
gradeSwipe.setOnRefreshListener { presenter.loadData(forceRefresh = true) }
}
override fun updateData(data: List<GradeHeader>) {
gradeAdapter.updateDataSet(data, true)
}
override fun isViewEmpty(): Boolean = gradeAdapter.isEmpty
override fun showEmptyView(show: Boolean) {
gradeEmpty.visibility = if (show) VISIBLE else GONE
}
override fun showProgress(show: Boolean) {
gradeProgress.visibility = if (show) VISIBLE else GONE
}
override fun setRefresh(show: Boolean) {
gradeSwipe.isRefreshing = show
}
Presenter
class GradePresenter #Inject constructor(
private val errorHandler: ErrorHandler,
private val schedulers: SchedulersManager,
private val gradeRepository: GradeRepository,
private val sessionRepository: SessionRepository) : BasePresenter<GradeView>(errorHandler) {
override fun attachView(view: GradeView) {
super.attachView(view)
view.initView()
}
fun loadData(forceRefresh: Boolean = false) {
disposable.add(sessionRepository.getSemesters()
.map { it.single { semester -> semester.current } }
.flatMap { gradeRepository.getGrades(it, forceRefresh) }
.map { it.groupBy { grade -> grade.subject } }
.map { createGradeItems(it) }
.subscribeOn(schedulers.backgroundThread())
.observeOn(schedulers.mainThread())
.doFinally { view?.setRefresh(false) }
.doOnSuccess { if (it.isEmpty()) view?.showEmptyView(true) }
.doOnError { view?.run { if (isViewEmpty()) showEmptyView(true) } }
.subscribe({ view?.updateData(it) }) { errorHandler.proceed(it) })
}
private fun createGradeItems(items: Map<String, List<Grade>>): List<GradeHeader> {
return items.map {
val gradesAverage = calcAverage(it.value)
GradeHeader().apply {
subject = it.key
average = view?.run {
if (gradesAverage == 0f) emptyAverageString()
else averageString().format(gradesAverage)
}.orEmpty()
number = view?.gradeNumberString(it.value.size).orEmpty()
subItems = (it.value.map { item ->
GradeItem().apply {
grade = item
weightString = view?.weightString().orEmpty()
valueColor = getValueColor(item.value)
}
})
}
}
}
fun onUpdateDataList(size: Int) {
if (size != 0) view?.showProgress(false)
}
After a few days, I managed to solve the problem by updating the SDK to version 28. RecyclerView no longer causes animation jams when inflating

Categories

Resources