带复选框的RecyclerView项选择:跨Fragment展示选中项及状态保存
搞定电影分类选择+状态保存+跨Fragment传递(适配电视端)
嘿,作为Kotlin新手,咱们一步步来实现这个需求,完全贴合你的现有代码,还适配电视端的RecyclerView操作~
一、先改造你的数据类和适配器,处理复选框状态
首先,咱们得给每个分类加个「选中状态」的标记。你可以直接给Data类加个isSelected属性(如果允许修改数据类的话),或者用一个单独的MutableSet<Int>来存选中的分类ID(这种方式更灵活,不污染原始API数据)。这里推荐第二种,因为原始接口返回的数据一般不会带选中状态:
改造后的适配器代码
class MovieCategoryAdapter( private val selectedCategoryIds: MutableSet<Int>, // 存储选中的分类ID private val onCheckedChange: (Int, Boolean) -> Unit // 状态变化的回调函数 ) : ListAdapter<Data, MovieCategoryAdapter.ViewHolder>(MOVIE_CATEGORY_COMPERATOR) { inner class ViewHolder(val binding: RvItemMoviecategoryBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(category: Data) { binding.apply { rvItemMoviecategory.text = category.title // 根据已选ID集合设置复选框状态 rvCheckBox.isChecked = selectedCategoryIds.contains(category.id) // 处理复选框选中状态变化(电视端也兼容,确认键会触发点击事件) rvCheckBox.setOnCheckedChangeListener { _, isChecked -> onCheckedChange(category.id, isChecked) } // 电视端优化:Item获取焦点时自动把焦点给到复选框,方便遥控器操作 itemView.setOnFocusChangeListener { _, hasFocus -> if (hasFocus) { rvCheckBox.requestFocus() } } } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( RvItemMoviecategoryBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val movieGenre = getItem(position)!! holder.bind(movieGenre) } companion object { private val MOVIE_CATEGORY_COMPERATOR = object : DiffUtil.ItemCallback<Data>() { override fun areItemsTheSame(oldItem: Data, newItem: Data) = oldItem.id == newItem.id override fun areContentsTheSame(oldItem: Data, newItem: Data) = oldItem == newItem } } }
二、在Fragment A中实现状态保存和恢复
咱们用SharedPreferences来持久化保存选中的分类ID,这是最适合新手的轻量存储方案:
Fragment A核心代码
class FragmentA : Fragment() { private lateinit var binding: FragmentABinding private lateinit var adapter: MovieCategoryAdapter private val selectedCategoryIds = mutableSetOf<Int>() private lateinit var sharedPrefs: SharedPreferences override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = FragmentABinding.inflate(inflater, container, false) sharedPrefs = requireContext().getSharedPreferences("MovieCategoryPrefs", Context.MODE_PRIVATE) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 1. 恢复上次保存的选中状态 restoreSelectedState() // 2. 初始化RecyclerView(电视端推荐用LinearLayoutManager,减少焦点乱跳) binding.rvCategory.layoutManager = LinearLayoutManager(requireContext()) adapter = MovieCategoryAdapter(selectedCategoryIds) { categoryId, isChecked -> // 更新选中ID集合 if (isChecked) { selectedCategoryIds.add(categoryId) } else { selectedCategoryIds.remove(categoryId) } } binding.rvCategory.adapter = adapter // 3. 用Retrofit加载分类数据(替换成你自己的接口请求代码) loadMovieCategories() // 4. 保存按钮点击逻辑 binding.btnSave.setOnClickListener { saveSelectedState() // 跳转到Fragment B并传递选中的分类 navigateToFragmentB() } } private fun restoreSelectedState() { // 从SharedPreferences读取已保存的ID集合 val savedIds = sharedPrefs.getStringSet("selected_category_ids", emptySet()) ?: emptySet() selectedCategoryIds.clear() selectedCategoryIds.addAll(savedIds.map { it.toInt() }) } private fun saveSelectedState() { // 将选中ID集合写入SharedPreferences val editor = sharedPrefs.edit() editor.putStringSet("selected_category_ids", selectedCategoryIds.map { it.toString() }.toSet()) editor.apply() } private fun loadMovieCategories() { // 这里替换成你的Retrofit请求逻辑,示例: // yourApiService.getMovieCategories().enqueue(object : Callback<List<Data>> { // override fun onResponse(call: Call<List<Data>>, response: Response<List<Data>>) { // response.body()?.let { adapter.submitList(it) } // } // override fun onFailure(call: Call<List<Data>>, t: Throwable) { // // 处理请求失败逻辑,比如Toast提示 // } // }) } private fun navigateToFragmentB() { // 从适配器数据中筛选出选中的分类 val selectedCategories = adapter.currentList.filter { selectedCategoryIds.contains(it.id) } // 用Bundle传递数据(也可以用ViewModel跨Fragment共享,更优雅) val bundle = Bundle().apply { putParcelableArrayList("selected_categories", ArrayList(selectedCategories)) } // 切换到Fragment B parentFragmentManager.beginTransaction() .replace(R.id.container, FragmentB().apply { arguments = bundle }) .addToBackStack(null) .commit() } }
三、Fragment B接收并展示选中的分类
Fragment B的逻辑很简单,只需要接收传递过来的分类数据,用RecyclerView展示即可:
Fragment B核心代码
class FragmentB : Fragment() { private lateinit var binding: FragmentBBinding private lateinit var adapter: SelectedCategoryAdapter override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = FragmentBBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 获取Fragment A传递过来的选中分类 val selectedCategories = arguments?.getParcelableArrayList<Data>("selected_categories") ?: emptyList() // 初始化RecyclerView binding.rvSelectedCategories.layoutManager = LinearLayoutManager(requireContext()) adapter = SelectedCategoryAdapter(selectedCategories) binding.rvSelectedCategories.adapter = adapter } // 简单的适配器,用于展示选中的分类 class SelectedCategoryAdapter(private val categories: List<Data>) : RecyclerView.Adapter<SelectedCategoryAdapter.ViewHolder>() { inner class ViewHolder(val binding: RvItemSelectedCategoryBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(category: Data) { binding.tvCategoryName.text = category.title } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( RvItemSelectedCategoryBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind(categories[position]) } override fun getItemCount() = categories.size } }
四、电视端适配关键要点
因为电视端用遥控器操作,需要注意这些细节:
- 给Item根布局添加焦点属性:
android:focusable="true"、android:focusableInTouchMode="true",再自定义一个focus_selectordrawable作为背景,让焦点状态清晰可见。 - 避免焦点丢失:优先用
LinearLayoutManager,比GridLayoutManager的焦点逻辑更稳定。 - 复选框与Item联动:适配器里已经处理了Item焦点切换时自动把焦点给到复选框,用户按确认键就能直接切换选中状态,符合电视端操作习惯。
为什么不用RecyclerView-Selection?
RecyclerView-Selection更适合批量多选(比如长按进入多选模式),但你的需求是单个复选框独立切换状态,用咱们上面的方案更简单,代码量少,新手更容易理解和调试,完全能满足你的需求~
内容的提问来源于stack exchange,提问作者Alex Mutschl
相关产品推荐
相关产品推荐

