Android RecyclerView 实战:点击频道栏切换对应内容

文章目录

本文以一个"小红书发现页"的实际开发场景为例,讲解如何使用 RecyclerView + Adapter + 回调 + 数据映射 实现:

点击顶部频道 → 当前频道改变 → 瀑布流显示对应内容

适合已经学习过 RecyclerView 基础,但还不太理解 Adapter 和 Fragment 如何进行数据交互的 Android 初学者。


一、引入:RecyclerView 学会了,为什么还是不会做"频道切换"?

很多人在刚学完 RecyclerView 后,已经知道了RecyclerView, LayoutManager, Adapter, ViewHolder各自负责的内容. 也知道 Adapter 大概需要写onCreateViewHolder(), onBindViewHolder(), getItemCount()这些方法, 但是一旦遇到真正的页面:

text 复制代码
┌──────────────────────────────┐
│ 推荐  穿搭  美食  彩妆  旅行   │ ← 频道栏
├──────────────────────────────┤
│                              │
│   图片       图片             │
│                              │
│   标题       标题             │
│                              │
│   图片       图片             │ ← 瀑布流
│                              │
└──────────────────────────────┘

就会出现一个问题:**点击上面的"美食",下面的 RecyclerView 怎么换成美食内容?RecyclerView 本身并不知道"美食"是什么。所以真正的问题其实不是:"怎么刷新 RecyclerView?". 而是点击事件发生以后,怎么把"用户选择了哪个频道"传递给负责数据的地方,然后让 Adapter 显示新的数据?**这就是本文要解决的问题。


二、先认识我们的页面结构

假设我们正在仿写一个小红书发现页。整个页面可以简化成:

text 复制代码
DiscoverFragment
│
├── 频道 RecyclerView
│      ├── 推荐
│      ├── 穿搭
│      ├── 美食
│      ├── 彩妆
│      └── ...
│
└── 瀑布流 RecyclerView
       ├── Note
       ├── Note
       ├── Note
       └── ...

这里实际上有两个 RecyclerView。

第一个频道 RecyclerView负责显示"推荐、穿搭、美食......", 第二个Feed RecyclerView负责显示具体的笔记内容。因此它们之间存在一个关系是频道 RecyclerView显示对应的子项, 用户点击"美食"后, 找到"美食"对应的数据, 传入到Feed RecyclerView后刷新数据, 显示美食笔记


三、第一步:先理解 RecyclerView 到底显示什么

假设我们的笔记是:

java 复制代码
public class FeedItem {

    private final int imageResId;
    private final String title;
    private final String nickname;
    private final int likeCount;

    public FeedItem(
            int imageResId,
            String title,
            String nickname,
            int likeCount) {

        this.imageResId = imageResId;
        this.title = title;
        this.nickname = nickname;
        this.likeCount = likeCount;
    }
}

一条笔记就是一个FeedItem. 那么List就是一组笔记. 例如, 多组笔记构成的推荐页面可以表示成List recommendList;


四、一个频道对应一组数据

接下来就很自然了。我们有三个频道栏目推荐页面, 穿搭页面和美食页面各自可以对应一组笔记, 可以各自表示成一组list列表, 如List, List, List. 如果我们把它放进一个 Map:Map<String, List>

它的含义就是**用频道名称找到这个频道对应的笔记列表。**例如:

java 复制代码
channelFeedMap.put(
        "推荐",
        recommendList
);

channelFeedMap.put(
        "穿搭",
        clothingList
);

channelFeedMap.put(
        "美食",
        foodList
);

于是就用一个Map可以容纳所有的List

text 复制代码
channelFeedMap
│
├── "推荐" → recommendList
├── "穿搭" → clothingList
└── "美食" → foodList

这一步其实就是整个功能的数据基础。


五、为什么需要 Adapter?

如果你之前学过 RecyclerView,应该知道RecyclerView自己并不知道"我应该显示哪 20 条笔记?"它需要问 Adapter去索要数据。

Adapter 可以理解成 RecyclerView 的"数据翻译员"。用List存储的数据举例, Adapter通过获得List对应的数据来通知RecyclerView如何显示Item的具体内容

java 复制代码
FeedAdapter adapter =  new FeedAdapter(recommendList);

findRecycler.setAdapter(adapter);

此时recommendList通过FeedAdapter来改变RecyclerView的显示内容


六、那么问题来了:点击频道以后怎么办?

