You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

带复选框的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_selector drawable作为背景,让焦点状态清晰可见。
  • 避免焦点丢失:优先用LinearLayoutManager,比GridLayoutManager的焦点逻辑更稳定。
  • 复选框与Item联动:适配器里已经处理了Item焦点切换时自动把焦点给到复选框,用户按确认键就能直接切换选中状态,符合电视端操作习惯。

为什么不用RecyclerView-Selection?

RecyclerView-Selection更适合批量多选(比如长按进入多选模式),但你的需求是单个复选框独立切换状态,用咱们上面的方案更简单,代码量少,新手更容易理解和调试,完全能满足你的需求~

内容的提问来源于stack exchange,提问作者Alex Mutschl

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 17:25:24