尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android ListView刷新不生效?从Adapter原理到RecyclerView迁移实战

Android ListView刷新不生效?从Adapter原理到RecyclerView迁移实战 开篇从一个真实场景入手吧。我自己手头维护的一个工具类App有一次用户反馈说列表数据刷新后界面没反应重启App才正常。当时我第一反应是SharedPreferences缓存没清干净查了一圈才发现是ListView的Adapter更新姿势不对。这个问题几乎每个写过ListView的Android开发者都会撞上尤其是刚入门的朋友一遇到数据变了但UI不动就一头雾水。其实ListView的数据更新机制并不复杂困住人的往往是对Adapter工作原理、数据源引用关系以及notifyDataSetChanged()调用时机这三个关键点的理解不够透彻。这篇文章我用一个最简单的记事本列表Demo作为贯穿案例从数据刷新为什么不生效讲起到标准刷新方案的完整实现再到高频踩坑点的排查思路、Local Refresh的性能优化技巧最后聊一聊现在新项目里从ListView迁移到RecyclerView的平滑改造思路。全文不是API文档的搬运而是我这些年实际调试中沉淀下来的经验记录代码可以直接抄踩过的坑也都标注了。1. 为什么改了数据界面上却一动不动很多人刚接触ListView时脑子里是这样一个自然逻辑模型我有一个ArrayList里面存了一堆数据我把这个ArrayList绑定给了ListView那么只要我往ArrayList里加数据、删数据ListView就应该跟着变。这个直觉在大多数场景下是错的因为ListView和ArrayList之间不是直接绑定的关系中间隔着一个Adapter。1.1 ListView刷新背后到底发生了什么Adapter在这里扮演的角色可以理解成一个翻译官。ListView本身并不知道你的数据是什么形态它只会在需要渲染某个位置的时候调用Adapter的getView()方法向Adapter索要这个位置应该显示成什么样的View。ListView也不关心你的数据源是ArrayList还是SQLite游标还是网络返回的JSON数组它只认Adapter暴露给它的两个核心方法getCount()告诉ListView一共有多少行决定滚动条的范围和列表总高度。getView()根据position返回一个View决定这一行具体长什么样。所以整个链条是这样的ListView滚动或首次加载时通过Adapter获取数据Adapter再从自己内部持有的数据源集合里读取最后把数据填充进Item布局。当你直接修改了那个ArrayListListView并不知道这件事发生了。只有调用adapter.notifyDataSetChanged()才会触发两个动作强制ListView重新调用getCount()获取新的行数同时重新遍历当前可见区域的每一行调用getView()重建可见Item的UI。我用一个生活化的类比帮你记这个机制Adapter就像餐厅的前台服务员ListView是传菜口你手上的数据集合是后厨的备菜间。你往备菜间里放了一盘新菜传菜口不会自己冒出来这道菜必须由服务员按一下铃、喊一声有新菜了传菜口才会重新核对菜谱、把新菜端出来。notifyDataSetChanged()就是那声铃响。1.2 更新不生效的典型误区我把这几年在StackOverflow、GitHub issues和同事代码Review里见到的ListView刷新不生效案例总结了三大类几乎99%的问题都绕不出这个圈。第一类是最容易自我否定的把数据改了但改动的是另一个对象的引用。很多人会把Adapter初始化和数据更新写在不同的方法里在更新方法里重新new了一个ArrayList或者直接对新创建的集合赋值而Adapter持有的还是旧集合的引用。这种场景下notifyDataSetChanged()不是没调用而是调用了也白调——Adapter拿到的还是那份老数据。第二类是最隐蔽的列表数据源本身没变化。比如直接把网络请求返回的List丢给了全局变量但服务端返回的是同一个对象引用你以为数据变了实际上集合内部的元素一个都没动又或者你在AsyncTask的doInBackground里修改了一个全局List但Adapter持有的其实是另一个副本。这类问题最好的排查方式就是在notifyDataSetChanged()调用前打日志打印getCount()和数据源的大小一对比就知道数据源有没有真的变化。第三类是最容易被忽略的调用refresh的时机不对。比如在Activity的onCreate里先绑定了Adapter之后又在某个异步回调里修改数据并调用notifyDataSetChanged()但异步回调发生时页面已经处于不可见状态或者在UI还没准备好之前就提前执行了刷新逻辑。这种情况虽然少见但真要碰到了会排查很久。1.3 绕不开的数据源一致性要彻底摆脱刷新不生效的魔咒核心原则就一句话Adapter持有什么数据源你就去更新什么数据源并且尽量保证数据源集合对象只有一个。最稳妥的做法是在Activity/Fragment中全局只维护一个List集合所有读操作、写操作、删除操作都基于这个集合完成。// 全局只维护这一个集合杜绝两个集合问题 private ListNote noteList new ArrayList(); private NoteAdapter noteAdapter; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 初始化Adapter时传入的就是全局集合本身 noteAdapter new NoteAdapter(noteList); listView.setAdapter(noteAdapter); } private void addNote(Note note) { // 修改数据只对noteList这个唯一的集合做操作 noteList.add(note); // 通知刷新刷新后ListView会重新走一遍getCount()和getView() noteAdapter.notifyDataSetChanged(); } private void removeNote(int index) { if (index 0 index noteList.size()) { noteList.remove(index); noteAdapter.notifyDataSetChanged(); } }如果你因为业务需求必须在某个时刻整体替换数据源不要新建一个List然后让Adapter重新指向它正确的做法是先把旧集合清空再用addAll()把新数据灌进去最后调用notifyDataSetChanged()。这样Adapter持有的引用永远不变数据内容却能整体替换。private void replaceData(ListNote newData) { noteList.clear(); // 清空旧数据 noteList.addAll(newData); // 灌入新数据 noteAdapter.notifyDataSetChanged(); }我见过有人图省事在数据整体替换时直接写listView.setAdapter(new NoteAdapter(newData))这个写法在功能上通常也能跑通但它会丢失滚动位置、重新触发一轮Item创建和测量布局浪费性能不说还容易造成界面闪烁。更重要的是它绕开了数据源一致性这个原则当ListView和Adapter数量多起来时代码会变得越来越难维护。2. 标准刷新方案的完整实现理论讲清楚了接下来实战。我以一个记事本列表为例完整走一遍初始化Adapter→修改数据→通知刷新的闭环。这个Demo虽然简单但包含了ListView刷新场景的所有标准要素BaseAdapter实现、ViewHolder缓存、多种数据变更操作、以及刷新后的界面反馈。2.1 一套标准的ListView刷新流程开发环境我用的还是传统稳定的Android Studio中控版本对API Level没有强制要求15以上的项目都能跑。核心逻辑不依赖任何第三方库全部基于android.widget.ListView和android.widget.BaseAdapter。实体类就一个Note包含标题、创建时间和内容摘要为了演示效果我加了一个颜色标记字段用来区分不同状态的笔记。public class Note { public String title; public String createTime; public String summary; public int colorTag; public Note(String title, String createTime, String summary, int colorTag) { this.title title; this.createTime createTime; this.summary summary; this.colorTag colorTag; } }Adapter部分是最关键的一环。BaseAdapter的四个方法必须全部实现getCount()返回列表长度getItem()返回指定位置的数据对象getItemId()返回行IDgetView()负责渲染每一个可见行。这里我强烈建议一上来就写ViewHolder模式就算列表只有十几个Item也别省这一步。ViewHolder模式的核心价值不是提升那几毫秒的性能而是把findViewById的调用次数从N次降为可见项数量次让你从源头上规避滚动卡顿和重复findViewById带来的效率浪费。public class NoteAdapter extends BaseAdapter { private ListNote data; public NoteAdapter(ListNote data) { this.data data; } Override public int getCount() { return data null ? 0 : data.size(); } Override public Note getItem(int position) { return data.get(position); } Override public long getItemId(int position) { return position; } Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView null) { convertView LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_note, parent, false); holder new ViewHolder(); holder.titleTv convertView.findViewById(R.id.tv_title); holder.timeTv convertView.findViewById(R.id.tv_time); holder.summaryTv convertView.findViewById(R.id.tv_summary); convertView.setTag(holder); } else { holder (ViewHolder) convertView.getTag(); } Note note getItem(position); holder.titleTv.setText(note.title); holder.timeTv.setText(note.createTime); holder.summaryTv.setText(note.summary); // 模拟一个可见的状态变化默认灰底colorTag为1时高亮 convertView.setBackgroundColor(note.colorTag 1 ? 0xFFE0F7FA : 0xFFFFFFFF); return convertView; } static class ViewHolder { TextView titleTv; TextView timeTv; TextView summaryTv; } }2.2 完整的数据更新场景示例数据更新不只是往列表末尾追加一条实际项目中常见的增删改插入都值得演示一遍。我在MainActivity里放了四个按钮分别对应四类典型场景。每个场景的共同点是先改数据后调notifyDataSetChanged()顺序不能反。如果你先notify再改数据等于给ListView发了个你该刷新了的信号结果数据还是旧的界面读到的自然也是旧数据。public class MainActivity extends AppCompatActivity { private ListNote noteList new ArrayList(); private NoteAdapter noteAdapter; private int serialNumber 0; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView listView findViewById(R.id.list_view); // 初始化三条测试数据 noteList.add(new Note(欢迎使用记事本, 2024-05-01 10:00, 这是一个演示, 1)); noteList.add(new Note(ListView学习笔记, 2024-05-02 09:30, Adapter是核心, 0)); noteList.add(new Note(购物清单, 2024-05-03 20:15, 鸡蛋、牛奶、面包, 0)); noteAdapter new NoteAdapter(noteList); listView.setAdapter(noteAdapter); findViewById(R.id.btn_add).setOnClickListener(v - addNewNote()); findViewById(R.id.btn_remove).setOnClickListener(v - removeFirstNote()); findViewById(R.id.btn_modify).setOnClickListener(v - modifyFirstNote()); findViewById(R.id.btn_insert).setOnClickListener(v - insertAtHeader()); } private void addNewNote() { serialNumber; noteList.add(new Note(新增笔记 serialNumber, 2024-05-10 12:00, 追加在列表末尾, 0)); noteAdapter.notifyDataSetChanged(); // 可选自动滚动到列表底部提升交互体验 } private void removeFirstNote() { if (!noteList.isEmpty()) { noteList.remove(0); noteAdapter.notifyDataSetChanged(); } } private void modifyFirstNote() { if (!noteList.isEmpty()) { Note first noteList.get(0); first.title 被修改的标题; first.colorTag 1; noteAdapter.notifyDataSetChanged(); } } private void insertAtHeader() { serialNumber; noteList.add(0, new Note(置顶笔记 serialNumber, 2024-05-10 12:30, 插入到列表头部, 1)); noteAdapter.notifyDataSetChanged(); } }一个容易被忽略的交互细节当新数据插入在列表头部时ListView的滚动位置会保持原来的索引也就是说用户当前看到的还是原来第0条的位置插进去的新数据被顶到了屏幕上方外面用户感知不到插入成功。这时候最好的做法是调用listView.smoothScrollToPosition(0)把列表平滑地拉回顶部让用户直观地看到置顶效果。同理追加在末尾的数据可以调用listView.setSelection(noteList.size() - 1)直接定位到最后一行。2.3 认识notifyDataSetChanged()的刷新范围必须明确一个容易产生误解的点notifyDataSetChanged()不是一个增量更新接口它的语义是我全部数据都变了请你重新加载。ListView收到通知后会依次对当前可见范围内的每个Item调用getView()。也就是说如果你有1000条数据但一屏只显示8条它不会去刷新1000条只刷新可见的8条加一个缓冲区的几条剩下的等滚动到时才创建和绑定。正因如此大家才会产生notifyDataSetChanged()会卡顿的刻板印象。其实在Item数量几百以内、布局不复杂的场景下全量通知的代价完全可接受。真正不可接受的写法是在getView()里做耗时操作——比如从磁盘读图、执行网络请求、做复杂字符串运算——因为这样无论刷新方式是全量还是局部滚动时每一行的创建都会严重拖慢帧率。正确处理方式是把耗时操作的结果提前缓存到对象字段里getView()只负责取缓存展示。这里再补充一个细节notifyDataSetChanged()最后会触发requestLayout()这意味着即使你只是修改了一个Item里的TextView文字ListView也会重新走一遍测量、布局、绘制流程。这个代价比想象中要大所以在数据量极大又只需要改一行数据的场景下可以考虑局部刷新的方案这部分第四章专门展开。3. 高频问题与排查技巧实录写代码时最怕的就是这个问题有人遇到过但我搜不到答案。这一节把我自己和同行们踩过的高频坑集中整理出来再加一套我自己常用的排查路径希望能帮你少走弯路。3.1 常见问题速查表现象根本原因解决方案数据变了界面完全不动没有调用notifyDataSetChanged()确认所有数据变更操作后都调用刷新数据变了界面完全不动修改的集合不是Adapter持有的集合统一使用同一个List实例清除new ArrayList的坏习惯刷新后列表跳到顶部Activity重建或setAdapter被重新执行记录滚动位置刷新后恢复scrollTo刷新后Item显示内容错乱getView()里复用了convertView但数据绑定不完整确保每个需要显示的字段都set不只是有值时才set点击事件拿到错误的position在getView()里直接用int position使用convertView的getTag()或setTag()绑定positionnotifyDataSetChanged()崩溃数据源为null且getCount()未做空判断getCount()里先判空Adapter不持空引用异步线程里notify崩溃子线程直接操作UI切回主线程再刷新见3.2节Item内容错乱是大家问得最多的一项这里单独展开讲一下。由于ListView的convertView是复用的如果不给每一项的所有控件都设置值就会出现在第5行的数据跑到第8行残留显示的情况。比如你的Note对象可能没有summary字段所以你在getView()里只set了title和时间没set summary那第8行复用的View就是上一个Item留下的summary文本。处理方案简单粗暴每一个TextView、ImageView无论本次数据有没有值都要set一遍没有值就set空串或可见性置为GONE。3.2 线程更新的主线程约束另一个非常经典的错误是在子线程里修改数据后直接调notifyDataSetChanged()。Android的UI框架不是线程安全的任何对View树的操作都必须在主线程执行。你可能会看到列表偶尔能刷新成功偶尔直接抛异常异常日志长这样android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.解决思路有四种按推荐程度从高到低排列。第一种是Handler post()最通用也最好理解。第二种是runOnUiThread()适合Activity内部使用代码更简洁。第三种是AsyncTask的onPostExecute()适合在异步任务结束时顺带刷新。第四种是ProgressDialog配合Handler的经典组合虽然现在多数项目已经转向协程或RxJava但思想是一样的。// 推荐写法子线程里封装数据变更然后切回主线程刷新 new Thread(() - { // 模拟耗时操作比如网络请求或数据库查询 ListNote result loadDataFromNetwork(); // 此时仍在子线程不能直接操作ListView和Adapter runOnUiThread(() - { noteList.clear(); noteList.addAll(result); noteAdapter.notifyDataSetChanged(); }); }).start();这段代码的逻辑核心是把数据获取和UI刷新拆成两段耗时操作留在子线程所有Adapter相关操作集中到runOnUiThread里执行。一旦养成了这个习惯由线程引起的notify崩溃几乎不会再出现。3.3 空列表、空指针和数据同步的坑ListView在数据为空时默认显示一个空白区域这个体验很糟糕。建议用setEmptyView()绑定一个提示布局比如暂无数据点击添加注意empty view必须和ListView在同一个父容器内。listView.setEmptyView(findViewById(R.id.tv_empty));另外还有一个保存Activity状态的老问题。用户旋转屏幕或者App被系统回收后Activity会重建noteList如果没做状态保存会回到初始状态。正确做法是让Note实体实现Serializable或Parcelable在onSaveInstanceState()里保存noteList在onCreate()里恢复。否则你会遇到一个诡异场景addNote()添加了数据界面也确实刷新了但屏幕一转数据全没了。这个现象和刷新机制本身无关但排查的时候容易误判成刷新失效。关于空指针我也提一句。不要在Adapter外面直接拿noteList.get(0)操作而不做isEmpty()判断因为当列表被清空后noteList已经不是null而是size等于0的集合此时get(0)会抛IndexOutOfBoundsException。这条看起来是老生常谈但我在Review代码时看新人犯这类错的频率真的非常高。4. 局部刷新与性能优化进阶notifyDataSetChanged()虽然好用但功能上的局限在于它没有我只改了一行的语义。假如你的列表有500条数据用户点赞了第100条点赞红心必须立刻变化明显的缩放动画还要走一遍这时候全量刷新就会显得笨重不仅浪费了499行的绑定工作动画也会因为整列表的requestLayout而掉帧。因此局部刷新是一项非常值得掌握的优化技能。4.1 用getFirstVisiblePosition做精准局部更新局部刷新的思路是ListView能看到多少行最多就更新这些行看不到的行等滚动到了自然通过getView()绑定新数据。核心API是getFirstVisiblePosition()和getChildAt()。private void updateSingleRow(int targetPosition) { // 第一个可见项的position int firstVisiblePosition listView.getFirstVisiblePosition(); // 最后一个可见项的position int lastVisiblePosition listView.getLastVisiblePosition(); // 目标行在可见范围内才能直接操作它 if (targetPosition firstVisiblePosition targetPosition lastVisiblePosition) { // 计算目标行在屏幕上对应的子View索引 int visibleIndex targetPosition - firstVisiblePosition; View targetView listView.getChildAt(visibleIndex); if (targetView ! null) { // 直接操作这个View更新UI TextView titleTv targetView.findViewById(R.id.tv_title); titleTv.setText(已更新); // 如果整行样式变化较大也可以直接调getView()重新绑定 } } // 如果不在可见范围内什么都不用做滚动到时getView()会自动读取最新数据 // 但前提是数据源集合已经更新 }这里最关键的前提是在调用这个局部刷新方法之前必须先更新数据源集合里的数据否则等用户滚到那一行时getView()读到的是旧数据局部更新就会前功尽弃。我的习惯是先改数据再定位View再改UI三步捆绑在一起。局部刷新也有它的适用边界。如果你的更新逻辑包含了复杂的布局结构变化(比如展开详情、切换多类型布局)那还是老老实实调用notifyDataSetChanged()更靠谱因为局部刷新假设行的结构不会变化只更新内容。自己强行写一套伪局部更新的复杂代码反而比全量刷新更容易出Bug。4.2 列表项内部状态更新的正确姿势实际业务中列表项的更新往往不是整行重绘而是某个控件的状态变化。比如收藏按钮的红心切换、CheckBox的勾选、进度条的百分比更新。对这种场景有一个隐藏很深的坑不要在getView()里根据数据源直接setCheck或setImageResource因为getView()会在任何一次刷新时机被调用包括Item滑出屏幕再滑回来、列表整体重绘等此时如果数据和当前控件状态不一致会引发错乱。一个稳妥的做法是状态先更新到数据源再刷新UI。举例来说在OnClickListener里维护一个Set存放已收藏的positionprivate SetInteger favoritePositions new HashSet(); // 在getView()里绑定收藏状态 holder.favoriteIv.setImageResource( favoritePositions.contains(position) ? R.drawable.ic_faved : R.drawable.ic_unfaved); holder.favoriteIv.setOnClickListener(v - { if (favoritePositions.contains(position)) { favoritePositions.remove(position); } else { favoritePositions.add(position); } // 局部更新避免整个列表重绘 updateSingleRow(position); });这个方案的思想是不要在View里临场计算状态所有状态都收敛到数据源——数据源是唯一的真相来源。如果你只在View层临时改图标数据源没同步下一次getView()一旦执行状态又弹回去了。4.3 大数据量下ListView的极限与突破ListView在几千条数据、复杂Item布局的情况下滚动性能会出现肉眼可见的卡顿这是由它在measure/layout阶段的机制决定的。Adapter的getView()虽然做了ViewHolder缓存但每次载入新行时仍要执行一次完整的LayoutInflater布局创建和measure流程。等到数据量上到几万条或Item里有图片、动画ListView就会显得力不从心。此时有两条路。第一条是优化现有实现把图片加载替换为Glide或Coil的缩略图方案、减少Item布局层级、避免在getView()里做任何new操作。第二条就是彻底迁移到RecyclerView这也是后面第五章的内容。在ListView还存续的前提下还有一个收益很高的技巧不要把所有数据一次性全部塞给Adapter。假设网络接口返回了1000条数据本地可以做一个分批加载初始只显示前50条监听到ListView滚动到底部时再加载下一批。这样getCount()第一次只返回50后续每次追加50getView()被调用的总量大幅下降滚动自然就顺滑了。5. 从ListView平滑迁移到RecyclerView到了2024年新项目里几乎不会有人再直接使用ListView了RecyclerView是Android官方的默认选择。但老项目里大量历史代码仍以ListView为主不少工作室在维护这类存量项目时都会面临一个选择要不要迁移、怎么迁移成本最低。5.1 迁移的核心差异与心理建设RecyclerView和ListView最本质的区别在于ListView把列表布局和数据适配耦合在一起而RecyclerView用LayoutManager完全接管了Item的摆放Adapter专注数据绑定Item布局通过ViewHolder强制规范。这意味着迁移时你不需要重写业务逻辑只需要把BaseAdapter换成RecyclerView.Adapter把getView()换成onCreateViewHolder()和onBindViewHolder()把ListView的setAdapter换成RecyclerView的setAdapter()加setLayoutManager()。逻辑结构几乎一一对应。5.2 RecyclerView的Diff机制带来质的飞跃RecyclerView最大的设计红利是DiffUtil和ListAdapter。这个机制能在进行一次局部刷新时自动计算出数据集合的新旧差异只更新真正变化的那一行而不是全量刷新。这在ListView时代是完全没有的——ListView的notifyDataSetChanged()永远是全量。如果你在迁移时仅仅把ListView换成RecyclerView却仍然沿用notifyDataSetChanged()的调用方式那你只享受到了一半的升级快感。要完整发挥DiffUtil的魅力应该让你的Adapter继承ListAdapter并搭配一个DiffUtil.ItemCallbackpublic class NoteDiffCallback extends DiffUtil.ItemCallbackNote { Override public boolean areItemsTheSame(Note oldItem, Note newItem) { // 判断两个Item是不是同一个对象一般用唯一ID return oldItem.id newItem.id; } Override public boolean areContentsTheSame(Note oldItem, Note newItem) { // 判断同一个Item的内容有没有变化 return oldItem.title.equals(newItem.title) oldItem.summary.equals(newItem.summary) oldItem.colorTag newItem.colorTag; } }ListAdapter内部会自动在后台线程执行diff计算计算完成后在主线程把差异结果交给RecyclerView执行精准的动画更新。这个方案对用户感知的提升是质的飞跃新增、删除、移动、修改都带有流畅的动画不再有整页闪烁的尴尬。5.3 RecyclerView示例与新旧方案对比下面是RecyclerView版本的完整Adapter写法。ViewHolder通过构造函数绑定控件onCreateViewHolder处理布局创建onBindViewHolder负责数据绑定。相比ListView最大的不同是RecyclerView的ViewHolder不再需要手动判断convertView是否为空因为创建和复用完全由RecyclerView的Recycler机制托管开发者只需要关注绑定逻辑。public class NoteRvAdapter extends RecyclerView.AdapterNoteRvAdapter.NoteVH { private ListNote data new ArrayList(); public void submitList(ListNote newData) { data.clear(); data.addAll(newData); notifyDataSetChanged(); } NonNull Override public NoteVH onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_note, parent, false); return new NoteVH(view); } Override public void onBindViewHolder(NonNull NoteVH holder, int position) { Note note data.get(position); holder.titleTv.setText(note.title); holder.timeTv.setText(note.createTime); holder.summaryTv.setText(note.summary); holder.itemView.setBackgroundColor( note.colorTag 1 ? 0xFFE0F7FA : 0xFFFFFFFF); } Override public int getItemCount() { return data.size(); } static class NoteVH extends RecyclerView.ViewHolder { TextView titleTv; TextView timeTv; TextView summaryTv; NoteVH(View itemView) { super(itemView); titleTv itemView.findViewById(R.id.tv_title); timeTv itemView.findViewById(R.id.tv_time); summaryTv itemView.findViewById(R.id.tv_summary); } } }Activity侧的改动也很直观setContentView之后先创建一个LinearLayoutManager再setLayoutManager最后setAdapter。如果你希望纵向滚动LinearLayoutManager默认就是这个方向想要网格效果换成GridLayoutManager即可。RecyclerView recyclerView findViewById(R.id.recycler_view); recyclerView.setLayoutManager(new LinearLayoutManager(this)); NoteRvAdapter adapter new NoteRvAdapter(); recyclerView.setAdapter(adapter);我执行迁移时的习惯是分三步走先把ListView改成RecyclerView跑通基础展示和点击事件再把notifyDataSetChanged()改成submitList()并接入DiffUtil最后再评估是否要引入动画库丰富增删改的视觉效果。这种渐进式改造风险低每一步改动都能单独验证不会出现一改全崩的尴尬局面。5.4 点击事件与跨方案统一的封装思路最后一个建议在写完RecyclerView版本之后不要忘了把点击事件的处理方式统一起来。ListView时代大家习惯用setOnItemClickListenerRecyclerView没有这个接口需要自己在ViewHolder里给itemView设置点击监听。我习惯在Adapter内部定义OnItemClickListener接口这样Activity里的调用方式可以保持高度一致未来再切换其他列表框架时也能快速适配。public interface OnItemClickListener { void onItemClick(int position, Note note); }如果团队里同时维护着ListView和RecyclerView两个版本的页面我建议把实体类和数据访问层抽出来共用UI层各写一套Adapter。不要让业务逻辑混进Adapter里面这样无论用ListView还是RecyclerView替换成本都压缩到最低。我个人在实际开发中的体会是ListView的数据更新问题本质上是事件驱动UI刷新这一思想的训练场。搞清楚数据源引用一致性、主线程约束、View复用规则之后你不但能从容解决ListView的刷新问题理解RecyclerView的DiffUtil、Compose的状态驱动也会轻松很多。希望这篇文章里记录的踩坑经验和代码模板能帮你少走几趟弯路。
返回列表