假设当前显示推荐页面, 里面是一组推荐内容, 当用户点击穿搭后, 我们希望程序能找到穿搭的对应数据, 让FeedAdapter里的数据List变成clothingList, 让RecyclerView显示新的内容. 所以我们需要解决两个问题:频道 RecyclerView 怎么知道用户点击了哪个频道?知道以后,怎么把这个信息告诉 DiscoverFragment?


七、在 Adapter 里面处理点击事件

最直接的写法就是在Adapter里面处理点击事件

java 复制代码
holder.itemView.setOnClickListener(v -> {

    int position = holder.getBindingAdapterPosition();

});

八、但是 Adapter 不应该直接修改 Fragment

初学者很容易想到在ChannelAdapter的点击事件里直接找到DiscoverFragment再去修改RecyclerView的内容. 但这种做法不太好。

为什么?因为 Adapter 的职责应该主要是:**把数据显示成 RecyclerView 的 Item,并处理 Item 的交互。**而"点击美食之后应该显示什么". 属于页面业务逻辑。所以我们让 Adapter:**只负责通知外面:"有人点击了第几个频道。"**这就需要一个回调。


九、使用接口传递点击事件

我们可以在 ChannelAdapter 里面定义:

java 复制代码
public interface OnChannelClickListener {

    void onChannelClick(int position);

}

这句话可以理解成:

"谁使用 ChannelAdapter,谁就可以注册一个监听器。当频道被点击时,我把 position 告诉他。"

然后 Adapter 保存这个监听器:

java 复制代码
private OnChannelClickListener listener;

构造 Adapter 时传进去:

java 复制代码
public ChannelAdapter(
        List<String> list,
        OnChannelClickListener listener) {

    channelList = list;
    this.listener = listener;
}

十、点击的时候通知外部

这里是原来的点击事件

java 复制代码
holder.itemView.setOnClickListener(v -> {

    int newPosition =
            holder.getBindingAdapterPosition();

    if (newPosition == RecyclerView.NO_POSITION) {
        return;
    }

});

我们可以通过接口改写成下面的代码

java 复制代码
holder.itemView.setOnClickListener(v -> {

    int newPosition =
            holder.getBindingAdapterPosition();

    if (newPosition == RecyclerView.NO_POSITION) {
        return;
    }

    if (listener != null) {
        listener.onChannelClick(newPosition);
    }

});

现在这种代码, 当用户点击频道后, 通知ChannelAdapter, 得到的是position, 通过listener接口去实现对应的点击事件

Adapter 不需要知道"美食应该显示什么。"

它只需要说"用户点了 position = 2。"


十一、Fragment 接收到这个事件

在 DiscoverFragment 中:

java 复制代码
channelAdapter = new ChannelAdapter(
        channels,
        position -> {

            // position 就是点击的频道位置

        }
);

例如:

java 复制代码
channelAdapter = new ChannelAdapter(
        channels,
        position -> {

            Log.d(
                    "DiscoverFragment",
                    "点击了第 " + position + " 个频道"
            );

        }
);

十二、从 position 找到频道名称

我们有:

java 复制代码
List<String> channels =
        Arrays.asList(
                "推荐",
                "穿搭",
                "美食",
                "彩妆",
                "旅行",
                "游戏",
                "摄影",
                "健身"
        );

如果position = 2 那么有

java 复制代码
String channelName =
        channels.get(position);

这样得到的就是美食


十三、再通过频道名称找到对应数据

现在我们已经有channelName, 例如"美食"

java 复制代码
List<FeedItem> newFeed =
        channelFeedMap.get(channelName);

整个过程如下所示

text 复制代码
position = 2
     ↓
channels.get(2)
     ↓
   "美食"
     ↓
channelFeedMap.get("美食")
     ↓
  foodList

这就是我们前面为什么要建立Map<String, List>的原因


十四、最后让 FeedAdapter 换数据

原来的 FeedAdapter里的List是这样写的

java 复制代码
public FeedAdapter(List<FeedItem> feedList) {
    this.feedList = feedList;
}

我们需要增加一个方法notifyDataSetChanged()可以去刷新显示的列表

java 复制代码
public void setFeedList(
        List<FeedItem> feedList) {

    this.feedList = feedList;

    notifyDataSetChanged();
}

于是feedAdapter.setFeedList(newFeed);就表示

"Adapter 不要再显示旧的 FeedList 了,现在改成新的 FeedList。"


十五、notifyDataSetChanged() 是干什么的?

这是整个功能最后的一环。我们执行feedAdapter.setFeedList(foodList);

现在实际上变成FeedAdapter传入foodList

但是 RecyclerView 需要知道"数据发生变化了!". 所以notifyDataSetChanged();

