androidkotlinandroid-recyclerviewlistadapterandroid-viewholder

Custom Search edittext and CheckBox Not Appearing in Selection Mode with RecyclerView and ListAdapter in Android


I am developing an app that displays a list of audio clips, I use a RecyclerView with a ListAdapter. I’ve implemented a Selection Mode feature where users can select multiple items, but I’m encountering two main issues:

1. My Custom Search View Not Appearing in Selection Mode:

When Selection Mode is activated via the startSelectionMode() function in HomeFragment, I set the visibility of EditText (customSearchView) to View.VISIBLE. However, the search field does not appear on the screen. The customSearchView is defined in fragment_home.xml

2. CheckBox Not Appearing in Selection Mode:

In Selection Mode, each item in the RecyclerView should display a CheckBox (defined in clip_item.xml) to allow selection. However, the CheckBox remains invisible despite setting its visibility to View.VISIBLE in ClipAdapter when isSelectionMode is true. I’ve tried refreshing the RecyclerView with notifyDataSetChanged() and submitList(), but the issue persists.

Additional Details

In HomeFragment, the startSelectionMode() function sets isSelectionMode to true and attempts to show binding.customSearchView by setting its visibility to View.VISIBLE. Despite this, the customSearchView (an EditText) does not appear on the screen. The layout (fragment_home.xml) includes this EditText, and I’ve verified that the binding is correctly initialized.

Second Issue: CheckBox Not Appearing In ClipAdapter, when isSelectionMode is true, I hide the moreOptions ImageView and show the selectionCheckBox.

I’ve attempted to force an update by calling submitList() with a new list when isSelectionMode changes, but this hasn’t helped.

My Code

this clip_item.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp">

    <ImageView
        android:id="@+id/albumImage"
        android:layout_width="@dimen/_48sdp"
        android:layout_height="@dimen/_48sdp"
        android:src="@android:drawable/ic_menu_gallery"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp"
        android:padding="8dp"
        android:orientation="vertical"
        app:layout_constraintBottom_toBottomOf="@+id/albumImage"
        app:layout_constraintEnd_toStartOf="@+id/moreOptions"
        app:layout_constraintStart_toEndOf="@+id/albumImage"
        app:layout_constraintTop_toTopOf="@+id/albumImage">

        <TextView
            android:id="@+id/clipTitle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:gravity="end"
            tools:text="Title"
            android:textSize="@dimen/_12ssp"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/clipArtist"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="4dp"
            android:gravity="end"
            tools:text="Artist"
            android:textSize="@dimen/_11ssp" />
    </LinearLayout>

    <ImageView
        android:id="@+id/moreOptions"
        android:layout_width="@dimen/_20sdp"
        android:layout_height="@dimen/_20sdp"
        android:src="@drawable/baseline_more_vert_48"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <CheckBox
        android:id="@+id/selectionCheckBox"
        android:layout_width="@dimen/_20sdp"
        android:layout_height="@dimen/_20sdp"
        android:visibility="invisible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

ClipAdapter.kt

