Android留言板开发实战:从RecyclerView到SQLite的数据链路详解

发布时间:2026/9/2 4:17:40
Android留言板开发实战:从RecyclerView到SQLite的数据链路详解 简介Android留言板小demo是一份面向安卓初学者的实战练习资源以留言的发表、删除与动态刷新为主线覆盖Activity界面交互、EditText与Button控件、SQLite数据库存储、ListView/RecyclerView列表展示以及Adapter数据刷新等核心知识点能让学习者在一个完整小应用中理解Android的开发链路。资源为RAR压缩包大小约6.09MB页面中暂未列出文件数量与类型明细压缩包体量轻巧适合随时练手。目前已有1196人浏览学习适合正在学习Android开发或准备课程设计的学生。通过这个小demo可以熟悉从布局设计、事件监听到数据库读写、列表适配与界面同步更新的完整过程并延伸了解组件生命周期、错误处理与体验优化思路也可为后续扩展登录、留言时间等个性化功能留有基础整体能帮助初学者快速建立Android应用开发的整体认知。1. 项目整体设计与功能定位1.1 为什么是留言板这个切入点带过一些新人和学生之后我越来越觉得留言板就是Android入门的最佳训练场。它足够简单——不需要网络请求、不需要复杂动画、不需要第三方SDK一个Activity加上两三个布局文件就能跑起来。但又足够完整——涉及UI布局、事件监听、数据存储、列表刷新这四个点恰好是几乎所有App的底层骨架。你去看那些教程里的记事本App、待办清单App本质上和留言板是同一个东西用户输入一段文本程序保存下来再通过列表展示出来。学会了这个demo你就等于掌握了输入-存储-展示这条最基本的业务闭环后面做任何CRUD类应用都是在这个基础上加东西。这个demo最适合两类人刚学完Android基础语法、想知道这些东西组合起来能做什么的新手以及想快速熟悉Android Studio操作流程、第一次接触RecyclerView和SQLite的同学。如果你已经能熟练写复杂业务这个demo对你来说太浅了可以直接跳到最后一节看进阶方向。1.2 技术选型不整花活但也不裸写留言板demo的技术选型其实是个很有意思的权衡过程。网上搜出来的代码五花八门有人用ListView有人用RecyclerView有人数据直接存在ArrayList里还有人一上来就上Room。我的建议是这个阶段就用原生三件套——RecyclerViewSQLiteOpenHelperArrayList别引入任何第三方库。原因很简单。第一作为demo目的是打通数据怎么存、怎么取、怎么展示这条链路用第三方库会把这些关键环节包装掉学完还是不知道底层发生了什么。第二Room这类库虽然好用但它的注解处理机制和协程绑定会让初学者同时面对数据库和异步编程两个新概念容易劝退。数据存储方面我选择SQLiteOpenHelper而不是直接存SharedPreferences。SharedPreferences适合存key-value配置不适合存列表数据。虽然这个demo用SharedPreferences也能凑合实现把列表序列化成JSON字符串存进去但那样你就学不到数据库的增删改查了。这是个demo但demo也要有示范意义把它做成一个可以重复练习CRUD四件套的小项目比单纯追求能跑有价值得多。2. 界面搭建先从Layout说起2.1 布局文件的两个核心模块留言板的界面从用户视角看就两个区域顶部是输入区和发送按钮下方是留言列表。对应的布局文件结构也很清晰?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:padding12dp EditText android:idid/et_message android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:hint说点什么吧... android:maxLines3 / Button android:idid/btn_send android:layout_widthwrap_content android:layout_heightwrap_content android:text发送 android:layout_marginStart8dp / /LinearLayout androidx.recyclerview.widget.RecyclerView android:idid/rv_messages android:layout_widthmatch_parent android:layout_heightmatch_parent / /LinearLayout这里面有两个细节值得多说两句。一是EditText用layout_weight1配合layout_width0dp让输入框自动占满剩余空间这样不同屏幕尺寸下按钮都能固定在右侧不会出现按钮被挤出去的情况。二是maxLines3限制了输入框的最大行数否则输入几行字之后输入框会无限长把下方的列表都挤出屏幕。列表项的布局item_message.xml就更简单了一个TextView就够了外面套个带圆角的CardView或者LinearLayout让每条留言在视觉上分开。这里我建议先别急着加头像、时间戳这些视觉元素demo阶段保持最小可用就行。2.2 为什么用RecyclerView而不是ListView很多老教程还在教ListView但我在实际开发中已经很少看到新项目用ListView了。RecyclerView是它的进化版核心变化在于ListView把每个列表项怎么显示写死在适配器里而RecyclerView把它拆成了ViewHolder缓存视图、Adapter绑定数据、LayoutManager决定排列方式三个独立角色。这个拆分对demo来说可能显得有点繁琐——你要多写一个ViewHolder类还要多写一个LayoutManager。但它是Android官方主推的方案也是你后面做任何复杂列表的必经之路。更重要的是RecyclerView内置了视图复用机制滑动大量数据时性能和内存占用都比ListView好一个档次。还有一个实际原因RecyclerView自带头部底部添加动画、条目删除动画的基础支持后面如果你想给留言板加删除某条留言的功能只需要调用notifyItemRemoved()方法就能看到平滑的动画效果用ListView就得自己写动画非常痛苦。3. 核心代码实现从数据层到界面层3.1 数据层SQLiteOpenHelper的正确写法先建一个数据库帮助类这是整个demo数据流的地基public class MessageDbHelper extends SQLiteOpenHelper { private static final String DB_NAME message_board.db; private static final int DB_VERSION 1; public static final String TABLE_MESSAGE message; public static final String COLUMN_ID _id; public static final String COLUMN_CONTENT content; public static final String COLUMN_CREATED_AT created_at; public MessageDbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { String createTableSql CREATE TABLE TABLE_MESSAGE ( COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT, COLUMN_CONTENT TEXT NOT NULL, COLUMN_CREATED_AT INTEGER NOT NULL ); db.execSQL(createTableSql); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // demo阶段直接删表重建实际项目要写迁移逻辑 db.execSQL(DROP TABLE IF EXISTS TABLE_MESSAGE); onCreate(db); } }建表时_id用INTEGER PRIMARY KEY AUTOINCREMENT这个字段会自动递增不需要你手动赋值。created_at我用的是INTEGER类型存时间戳而不是用TEXT存2024-01-01 12:30:00这种字符串。原因在于时间戳是数字比较大小、排序比如按时间倒序展示都很高效而字符串日期要处理格式化和时区问题对demo来说完全是多余的复杂度。有一件事必须提前说onUpgrade里直接删表重建在真实项目里是大忌。用户升级App后数据会全部清空这在生产环境是不可接受的。但作为demo这个写法能帮你快速迭代表结构所以我保留它但你在自己的项目里一定要改成真正的ALTER TABLE迁移逻辑。3.2 DAO层把增删改查封装好接下来写数据访问对象把数据库操作封起来MainActivity里不直接碰SQLiteDatabasepublic class MessageDao { private final MessageDbHelper dbHelper; public MessageDao(Context context) { dbHelper new MessageDbHelper(context.getApplicationContext()); } public long insert(Message message) { ContentValues values new ContentValues(); values.put(MessageDbHelper.COLUMN_CONTENT, message.getContent()); values.put(MessageDbHelper.COLUMN_CREATED_AT, message.getCreatedAt()); SQLiteDatabase db dbHelper.getWritableDatabase(); long rowId db.insert(MessageDbHelper.TABLE_MESSAGE, null, values); db.close(); return rowId; } public ListMessage queryAll() { ListMessage messages new ArrayList(); SQLiteDatabase db dbHelper.getReadableDatabase(); Cursor cursor db.query( MessageDbHelper.TABLE_MESSAGE, null, null, null, null, null, MessageDbHelper.COLUMN_CREATED_AT DESC ); while (cursor.moveToNext()) { int id cursor.getInt(cursor.getColumnIndexOrThrow(MessageDbHelper.COLUMN_ID)); String content cursor.getString(cursor.getColumnIndexOrThrow(MessageDbHelper.COLUMN_CONTENT)); long createdAt cursor.getLong(cursor.getColumnIndexOrThrow(MessageDbHelper.COLUMN_CREATED_AT)); messages.add(new Message(id, content, createdAt)); } cursor.close(); db.close(); return messages; } }这里query方法用COLUMN_CREATED_AT DESC做倒序排序新留言出现在顶部和微信朋友圈的展示逻辑一致。注意cursor用完记得close()SQLiteDatabase也要close()不然长时间运行会泄漏连接。虽然现在的JDBC和SQLite实现大多会在GC时回收但养成手动关闭的习惯以后做真实的App能少很多问题。Message这个实体类也很简单三个字段id、content、createdAt加getter/setter就够了不需要额外逻辑。3.3 界面层RecyclerView.Adapter的关键实现public class MessageAdapter extends RecyclerView.AdapterMessageAdapter.MessageViewHolder { private final ListMessage messages; public MessageAdapter(ListMessage messages) { this.messages messages; } NonNull Override public MessageViewHolder onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_message, parent, false); return new MessageViewHolder(view); } Override public void onBindViewHolder(NonNull MessageViewHolder holder, int position) { Message message messages.get(position); holder.tvContent.setText(message.getContent()); } Override public int getItemCount() { return messages.size(); } static class MessageViewHolder extends RecyclerView.ViewHolder { TextView tvContent; MessageViewHolder(View itemView) { super(itemView); tvContent itemView.findViewById(R.id.tv_content); } } }关于onCreateViewHolder和onBindViewHolder的区别我经常用一个类比来解释onCreateViewHolder是厨师备菜——把空的餐具View准备好onBindViewHolder是上菜——把对应的菜数据放到这个餐具里。RecyclerView会创建足够多的餐具并循环使用滑动时只在onBindViewHolder里更新数据这样性能才会高。LayoutInflater.from(parent.getContext()).inflate(R.layout.item_message, parent, false)这个第三个参数false很多新手会问为什么。它的含义是不要把这个布局直接添加到parent上因为RecyclerView会自己控制条目视图的挂载时机如果你传入true条目会被提前加到RecyclerView上还会导致布局显示异常。这个坑我见过很多次先写在这里帮你避了。3.4 MainActivity把一切串起来最后是主界面负责初始化视图、读取数据、设置适配器public class MainActivity extends AppCompatActivity { private MessageDao messageDao; private MessageAdapter adapter; private ListMessage messages; private EditText etMessage; private RecyclerView rvMessages; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); etMessage findViewById(R.id.et_message); rvMessages findViewById(R.id.rv_messages); Button btnSend findViewById(R.id.btn_send); messageDao new MessageDao(this); messages new ArrayList(); loadMessages(); RecyclerView.LayoutManager layoutManager new LinearLayoutManager(this); rvMessages.setLayoutManager(layoutManager); adapter new MessageAdapter(messages); rvMessages.setAdapter(adapter); btnSend.setOnClickListener(v - saveMessage()); } private void loadMessages() { ListMessage allMessages messageDao.queryAll(); messages.clear(); messages.addAll(allMessages); } private void saveMessage() { String content etMessage.getText().toString().trim(); if (content.isEmpty()) { Toast.makeText(this, 留言内容不能为空, Toast.LENGTH_SHORT).show(); return; } Message message new Message(0, content, System.currentTimeMillis()); messageDao.insert(message); etMessage.setText(); loadMessages(); adapter.notifyDataSetChanged(); rvMessages.scrollToPosition(0); } }这段代码里有两个细节。一是loadMessages里先clear()再addAll()复用同一个List对象保证Adapter持有的引用还能指向最新数据不需要重新new一个Adapter。二是发送成功后的scrollToPosition(0)配合倒序排列让列表自动滚到最新一条用户体验会好很多。关于notifyDataSetChanged()它是粗暴的整表刷新demo里完全够用。但如果你后面做了删除单条的功能更优雅的方式是notifyItemRemoved(position)notifyItemRangeChanged(position, getItemCount())这样RecyclerView只会刷新变动区域还有动画过渡效果。这就是我之前说的用RecyclerView才能享受这些便利。4. 从demo到工程那些坑我先替你踩了4.1 子线程操作UI新手第一大坑很多人在写留言板的时候会顺手加个从网络加载留言的模拟操作——在Thread里sleep(2)然后直接更新UI。跑起来往往会闪退或者界面不刷新日志里报CalledFromWrongThreadException。这不是代码语法问题而是Android的单线程模型约束UI只能在主线程也叫UI线程操作。子线程里做完耗时操作后必须通过runOnUiThread()、Handler或者View.post()把UI更新逻辑切回主线程。这个demo虽然所有操作都在主线程跑没有问题但我强烈建议你把这个意识提前建立起来后面做网络请求、图片加载这些真耗时操作时它就是必修课而不是可选项。4.2 EditText输入法遮挡列表模拟器里测试的时候点击输入框弹出软键盘列表底部会被键盘挡住。这个问题在真机上更明显不同厂商的ROM对键盘和Activity的交互处理还不一样。最省事的解决方案是在AndroidManifest.xml的activity标签上加android:windowSoftInputModeadjustResize让Activity在键盘弹出时自动压缩高度。如果你希望键盘弹出时输入框自动上移、列表不被遮住就要注意布局的根View能否响应adjustResize的行为。在实际测试中LinearLayout纵向布局配合这个参数基本都能正常工作但如果你用了全屏主题adjustResize就经常失效需要额外处理。这个内容超出demo范围了先记住一点默认主题下加adjustResize是最省事且最稳妥的做法。4.3 空数据的UI反馈demo做完之后很自然的一个体验问题是如果数据库是空的列表区域就是一片空白。用户打开App看到的是一个空壳子会下意识以为App坏了。处理方式很简单在布局里加一个空视图提示TextView android:idid/tv_empty android:layout_widthmatch_parent android:layout_heightmatch_parent android:text还没有留言来说第一句吧 android:gravitycenter android:visibilitygone /然后在loadMessages()里根据数据条数切换可见性private void loadMessages() { ListMessage allMessages messageDao.queryAll(); messages.clear(); messages.addAll(allMessages); tvEmpty.setVisibility(messages.isEmpty() ? View.VISIBLE : View.GONE); adapter.notifyDataSetChanged(); }这是很基础的空状态处理可能有人认为demo不用管这些。但我想说的是把空状态、加载中、失败这三种界面反馈考虑到位是区分能跑的demo和能给人看的demo的分水岭。4.4 数据丢失问题内存存储和持久化存储如果你看过别人写的简化版留言板很可能是用ArrayList直接存数据、App一关就没了。这就是内存存储和持久化存储的区别。这个demo用SQLite来存就是为了避免关掉App数据消失这个尴尬局面。但SQLite也不是万能的它只是一个本地存储方案。如果你的留言板还有多用户数据同步的需求那就需要引入后端和网络层这已经超出demo的范畴了。做项目的时候要搞清楚这个数据是设备本地的还是需要跨设备同步的这个决策会直接影响你的存储选型和整体架构。5. 进阶方向demo之后往哪里走5.1 转向MVVM架构当前demo的三层结构UI层、数据层、逻辑层是MVP的思路Activity既承担视图展示又承担业务逻辑。它的问题在于Activity会越来越臃肿测试困难而且配置变更比如旋转屏幕会导致Activity重建、临时数据丢失。下一步你可以把项目迁移到MVVMLiveData管理数据变化、ViewModel在配置变更时保留数据、Repository模式统一数据来源。迁移过程会很痛苦但做一遍之后你对架构模式到底解决了什么问题的理解比看十篇博客都有用。具体来说MessageViewModel会暴露一个MutableLiveDataListMessageMainActivity观察它并刷新Adapter数据的增删改查全部收拢到ViewModel中。实测这套架构做留言板大概多写100行代码但代码的可读性和维护性会明显提升。5.2 使用Room替代SQLiteOpenHelper如果你已经习惯了MVVM下一步就是用Room替换掉手写的SQLiteOpenHelper。Room是官方在SQLiteOpenHelper之上封装的ORM库编译时会帮你校验SQL语句是否正确还有丰富的注解支持。迁移过去只需要定义Entity、Dao和Database三个点数据源切换的改动量控制在20行以内。不过我要泼一点冷水不要为了用Room而用Room。如果你还在学数据库基础概念手写SQLite反而能帮你建立SQL长什么样、Cursor是什么、事务怎么开这些底层认知。先用好SQLiteOpenHelper等你能独立完成增删改查和联表查询了再切换到Room那时你会觉得Room是如虎添翼而不是雪中送炭。5.3 功能扩展的方向判断留言板这个demo天然是一个可以无限扩展的小项目我给你的建议是给每条留言增加删除功能滑动删除或长按弹菜单练notifyItemRemoved和动画增加编辑功能练Dialog和Adapter的数据更新增加简单的关键词搜索练SQLite的WHERE LIKE语句增加标签或分类练Spinner和RecyclerView的多类型条目每次扩展一个小功能就足够你练手一周了。当你把这些都做完你已经不是只会写demo的水平了你可以开始接触真实的项目代码或者尝试发布一个属于你自己的小应用。我在写这个demo、带着身边人做这个demo的过程中最深的体会是很多人学Android是被配置环境和选框架卡住的而不是被Android本身卡住的。如果你现在还在纠结要不要用一个更高级的框架来写这个留言板我的建议是直接上手写写完再重构。框架选型的优劣要到一定的项目复杂度之后才能体现在留言板这个体量上先动手比什么都重要。本文还有配套的精品资源点击获取