就是告诉 RecyclerViewAdapter 的数据变了,请重新刷新。

于是调用notifyDataSetChanged()后会刷新对应的RecyclerView


十六、最终代码结构

最终 DiscoverFragment 中核心逻辑大概是:

java 复制代码
channelAdapter = new ChannelAdapter(
        channels,
        position -> {

            String channelName =
                    channels.get(position);

            List<FeedItem> newFeed =
                    channelFeedMap.get(channelName);

            if (newFeed == null) {
                return;
            }

            feedAdapter.setFeedList(
                    newFeed
            );

            findRecycler.scrollToPosition(0);
        }
);

这几行虽然不多,但是实际上完成了很多事情:

text 复制代码
position
   ↓
频道
   ↓
数据
   ↓
Adapter
   ↓
RecyclerView

十七、为什么不直接用 List<List<FeedItem>>?

你可能会想到:

java 复制代码
List<List<FeedItem>> allFeed;

然后:

java 复制代码
allFeed.get(position);

其实也完全可以。

例如:

text 复制代码
position 0 → 推荐
position 1 → 穿搭
position 2 → 美食

于是:

java 复制代码
List<FeedItem> newFeed =
        allFeed.get(position);

这种方式也能工作。

那么为什么我们这里使用:

java 复制代码
Map<String, List<FeedItem>>

?

因为它表达的语义更加直观:

text 复制代码
"美食" → 美食数据

而不是:

text 复制代码
2 → 某一组数据

当项目变复杂以后,数据结构表达的含义本身就是代码可读性的一部分。

不过对于纯粹按照 position 管理的页面,List<List<FeedItem>> 也完全合理。


十八、还有一个更重要的问题:频道名称真的适合当 Key 吗?

目前我们写:

java 复制代码
Map<String, List<FeedItem>>

Key 是:

text 复制代码
"推荐"
"穿搭"
"美食"

小项目完全没问题。

但真正的 App 通常不会这么设计。

例如:

java 复制代码
class Channel {

    int id;
    String name;
}

数据可能变成:

text 复制代码
Channel
├── id = 1
├── name = "推荐"
│
Channel
├── id = 2
├── name = "穿搭"
│
Channel
├── id = 3
└── name = "美食"

然后:

text 复制代码
频道 ID
   ↓
数据查询

这就比直接拿中文名称作为唯一标识更加稳定。


十九、再进一步:不要让 Fragment 管理所有数据

现在我们的结构是:

text 复制代码
DiscoverFragment
│
├── channels
├── channelFeedMap
├── ChannelAdapter
└── FeedAdapter

对于当前这个学习项目完全可以。

但是项目继续扩大以后,Fragment 会越来越臃肿:

text 复制代码
DiscoverFragment
├── 创建数据
├── 管理频道
├── 网络请求
├── 刷新
├── 数据缓存
├── 点击事件
├── UI 状态
└── ...

这时候就应该开始考虑:

text 复制代码
ViewModel

例如:

text 复制代码
DiscoverFragment
       │
       ↓
DiscoverViewModel
       │
       ↓
Repository
       │
       ├── 网络
       └── 本地数据库

Fragment 主要负责:

UI。

ViewModel 负责:

页面状态。

Repository 负责:

数据从哪里来。


二十、再进一步:notifyDataSetChanged() 并不是最优方案

我们目前使用:

java 复制代码
notifyDataSetChanged();

非常适合学习阶段,因为简单直接。

但是它相当于告诉 RecyclerView:

"别管哪里变了,整个列表重新检查一下。"

如果数据很多,这种方式就比较粗。

Android 后面提供了:

text 复制代码
ListAdapter
+
DiffUtil

可以比较:

text 复制代码
旧 List
vs
新 List

然后只更新真正发生变化的 Item。

例如:

text 复制代码
100 条笔记

只有第 35 条变化

DiffUtil 可以判断:

text 复制代码
35 → changed
其他 → unchanged

这就是 RecyclerView 性能优化中非常重要的一块。

不过当前阶段不需要为了"高级"而提前使用它。

先把:

text 复制代码
数据 → Adapter → RecyclerView

理解清楚更重要。


二十一、再进一步:如果数据来自网络怎么办?

现在我们的数据是:

text 复制代码
代码里写死

真实项目可能是:

text 复制代码
服务器
   ↓
JSON
   ↓
Java/Kotlin 对象
   ↓
List<FeedItem>
   ↓
Adapter
   ↓
RecyclerView

例如:

