Fragment visibility issue: PlaylistsFragment shows blank screen after navigating back from child fragments

2 weeks ago 16
ARTICLE AD BOX

I have an audio player app, it's contain a PlaylistsFragment that contains 4 cards which navigate to other fragments (FavouritesFragment, RecentlyPlayedFragment, MostPlayedFragment, RecentlyAddedFragment). When I navigate to any of these fragments and press the back button, the PlaylistsFragment appears as a blank / empty screen. However, if I switch to another tab and come back, the fragment displays correctly

PlaylistsFragment

FavouritesFragment and foucas on back button

PlaylistsFragment blank

This my Current Implementation :

@AndroidEntryPoint class PlaylistsFragment : Fragment(R.layout.fragment_playlists), MenuProvider, PlaylistsAdapter.PlaylistAdapterListener { companion object { private const val TAG = "PlaylistsFragment" const val NEW_PLAYLIST_ID_MARKER = -100L } private var _binding: FragmentPlaylistsBinding? = null private val binding get() = _binding!! private val playlistsViewModel by viewModels<PlaylistsViewModel>() private lateinit var playlistsAdapter: PlaylistsAdapter private val selectedPositions: MutableSet<Int> = mutableSetOf() private var actionMode: ActionMode? = null private var originalPlaylists = arrayListOf<Playlist>() private var mMainActivityUiController: MainActivityUiController? = null private var menu: Menu? = null private var sortType: PlaylistSortType = PlaylistSortType.NAME_ASC private lateinit var requestPermissionLauncher: ActivityResultLauncher<String> private var storagePermissionGranted = false private var menuHost: MenuHost? = null private val recentlyPlayedViewModel by viewModels<RecentlyPlayedViewModel>() private val mostPlayedViewModel by viewModels<MostPlayedViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setupPermissionLauncher() } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentPlaylistsBinding.inflate(inflater, container, false) playlistsAdapter = PlaylistsAdapter(this) menuHost = requireActivity() menuHost?.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) Log.d(TAG, "onViewCreated") binding.playlistsFragment.visibility = View.VISIBLE setupRecyclerView() observePlaylistList() checkPermissionsAndLoadDataIfNeeded() setupMainActivityUiController() observeRecentlyPlayedCount() observeMostPlayedCount() observeFavouriteClipsCount() binding.addNewPlaylistButton.setOnClickListener { CreatePlaylistDialog.show(requireContext()) { playlistName -> Log.d( TAG, "Playlist name entered: $playlistName. Navigating to AddClipsToPlaylistFragment for new playlist." ) val fragment = AddClipsToPlaylistFragment.newInstance(NEW_PLAYLIST_ID_MARKER, playlistName) parentFragmentManager.beginTransaction() .replace(R.id.main, fragment) .addToBackStack(null) .commit() } } binding.cardRecentlyPlayed.setOnClickListener { val recentlyPlayedFragment = RecentlyPlayedFragment() val fragmentManager = requireActivity().supportFragmentManager val transaction = fragmentManager.beginTransaction() transaction.apply { addToBackStack(null) setReorderingAllowed(true) hide(this@PlaylistsFragment) add(R.id.main, recentlyPlayedFragment) commit() } binding.playlistsFragment.visibility = GONE mMainActivityUiController?.hideTabLayout() } binding.cardMostPlayed.setOnClickListener { val mostPlayedFragment = MostPlayedFragment() val fragmentManager = requireActivity().supportFragmentManager val transaction = fragmentManager.beginTransaction() transaction.apply { addToBackStack(null) setReorderingAllowed(true) hide(this@PlaylistsFragment) add(R.id.main, mostPlayedFragment) commit() } binding.playlistsFragment.visibility = GONE mMainActivityUiController?.hideTabLayout() } binding.cardFavorites.setOnClickListener { val favouritesClipsFragment = FavouritesClipsFragment() val fragmentManager = requireActivity().supportFragmentManager val transaction = fragmentManager.beginTransaction() transaction.apply { addToBackStack(null) setReorderingAllowed(true) hide(this@PlaylistsFragment) add(R.id.main, favouritesClipsFragment) commit() } binding.playlistsFragment.visibility = GONE mMainActivityUiController?.hideTabLayout() } } override fun onResume() { super.onResume() Log.d(TAG, "onResume: Fragment resumed.") (activity as? MainActivity)?.getToolbar()?.visibility = View.VISIBLE mMainActivityUiController?.showTabLayout() binding.playlistsFragment.visibility = View.VISIBLE } override fun onDestroyView() { super.onDestroyView() Log.d(TAG, "onDestroyView: Cleaning up.") menuHost?.removeMenuProvider(this) binding.recyclerPlaylists.adapter = null actionMode?.finish() actionMode = null _binding = null } private fun setupMainActivityUiController() { if (activity is MainActivityUiController) { mMainActivityUiController = activity as MainActivityUiController } } private fun observePlaylistList() { viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { Log.d(TAG, "Observer: STARTED collecting playlistList StateFlow.") playlistsViewModel.playlists.collect { resource -> if (!isAdded || _binding == null) { Log.w(TAG, "Observer: Fragment view null or not added. Skipping UI update.") return@collect } Log.d( TAG, "Observer: Collected playlistList resource: ${resource::class.java.simpleName}" ) binding.progressBar.visibility = if (resource is Resource.Loading) View.VISIBLE else GONE val showList = resource is Resource.Success && resource.data.isNotEmpty() val showEmpty = (resource is Resource.Success && resource.data.isEmpty()) || resource is Resource.Empty binding.recyclerPlaylists.visibility = if (showList) View.VISIBLE else View.INVISIBLE binding.emptyView.visibility = if (showEmpty) View.VISIBLE else GONE when (resource) { is Resource.Success -> { val playlists = resource.data originalPlaylists.clear() originalPlaylists.addAll(playlists) playlistsAdapter.submitList(ArrayList(playlists)) { val itemCount = playlistsAdapter.itemCount binding.recyclerPlaylists.visibility = if (itemCount > 0) View.VISIBLE else View.INVISIBLE binding.emptyView.visibility = if (itemCount == 0) View.VISIBLE else GONE binding.emptyView.text = getString(R.string.no_playlists_found) } Log.d( TAG, "Successfully submitted ${playlists.size} playlists to adapter." ) } is Resource.Error -> { binding.emptyView.text = resource.message Log.e(TAG, "Error loading playlists: ${resource.message}") } is Resource.Empty -> { playlistsAdapter.submitList(emptyList()) binding.emptyView.text = getString(R.string.no_playlists_found) } else -> {} } } playlistsViewModel.counts.collect { resource -> if (resource is Resource.Success) { binding.countRecentlyAdded.text = "${resource.data.recentlyAddedCount} الأغاني" binding.countFavorites.text = "${resource.data.favoriteCount} الأغاني" binding.countMostPlayed.text = "${resource.data.mostPlayedCount} الأغاني" } else if (resource is Resource.Empty) { binding.countRecentlyAdded.text = "0 الأغاني" binding.countFavorites.text = "0 الأغاني" binding.countMostPlayed.text = "0 الأغاني" binding.countRecentlyPlayed.text = "0 الأغاني" } } } } } private fun customizeMenuItem(menu: Menu, itemId: Int, title: String) { val menuItem = menu.findItem(itemId) val spannableString = SpannableString(" $title") val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.baseline_arrow_left_24) drawable?.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) val imageSpan = android.text.style.ImageSpan(drawable!!, android.text.style.ImageSpan.ALIGN_BOTTOM) spannableString.setSpan( imageSpan, 0, 1, android.text.SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE ) menuItem.title = spannableString } override fun onPlaylistClicked(playlist: Playlist) { Log.d(TAG, "Playlist clicked: ${playlist.name}") = val playlistDetailsFragment = PlaylistDetailsFragment().apply { arguments = Bundle().apply { putParcelable("playlist", playlist) } } val fragmentManager = requireActivity().supportFragmentManager val transaction = fragmentManager.beginTransaction() transaction.apply { addToBackStack(null) setReorderingAllowed(true) hide(this@PlaylistsFragment) add(R.id.main, playlistDetailsFragment, "playlistFragment") commit() } binding.playlistsFragment.visibility = GONE mMainActivityUiController?.hideTabLayout() } override fun onPlaylistMoreOptionsClicked(playlist: Playlist, anchorView: View) { showBottomSheet(playlist, anchorView) } override fun onItemSelectionChanged(position: Int, isSelected: Boolean) { Log.d(TAG, "Playlist selection changed: Pos $position, isSelected=$isSelected") if (isSelected) selectedPositions.add(position) else selectedPositions.remove(position) updateActionModeTitle() } override fun onStartSelectionMode(position: Int) { TODO("Not yet implemented") } @SuppressLint("MissingInflatedId") private fun showBottomSheet(playlist: Playlist, anchorView: View) { val bottomSheetDialog = android.app.AlertDialog.Builder(requireContext()) .setTitle(playlist.name) .setItems(arrayOf("Play", "Play Next", "Add to Queue", "Delete")) { _, which -> when (which) { 0 -> Toast.makeText( requireContext(), "Play ${playlist.name}", Toast.LENGTH_SHORT ).show() 1 -> Toast.makeText( requireContext(), "Play Next ${playlist.name}", Toast.LENGTH_SHORT ).show() 2 -> Toast.makeText( requireContext(), "Add to Queue ${playlist.name}", Toast.LENGTH_SHORT ).show() 3 -> showDeletePlaylistConfirmationDialog(playlist) } } .create() bottomSheetDialog.show() } private fun showDeletePlaylistConfirmationDialog(playlist: Playlist) { MaterialAlertDialogBuilder(requireContext()) .setTitle(getString(R.string.delete_playlist_title)) .setMessage(getString(R.string.delete_playlist_confirmation_message, playlist.name)) .setPositiveButton(R.string.delete) { _, _ -> playlistsViewModel.deletePlaylist(playlist.id) Toast.makeText(requireContext(), "Deleted ${playlist.name}", Toast.LENGTH_SHORT) .show() } .setNegativeButton(R.string.cancel, null) .show() } class CreatePlaylistDialog { companion object { @JvmStatic fun show(context: Context, onPlaylistCreated: (String) -> Unit) { val dialog = Dialog(context) dialog.requestWindowFeature(Window.FEATURE_NO_TITLE) dialog.setContentView(R.layout.dialog_create_playlist) // Set dialog properties dialog.window?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable()) dialog.window?.setLayout( (context.resources.displayMetrics.widthPixels * 0.9).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT ) val editTextPlaylistName = dialog.findViewById<EditText>(R.id.editTextPlaylistName) val btnCreatePlaylist = dialog.findViewById<Button>(R.id.btnCreatePlaylist) val btnCancel = dialog.findViewById<Button>(R.id.btnCancel) // Set focus and show keyboard editTextPlaylistName.requestFocus() val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0) btnCreatePlaylist.setOnClickListener { val playlistName = editTextPlaylistName.text.toString().trim() if (playlistName.isNotEmpty()) { onPlaylistCreated(playlistName) dialog.dismiss() } else { editTextPlaylistName.error = "يرجى إدخال اسم قائمة التشغيل" } } btnCancel.setOnClickListener { dialog.dismiss() } dialog.setOnDismissListener { imm.hideSoftInputFromWindow(editTextPlaylistName.windowToken, 0) } dialog.show() } } } }

What I've Tried So far:

Using replace() instead of hide()/add(): This caused fragments to overlap with each other, showing content from multiple fragments simultaneously

Adding onHiddenChanged() callback:

override fun onHiddenChanged(hidden: Boolean) { super.onHiddenChanged(hidden) if (!hidden) { _binding?.let { it.playlistsFragment.visibility = View.VISIBLE it.root.bringToFront() } (activity as? MainActivity)?.getToolbar()?.visibility = View.VISIBLE mMainActivityUiController?.showTabLayout() } } Manually removing old fragments before navigation: private fun navigateToFragment(fragment: Fragment) { val fragmentManager = requireActivity().supportFragmentManager fragmentManager.fragments.forEach { existingFragment -> if (existingFragment != this && existingFragment !is PlaylistsFragment && existingFragment.isAdded) { fragmentManager.beginTransaction() .remove(existingFragment) .commitNow() } } fragmentManager.beginTransaction().apply { setReorderingAllowed(true) hide(this@PlaylistsFragment) add(R.id.main, fragment) addToBackStack(null) commit() } mMainActivityUiController?.hideTabLayout() } Using bringToFront() in onResume and onHiddenChanged: The view appears to be present in the hierarchy but not visible on screen

The Layout of Playlists Fragment

<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/playlistsFragment" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#1A1A1A" tools:context=".ui.playlists_ui.PlaylistsFragment"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="16dp"> <!-- ProgressBar --> <ProgressBar android:id="@+id/progressBar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:visibility="gone" /> <!-- Empty View for no data or error state --> <TextView android:id="@+id/emptyView" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:text="@string/no_playlists_found" android:textColor="#FFFFFF" android:textSize="16sp" android:visibility="gone" /> <!-- Playlists count --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="24dp" android:gravity="center_vertical" android:orientation="horizontal" android:layoutDirection="rtl"> <TextView android:id="@+id/playlistsCountText" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="6" android:textColor="#FFFFFF" android:textSize="18sp" android:textStyle="bold" android:textAlignment="textStart" tools:text="6 Playlists" /> <ImageView android:id="@+id/addNewPlaylistButton" android:layout_width="@dimen/_24sdp" android:layout_height="@dimen/_24sdp" android:layout_weight="1" android:background="?selectableItemBackground" android:clickable="true" android:focusable="true" android:src="@drawable/ic_add_48" /> </LinearLayout> <!-- Playlist cards grid --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <!-- Row 1 --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="16dp" android:orientation="horizontal" android:layoutDirection="ltr"> <!-- Recently Added --> <androidx.cardview.widget.CardView android:id="@+id/card_recently_added" android:layout_width="0dp" android:layout_height="120dp" android:layout_marginStart="8dp" android:layout_marginEnd="16dp" android:layout_weight="1" android:backgroundTint="#2E7D8F" app:cardCornerRadius="12dp" app:cardElevation="0dp"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" android:layoutDirection="rtl"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="@string/recently_added" android:textColor="#FFFFFF" android:textSize="16sp" android:textStyle="bold" /> <TextView android:id="@+id/countRecentlyAdded" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentStart="true" android:layout_alignParentBottom="true" android:textColor="#B3FFFFFF" android:textSize="12sp" tools:text="94 Songs" /> </RelativeLayout> </androidx.cardview.widget.CardView> <!-- Favorites --> <androidx.cardview.widget.CardView android:id="@+id/card_favorites" android:layout_width="0dp" android:layout_height="120dp" android:layout_marginEnd="8dp" android:layout_weight="1" android:backgroundTint="#B83D8E" app:cardCornerRadius="12dp" app:cardElevation="0dp"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" android:layoutDirection="rtl"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="@string/my_favorites" android:textColor="#FFFFFF" android:textSize="16sp" android:textStyle="bold" /> <TextView android:id="@+id/countFavorites" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentStart="true" android:layout_alignParentBottom="true" android:textColor="#B3FFFFFF" android:textSize="12sp" tools:text="0 Songs" /> </RelativeLayout> </androidx.cardview.widget.CardView> </LinearLayout> <!-- Row 2 --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="32dp" android:orientation="horizontal"> <!-- Most Played --> <androidx.cardview.widget.CardView android:id="@+id/card_most_played" android:layout_width="0dp" android:layout_height="120dp" android:layout_marginStart="8dp" android:layout_marginEnd="16dp" android:layout_weight="1" android:backgroundTint="#B8632A" app:cardCornerRadius="12dp" app:cardElevation="0dp"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" android:layoutDirection="rtl"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="@string/most_played" android:textColor="#FFFFFF" android:textSize="16sp" android:textStyle="bold" /> <TextView android:id="@+id/countMostPlayed" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentStart="true" android:layout_alignParentBottom="true" android:textColor="#B3FFFFFF" android:textSize="12sp" tools:text="0 Songs" /> </RelativeLayout> </androidx.cardview.widget.CardView> <!-- Recently Played --> <androidx.cardview.widget.CardView android:id="@+id/card_recently_played" android:layout_width="0dp" android:layout_height="120dp" android:layout_marginEnd="8dp" android:layout_weight="1" android:backgroundTint="#4A5D8A" app:cardCornerRadius="12dp" app:cardElevation="0dp"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" android:layoutDirection="rtl"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="@string/recently_played" android:textColor="#FFFFFF" android:textSize="16sp" android:textStyle="bold" /> <TextView android:id="@+id/countRecentlyPlayed" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentStart="true" android:layout_alignParentBottom="true" android:textColor="#B3FFFFFF" android:textSize="12sp" tools:text="0 Songs" /> </RelativeLayout> </androidx.cardview.widget.CardView> </LinearLayout> </LinearLayout> <!-- My playlists title --> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="16dp" android:text="@string/my_playlists" android:textColor="#FFFFFF" android:textSize="18sp" android:textStyle="bold" /> <!-- RecyclerView --> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_playlists" android:layout_width="match_parent" android:layout_height="match_parent" android:nestedScrollingEnabled="false" tools:listitem="@layout/item_playlist" /> </LinearLayout> </ScrollView>

So, what is the correct approach to ensure the fragment is visible when navigating back from the backstack without causing overlap with the child navigation's content when navigating to them?

Read Entire Article