class ClipAdapter(
    private val selectionListener: OnItemSelectionListener) : ListAdapter<Clip, ClipAdapter.ClipViewHolder>(ClipDiffCallback) {

    var isSelectionMode: Boolean = false
        set(value) {
            field = value

            val newList = currentList.map { it.copy() } 
            submitList(newList)
        }

    private val selectionState: MutableMap<Long, Boolean> = mutableMapOf()

    fun updateSelectionState(clipId: Long, isSelected: Boolean) {
        selectionState[clipId] = isSelected
        notifyItemChanged(currentList.indexOfFirst { it.id == clipId })
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ClipViewHolder {
        val binding = ClipItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ClipViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ClipViewHolder, position: Int) {
        val clip = getItem(position)
        Log.d("ClipAdapter", "Binding item at position $position: ${clip.title}")
        holder.bind(clip)
    }

    inner class ClipViewHolder(
        private val binding: ClipItemBinding
    ): RecyclerView.ViewHolder(binding.root) {

fun bind(clip: Clip) {
     binding.apply {
    if (clip.artUri != null) {
          Glide.with(binding.root)
         .load(clip.artUri)
           .placeholder(R.mipmap.ic_launcher)
             .error(R.mipmap.ic_launcher)
              .into(albumImage)
                } else {
                    albumImage.setImageResource(R.mipmap.ic_launcher)
                }

                clipTitle.text = clip.title
                clipArtist.text = clip.artist ?: "<unknown>"

                if (isSelectionMode) {

                    moreOptions.visibility = View.GONE
                    selectionCheckBox.visibility = View.VISIBLE
                    selectionCheckBox.isChecked = clip.isSelected

                    selectionCheckBox.setOnCheckedChangeListener { _, isChecked ->
                        clip.isSelected = isChecked
                        selectionListener.onItemSelectionChanged(adapterPosition, isChecked)
                    }
                } else {
                    moreOptions.visibility = View.VISIBLE
                    selectionCheckBox.visibility = View.GONE
                    selectionCheckBox.setOnCheckedChangeListener(null) 
                }
            }
        }
    }

    companion object {
        val ClipDiffCallback = object : DiffUtil.ItemCallback<Clip>() {
            override fun areItemsTheSame(oldItem: Clip, newItem: Clip): Boolean {
                val result = oldItem.id == newItem.id
                Log.d(
                    "ClipDiffCallback",
                    "areItemsTheSame: oldItem.id=${oldItem.id}, newItem.id=${newItem.id}, result=$result"
                )
                return result
            }

            override fun areContentsTheSame(oldItem: Clip, newItem: Clip): Boolean {
                val result = oldItem.title == newItem.title &&
                        oldItem.artist == newItem.artist &&
                        oldItem.artUri == newItem.artUri &&
                        oldItem.isSelected == newItem.isSelected
                Log.d(
                    "ClipDiffCallback",
                    "areContentsTheSame: oldItem=$oldItem, newItem=$newItem, result=$result"
                )
                return result
            }
        }
    }

}

HomeFragment This fragment manages the RecyclerView and Selection Mode

interface OnItemSelectionListener {
    fun onItemSelectionChanged(position: Int, isSelected: Boolean)
}

@AndroidEntryPoint
class HomeFragment : Fragment(), MenuProvider, OnItemSelectionListener {

    private var _binding: FragmentHomeBinding? = null
    private val binding get() = _binding!!

    private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
    private val clipViewModel by viewModels<ClipViewModel>()
    private val selectedPositions: MutableSet<Int> = mutableSetOf()
    private var clipAdapter: ClipAdapter? = null
    private var mMainActivityUiController: MainActivityUiController? = null
    private var actionMode: ActionMode? = null
    private var originalClips: List<Clip> = emptyList() 
    private var clipPosition: Int = 0
    private var menu: Menu? = null

    private var isSelectionMode: Boolean = false
        set(value) {
            field = value
            clipAdapter?.isSelectionMode = value
            if (!value) {
                selectedPositions.clear()
                val newList = clipAdapter?.currentList?.map { it.copy(isSelected = false) }
                binding.clipsRV.post { clipAdapter?.submitList(newList) }
            }
        }


    private var menuHost: MenuHost? = null

    fun hideMenu() {
        isSelectionMode = true
        menu?.setGroupVisible(0, false)

    }

    fun showMenu() {
        isSelectionMode = false

        menu?.setGroupVisible(0, true)
        (requireActivity() as MainActivity).getToolbar().visibility = View.VISIBLE

    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        Log.d(TAG, "Adding MenuProvider for HomeFragment")
        menuHost = requireActivity()
        menuHost?.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.STARTED)
        this.clipAdapter = ClipAdapter(this)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setupPermissionLauncher()
        checkAndRequestPermissions()
        observeClips()
        setupRecyclerView()

        binding.clipsRV.visibility = View.VISIBLE

        binding.customSearchView.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                Log.d(TAG, "Filtering with query: $s")
                filterSelectedClips(s.toString())
            }

            override fun afterTextChanged(s: Editable?) {}
        })

        if (activity is MainActivityUiController) {
            mMainActivityUiController = activity as MainActivityUiController
        }
    }

    override fun onResume() {
        super.onResume()
        (requireActivity() as MainActivity).getToolbar().visibility = View.VISIBLE
        mMainActivityUiController?.showTabLayout()
        requireActivity().invalidateOptionsMenu()
    }

    override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {

        this.menu = menu
       
        menu.clear()
        menuInflater.inflate(R.menu.clips_home_menu, menu)
   
        customizeMenuItem(menu, R.id.sortBy, "Sort by")
        customizeMenuItem(menu, R.id.change_layout, "Change layout")

        if (isSelectionMode) {
            menu.setGroupVisible(0, false)
        }

        
        setupSearchView(menu)
    }

   override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
    return when (menuItem.itemId) {
        R.id.select -> {
            isSelectionMode = !isSelectionMode
            if (isSelectionMode) {
                startSelectionMode()
            } else {
                actionMode?.finish()
            }
            true
        }
        R.id.sortBy -> {     
                        true
                    }
                    else -> false
                }
            }

            true
        }
        R.id.change_layout -> {
            
            true
        }
        else -> false
    }
}


    private fun setupSearchView(menu: Menu) {
        val searchManager = requireActivity().getSystemService(SEARCH_SERVICE) as SearchManager
        val searchView = menu.findItem(R.id.app_bar_search).actionView as SearchView
        searchView.setSearchableInfo(searchManager.getSearchableInfo(requireActivity().componentName))
        searchView.queryHint = getString(R.string.library_search)

        searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
            override fun onQueryTextSubmit(keyword: String): Boolean {
                if (keyword.isEmpty()) {
                    Snackbar.make(
                        binding.root,
                        "please enter keyword to search",
                        Snackbar.LENGTH_SHORT
                    ).show()
                    return false
                }
                filterClips(keyword)
                return false
            }

            override fun onQueryTextChange(newText: String): Boolean {
                

                return true
            }
        })
    }
    
    private fun filterClips(keyword: String) {
        val filteredList = if (keyword.isEmpty()) {
            originalClips 
        } else {
            originalClips.filter {
                it.title.contains(keyword, ignoreCase = true) 
            }
        }
        clipAdapter?.submitList(filteredList)
    }

    
    private fun filterSelectedClips(keyword: String) {
        val filteredList = if (keyword.isEmpty()) {
            originalClips 
        } else {
            originalClips.filter {
                it.title.contains(keyword, ignoreCase = true)
            }
        }
        clipAdapter?.submitList(filteredList)
    }

    private fun startSelectionMode() {
        isSelectionMode = true
        
        hideMenu()
        

        binding.customSearchView.visibility = View.VISIBLE

        mMainActivityUiController?.hideTabLayout()

        actionMode = (requireActivity() as MainActivity).startSupportActionMode(object :
            ActionMode.Callback {
            override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
                (requireActivity()as MainActivity).getToolbar().visibility = View.GONE
                mode.menuInflater.inflate(R.menu.select_all_menu, menu)
                mode.title = "${selectedPositions.size} clips selected"
                updateActionModeTitle()
                return true
            }

            override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean = false

            override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
                return when (item.itemId) {
                    R.id.action_select_all -> {
                        selectAll()
                        true
                    }

                    else -> false
                }
            }

            override fun onDestroyActionMode(mode: ActionMode) {
                isSelectionMode = false
                clipAdapter?.isSelectionMode = false
                
                showMenu()
                
                mMainActivityUiController?.showTabLayout()
                
                binding.customSearchView.visibility = View.GONE
                binding.customSearchView.text.clear() 
                clipAdapter?.submitList(originalClips) 
                actionMode = null
                binding.clipsRV.scrollToPosition(0)
                requireActivity().invalidateOptionsMenu()
            }
        })
    }

    private fun updateActionModeTitle() {
        val count = selectedPositions.size
        actionMode?.title = "$count selected clips"
    }

    override fun onItemSelectionChanged(position: Int, isSelected: Boolean) {
        if (isSelected) {
            selectedPositions.add(position)
        } else {
            selectedPositions.remove(position)
        }

        
        val newList = clipAdapter?.currentList!!.toMutableList()
        newList[position] = newList[position].copy(isSelected = isSelected)
        binding.clipsRV.post { clipAdapter?.submitList(newList) }

        updateActionModeTitle()
    }

    private fun selectAll() {
        if (clipAdapter?.currentList?.size == selectedPositions.size) {
            
            selectedPositions.clear()
            val newList = clipAdapter?.currentList!!.map { it.copy(isSelected = false) }
            binding.clipsRV.post { clipAdapter?.submitList(newList) }
        } else {
            
            selectedPositions.clear()
            selectedPositions.addAll(clipAdapter?.currentList!!.indices)
            val newList = clipAdapter?.currentList?.map { it.copy(isSelected = true) }
            binding.clipsRV.post { clipAdapter?.submitList(newList) }
        }
        updateActionModeTitle()
    }

    private fun setupPermissionLauncher() {
        requestPermissionLauncher =
            registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
                if (isGranted) {
                    clipViewModel.syncAndFetchClips()
                } else {
                    val permissionToRequest =
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                            Manifest.permission.READ_MEDIA_AUDIO
                        } else {
                            Manifest.permission.READ_EXTERNAL_STORAGE
                        }
                    if (!shouldShowRequestPermissionRationale(permissionToRequest)) {
                        AlertDialog.Builder(requireContext())
                            .setTitle("Permission Denied")
                            .setMessage("This app needs access to your music files to display and play them. Please enable the permission in app settings.")
                            .setPositiveButton("Go to Settings") { _, _ ->
                                val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                                intent.data =
                                    Uri.fromParts("package", requireContext().packageName, null)
                                startActivity(intent)
                            }
                            .setNegativeButton("Cancel") { _, _ ->
                                Toast.makeText(
                                    requireContext(),
                                    "Permission denied. Cannot access music files.",
                                    Toast.LENGTH_SHORT
                                ).show()
                            }
                            .show()
                    } else {
                        Toast.makeText(
                            requireContext(),
                            "Permission denied. Cannot access music files.",
                            Toast.LENGTH_SHORT
                        ).show()
                    }
                }
            }
    }

    private fun checkAndRequestPermissions() {
        val permissionToRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            Manifest.permission.READ_MEDIA_AUDIO
        } else {
            Manifest.permission.READ_EXTERNAL_STORAGE
        }

        if (ContextCompat.checkSelfPermission(
                requireContext(),
                permissionToRequest
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            if (shouldShowRequestPermissionRationale(permissionToRequest)) {
                showPermissionRationaleDialog(permissionToRequest)
            } else {
                requestPermissionLauncher.launch(permissionToRequest)
            }
        } else {
            clipViewModel.syncAndFetchClips()
        }
    }

    private fun showPermissionRationaleDialog(permission: String) {
        AlertDialog.Builder(requireContext())
            .setTitle("Permission Required")
            .setMessage("This app needs access to your music files to display and play them. Please grant the permission to continue.")
            .setPositiveButton("Grant") { _, _ ->
                requestPermissionLauncher.launch(permission)
            }
            .setNegativeButton("Deny") { _, _ ->
                Toast.makeText(
                    requireContext(),
                    "Permission denied. Cannot access music files.",
                    Toast.LENGTH_SHORT
                ).show()
            }
            .show()
    }

    private fun observeClips() {
        lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                clipViewModel.clipList.collect { resource ->
                    when (resource) {
                        is Resource.Loading -> {
                            binding.progressBar.visibility = View.VISIBLE
                        }

                        is Resource.Success -> {
                            binding.progressBar.visibility = View.GONE
                            originalClips = resource.data ?: emptyList()
                            Log.d(TAG, "Clips loaded, size: ${originalClips.size}")
                            clipAdapter?.submitList(originalClips) {
                                Log.d(TAG, "Submitted original clips to clipAdapter")
                            }
                        }

                        is Resource.Error -> {
                            binding.progressBar.visibility = View.GONE
                            Toast.makeText(
                                requireContext(),
                                "Error: ${resource.message}",
                                Toast.LENGTH_SHORT
                            ).show()
                        }

                        is Resource.Empty -> {
                            binding.progressBar.visibility = View.GONE
                        }

                        else -> {}
                    }
                }
            }
        }
    }

    private fun setupRecyclerView() {
        binding.clipsRV.apply {
            setHasFixedSize(true)
            setItemViewCacheSize(10)
            layoutManager =
                LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
            adapter = clipAdapter

            
            onItemClick { _, position, _ ->
                if (!isSelectionMode) {
                    
                    clipPosition = position

                    val navController = requireActivity()
                        .findNavController(R.id.nav_host_fragment_container)

                    (requireActivity() as MainActivity).showNavHostFragmentContainer()

                    if (navController.currentDestination?.id == R.id.homeFragment) {
                        navController.navigate(
                            HomeFragmentDirections.actionHomeFragmentToPlayerFragment(
                                clipPosition,
                                clipsArray = clipAdapter?.currentList!!.toTypedArray()
                            )
                        )
                    }
                } else {
                    
                    val clip = clipAdapter?.currentList[position]
                    val newCheckedState = !clip?.isSelected!!
                    clip.isSelected = newCheckedState
                    onItemSelectionChanged(position, newCheckedState)
                }
            }
            
            onItemCheckChange { _, position, isChecked ->
                if (isSelectionMode) {
                    onItemSelectionChanged(position, isChecked)
                }
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        Log.d(TAG, "Removing MenuProvider for HomeFragment")
        clipAdapter = null
        menuHost?.removeMenuProvider(this)
        binding.clipsRV.removeItemClickSupport()
        _binding = null
    }

}

And I handle clicks and checkboxes with this class ItemClickSupport.kt

class ItemClickSupport private constructor(private val recyclerView: RecyclerView) {

    private var onItemClickListener: OnRecyclerViewItemClickListener? = null
    private var onItemLongClickListener: OnRecyclerViewItemLongClickListener? = null
    private var onItemCheckChangeListener: OnRecyclerViewItemCheckChangeListener? = null

    private val attachListener: RecyclerView.OnChildAttachStateChangeListener = object : RecyclerView.OnChildAttachStateChangeListener {
        override fun onChildViewAttachedToWindow(view: View) {
            val holder = this@ItemClickSupport.recyclerView.getChildViewHolder(view)


            val isClickable = (holder as? ItemClickSupportViewHolder)?.isClickable ?: true
            val isLongClickable = (holder as? ItemClickSupportViewHolder)?.isLongClickable ?: true

            if (onItemClickListener != null && isClickable) {
                view.setOnClickListener(onClickListener)
            }

            if (onItemLongClickListener != null && isLongClickable) {
                view.setOnLongClickListener(onLongClickListener)
            }

            val checkBox = view.findViewById<CheckBox>(R.id.selectionCheckBox)
            if (checkBox != null && onItemCheckChangeListener != null) {
                checkBox.setOnCheckedChangeListener { _, isChecked ->
                    val position = holder.getAdapterPosition()
                    if (position != RecyclerView.NO_POSITION) {
                        onItemCheckChangeListener?.invoke(recyclerView, position, isChecked)
                    }
                }
            }
        }

        override fun onChildViewDetachedFromWindow(view: View) {
            val checkBox = view.findViewById<CheckBox>(R.id.selectionCheckBox)
            checkBox?.setOnCheckedChangeListener(null)
        }
    }

    init {
        this.recyclerView.setTag(R.id.item_click_support, this)
        this.recyclerView.addOnChildAttachStateChangeListener(attachListener)
    }

    companion object {
        fun addTo(view: RecyclerView): ItemClickSupport {
            var support: ItemClickSupport? = view.getTag(R.id.item_click_support) as? ItemClickSupport
            if (support == null) {
                support = ItemClickSupport(view)
            }
            return support
        }

        fun removeFrom(view: RecyclerView): ItemClickSupport? {
            val support = view.getTag(R.id.item_click_support) as? ItemClickSupport
            support?.detach(view)
            return support
        }
    }

    private val onClickListener = View.OnClickListener { v ->
        val listener = onItemClickListener ?: return@OnClickListener
        val holder = this.recyclerView.getChildViewHolder(v)
        val position = holder.getAdapterPosition()
        if (position != RecyclerView.NO_POSITION) {
            listener.invoke(this.recyclerView, position, v)
        }
    }

    private val onLongClickListener = View.OnLongClickListener { v ->
        val listener = onItemLongClickListener ?: return@OnLongClickListener false
        val holder = this.recyclerView.getChildViewHolder(v)
        val position = holder.getAdapterPosition()
        if (position != RecyclerView.NO_POSITION) {
            return@OnLongClickListener listener.invoke(this.recyclerView, position, v)
        }
        return@OnLongClickListener false
    }

    private fun detach(view: RecyclerView) {
        view.removeOnChildAttachStateChangeListener(attachListener)
        view.setTag(R.id.item_click_support, null)
    }

    fun onItemClick(listener: OnRecyclerViewItemClickListener?): ItemClickSupport {
        onItemClickListener = listener
        return this
    }

    fun onItemLongClick(listener: OnRecyclerViewItemLongClickListener?): ItemClickSupport {
        onItemLongClickListener = listener
        return this
    }

    fun onItemCheckChange(listener: OnRecyclerViewItemCheckChangeListener?): ItemClickSupport {
        onItemCheckChangeListener = listener
        return this
    }
}

interface ItemClickSupportViewHolder {
    val isClickable: Boolean get() = true
    val isLongClickable: Boolean get() = true
}

fun RecyclerView.addItemClickSupport(configuration: ItemClickSupport.() -> Unit = {}) = ItemClickSupport.addTo(this).apply(configuration)

fun RecyclerView.removeItemClickSupport() = ItemClickSupport.removeFrom(this)

fun RecyclerView.onItemClick(onClick: OnRecyclerViewItemClickListener) {
    addItemClickSupport { onItemClick(onClick) }
}

fun RecyclerView.onItemLongClick(onLongClick: OnRecyclerViewItemLongClickListener) {
    addItemClickSupport { onItemLongClick(onLongClick) }
}

fun RecyclerView.onItemCheckChange(onCheckChange: OnRecyclerViewItemCheckChangeListener) {
    addItemClickSupport { onItemCheckChange(onCheckChange) }
}

This image shows the current two issues when I enter the selection mode and change all three dots with checkboxes, and shows my custom search editText

enter image description here


Solution

  • [Solved] Fragment onCreateView being called twice with ViewPager2 and NavHostFragment (Causing UI Issues)

    After significant debugging, I've identified and resolved the issues described in my question, where my customSearchView and item CheckBox were not appearing correctly during selection mode in my HomeFragment. The root cause was subtle, stemming from the interaction between ViewPager2 and NavHostFragment within my MainActivity.

    Original Problems Recap:

    1. My customSearchView (an EditText) in fragment_home.xml failed to become visible when selection mode was programmatically activated.

    2. The CheckBox within my RecyclerView items (clip_item.xml) also remained invisible in selection mode, despite adapter logic setting its visibility to VISIBLE.

    Debugging and Root Cause Discovery:

    By adding detailed lifecycle logging (onCreate, onCreateView, onViewCreated with instance logging) to HomeFragment and logging createFragment in my ViewPagerAdapter, I confirmed that HomeFragment was being instantiated twice during the initial launch of MainActivity.

    This double instantiation, managed by two different systems (FragmentStateAdapter and NavHostFragment), created lifecycle conflicts and state inconsistencies. This explained why UI updates (like making the search view visible or updating RecyclerView items to show checkboxes) were failing or behaving unpredictably. The updates were likely targeting one instance while the other was potentially being displayed or measured, or state management between the two became unreliable. The double permission requests I initially encountered were also a direct symptom of this duplicate fragment creation.

    The Solution: Fixing the Conflicting startDestination

    The key was to ensure only one system was responsible for creating the initial HomeFragment. Since it belongs in the ViewPager2, I needed to prevent the NavHostFragment from creating it as well. (My NavHostFragment is primarily used for navigating to detail screens like PlayerFragment from the tabs).

    1. Created a Placeholder Fragment: I introduced a simple, empty fragment (NavHostPlaceholderFragment) solely to serve as a neutral starting point for the NavHost.

      Kotlin

      // NavHostPlaceholderFragment.kt
      package com.example.mmlaudioplayer.ui // My package name
      
      import android.os.Bundle
      import android.view.LayoutInflater
      import android.view.View
      import android.view.ViewGroup
      import androidx.fragment.app.Fragment
      import android.util.Log // Added Log
      
      class NavHostPlaceholderFragment : Fragment() {
          override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
              Log.d("NavHostPlaceholder", "onCreateView called for placeholder")
              return null // No view needed
          }
      }
      
      
    2. Modified nav_graph.xml: I changed the app:startDestination attribute in the root <navigation> tag to point to the ID of this new placeholder fragment. The <fragment> definition for homeFragment itself remains necessary to define the action used for navigating away from it.

      XML

      <navigation ...
          app:startDestination="@id/navHostPlaceholderFragment"> <fragment
              android:id="@+id/navHostPlaceholderFragment"
              android:name="com.example.mmlaudioplayer.ui.NavHostPlaceholderFragment"
              android:label="NavHostPlaceholderFragment" />
      
          <fragment
              android:id="@+id/homeFragment"
              android:name="com.example.mmlaudioplayer.ui.HomeFragment"
              ...>
              <action
                  android:id="@+id/action_homeFragment_to_playerFragment"
                  app:destination="@id/playerFragment" ... />
          </fragment>
      
          <fragment android:id="@+id/playerFragment" ... > ... </fragment>
      </navigation>
      
      

    Outcome:

    After implementing this change (and ensuring a clean build/install), HomeFragment is now created only once by the ViewPagerAdapter. The double lifecycle calls are gone, the permission requests behave correctly, and most importantly, the UI updates for entering selection mode now work as expected – both the customSearchView and the item CheckBoxes appear reliably because the logic is operating on the single, correct fragment instance.

    Issue Tracker Reference:

    For others who might encounter this specific interaction between ViewPager2/FragmentStateAdapter and a NavHostFragment using the same fragment class for a page and as the startDestination, I have reported this behavior to the Google Issue Tracker: https://issuetracker.google.com/issues/411460072 (You may want to star the issue or add your own findings if you encounter this).