text 复制代码
用户点击"美食"
       ↓
请求:
/feed?channel=food
       ↓
服务器返回 JSON
       ↓
解析成 List<FeedItem>
       ↓
Adapter 更新

这时候你之前学习的:

text 复制代码
HTTP
JSON
Retrofit
Kotlin 协程

就开始和 RecyclerView 真正联系起来了。


二十二、再进一步:如果用户切换频道很频繁怎么办?

比如:

text 复制代码
推荐
 ↓
美食
 ↓
穿搭
 ↓
旅行
 ↓
美食

如果每次都访问网络:

text 复制代码
点击美食
 ↓
请求网络

点击穿搭
 ↓
请求网络

再次点击美食
 ↓
又请求网络

就会产生重复请求。

这时候可以加入缓存:

text 复制代码
channelCache
│
├── 推荐 → 已加载
├── 穿搭 → 已加载
├── 美食 → 已加载
└── 旅行 → 未加载

于是:

text 复制代码
第一次点击美食
    ↓
网络请求
    ↓
保存数据

第二次点击美食
    ↓
直接使用缓存

这就开始从:

RecyclerView UI

逐渐进入:

数据层设计。


二十三、最终可以形成一套完整架构

我们这篇文章最开始只是:

text 复制代码
点击频道
↓
换 RecyclerView 内容

但一步一步向后扩展,就会得到:

text 复制代码
┌─────────────────────────────┐
│          UI 层              │
│                             │
│ Channel RecyclerView        │
│ Feed RecyclerView           │
└──────────────┬──────────────┘
               ↓
        ChannelAdapter
               ↓
       OnChannelClick
               ↓
       DiscoverFragment
               ↓
        ViewModel(以后)
               ↓
         Repository
          ↙       ↘
      Network     Cache

你会发现,很多 Android 的"高级知识",其实都是在解决最开始这个简单问题:数据到底应该放在哪里、怎么流动、谁负责什么。


二十四、总结

这次我们实现的功能可以浓缩成一句话:

用户点击频道后,通过 Adapter 回调把点击位置传递给 Fragment,Fragment 根据频道找到对应的 Feed 数据,再让 FeedAdapter 更新数据并通知 RecyclerView 刷新。

核心数据流:

text 复制代码
点击频道
   ↓
position
   ↓
OnChannelClickListener
   ↓
DiscoverFragment
   ↓
频道 → FeedList
   ↓
FeedAdapter.setFeedList()
   ↓
notifyDataSetChanged()
   ↓
RecyclerView

而这套思路其实不只适用于"小红书频道"。

同样的模式还可以用于:

text 复制代码
新闻 App
├── 国内
├── 国际
├── 科技
└── 体育

电商 App
├── 推荐
├── 数码
├── 服装
└── 食品

音乐 App
├── 推荐
├── 华语
├── 欧美
└── 日语

本质都是:

text 复制代码
用户选择
   ↓
状态改变
   ↓
找到对应数据
   ↓
Adapter 更新
   ↓
UI 更新

这就是 RecyclerView 从"会用"到"真正开始用于实际项目"的一个很好的分水岭。

相关推荐
Evand J36 分钟前
【MATLAB例程】三维快速扩展随机树(RRT)路径规划与到达角(AOA)、到达时间差(TDOA)融合定位,附下载链接
开发语言·matlab·代码·定位·例程
海盗123440 分钟前
微软技术日报 2026-10-02:微软首发流式转录模型,Win11 26H2 悄悄省内存
人工智能·microsoft·机器人·aigc
开开心心就好40 分钟前
图片白底怎么去掉?抠图工具抠完背景透明
java·服务器·开发语言·pdf·ocr·散列表·启发式算法
朝朝辞暮i42 分钟前
C++ 第 22 课:const —— 不允许随便修改的数据
开发语言·c++·算法
web打印社区1 小时前
JS 静默打印怎么做:纯前端为什么不行,以及最小可跑通写法
开发语言·前端·javascript·websocket·网络协议·http·pdf
大圣编蚕1 小时前
C语言怎么写小游戏?从零开始做一个贪吃蛇
c语言·开发语言
新鲜势力呀1 小时前
PHP 定时数据同步实战:从第三方接口超时到异步同步 + 增量更新架构优化全过程
android
jnrjian1 小时前
Connect Microsoft Tools to Oracle Databases powerbi 之类的oracle 客户端
数据库·microsoft·oracle
Chen—LSN1 小时前
数据结构——拿捏复杂度
java·c语言·开发语言·数据结构·经验分享·笔记·算法