ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Android客户端模块设计:SQLite持久化与RecyclerView列表实践

Android客户端模块设计:SQLite持久化与RecyclerView列表实践 简介面向Android毕业设计学习者基于Android的网络学习资源整合平台源码项目实现了从用户注册登录、课程视频推荐、网络书籍推荐、课程书籍收藏到学习交流、我的收藏、我的发布以及个人信息管理的一整套流程。资源包压缩后73.44MB共含2011个文件其中xml布局/配置文件1602个、Java逻辑类207个、json数据文件125个另有jsp/html页面、sql数据库脚本、css样式、js脚本及docx安装说明文档结构清晰便于定位功能模块可直接导入开发工具运行调试。该项目已有104人学习下载压缩包内附有开发环境搭建说明文档便于快速搭建环境。通过这套源码可以学习Android项目分层结构、网络请求与数据解析、列表绑定、SharedPreferences/数据库存储、评论交互等关键技术点并基于现有功能快速扩展适合课程设计、毕业设计参考及二次开发。1. 从一门在线课程App反推Android客户端的模块边界做一个“网络学习资源整合平台”的Android端最容易被低估的不是某个列表页而是模块之间的数据关系。这个项目把课程视频、网络书籍、收藏夹和学习交流四类功能塞进同一个App意味着用户状态、资源推荐和互动数据必须共享同一套本地存储。我拿到源码包时发现资源里不仅有Android工程还带了AS软件下载和安装说明、后台编辑器的CSS文件tinymce.css、select.css等可以理解为前后端分离的简化版本。如果你正打算用Android Studio从头搭一个能跑通的毕设或者想看看通用学习类App的模块怎么设计这个项目能帮你把登录注册、RecyclerView列表、SQLite持久化、评论收藏这几条线串起来。相比只做课表或记账的Demo这种整合型项目更接近真实产品的信息架构因为每个页面背后都有明确的表结构支撑。下面我从用户体系、资源推荐、互动模块到个人中心一层层拆实现细节。2. 用户体系与本地持久化注册登录、会话保持和密码安全2.1 用户表的设计不要只存用户名和密码新手设计用户表最容易出现两个问题一是只有username和password二是不给用户名加唯一约束。真实项目里用户档案、头像、注册时间都是必需的否则后面做“学习交流”模块时评论列表连昵称都没法展示。我一般会把user表设计成下面这个样子CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, nickname TEXT, avatar TEXT, create_time TIMESTAMP DEFAULT (datetime(now, localtime)) );id作为主键自增username加UNIQUE约束注册时用ContentValues插入就会因为约束冲突直接返回-1不需要先查一遍再决定是否注册。password字段保存加密后的摘要create_time让SQLite自己生成本地时间避免在Java层拼字符串出格式问题。nickname和avatar空着不影响登录但会影响评论模块的信息展示建议注册时就提示用户填写。2.2 密码加密与注册逻辑Android默认没有内置BCrypt毕设阶段用SHA-256已经能避免明文落库。现在不太推荐MD5碰撞成本太低网上还有现成彩虹表可查。下面这个工具类放在util包里注册和登录都能复用public static String sha256(String input) { try { MessageDigest md MessageDigest.getInstance(SHA-256); byte[] digest md.digest(input.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : digest) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return ; } }参数input是用户输入的原始密码返回64位十六进制摘要。注册时执行String hashed sha256(password);再把username、hashed、nickname写入数据库。登录页拿到输入后做相同的sha256运算再和库里比对整个过程不要直接传明文给SQL。字符串拼接哈希值时记得用String.format(%02x, b)否则字节小于16时高位会丢。加密方式选型可以参考下面的表格实际要看后端能力而本地单机App用SHA-256足够撑起这个项目方式是否加盐适用场景备注MD5否已淘汰查表可逆不建议SHA-256否本地单机App无彩虹表风险但不够强SHA-256 盐是有后端的联调场景盐值需要一并存储BCrypt是生产环境Android需引入第三方库2.3 登录状态的保存SharedPreferences还是Token登录成功之后App需要记住当前是哪个用户。最简单的方式是把userId和username写进SharedPreferences所有需要用户身份的操作都从这里取SharedPreferences sp getSharedPreferences(session, Context.MODE_PRIVATE); sp.edit() .putLong(userId, user.getId()) .putString(username, user.getUsername()) .putString(nickname, user.getNickname()) .apply();apply()会先更新内存对象再异步写磁盘性能比commit()好适合这种一次性保存。注意这里绝不能存password也不建议存完整的User对象序列化结果因为数据一旦被拿到会话就完全暴露了。后续如果要接后端可以改用Token登录接口返回一个token字符串存在SharedPreferences里每次网络请求带上。本地数据库仍然保留userId用于查询收藏、帖子等私有数据。2.4 数据库升级与登录状态检查SQLiteOpenHelper里常见的错误是只在onCreate里建表一旦要加字段就卸载重装。正确做法是在onUpgrade里按版本号迁移Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion 2) { db.execSQL(ALTER TABLE user ADD COLUMN avatar TEXT); } }这里用“ALTER TABLE添加字段”演示增量迁移而不是drop table因为收藏表、帖子表里已经可能有用户数据重建会丢用户行为记录。登录状态检查可以写一个BaseActivity在onResume里读取SharedPreferences如果userId 0就跳转LoginActivity。新页面只需要继承BaseActivity不需要复制登录判断逻辑。3. 资源推荐模块的数据模型与列表加载3.1 视频和书籍的字段模型课程视频推荐和网络书籍推荐是首页的核心入口。虽然都叫“推荐”但字段差别很大视频需要记录时长、讲师、播放地址书籍需要作者、ISBN、出版社。我建议分成两张表而不是用一张resource表加type字段硬扛否则视频没有isbn、书籍没有duration会留下一堆空列。下面是对照表字段视频表(video)书籍表(book)id主键主键title课程名称书名cover封面URL封面URLauthor/teacherteacherauthorextraduration分钟isbn、publishersummary内容简介内容简介create_time推荐时间推荐时间在Java里定义两个实体类更清晰视频的示例public class VideoItem { private int id; private String title; private String teacher; private String cover; private String videoUrl; private String summary; public String getTeacherName() { return (teacher null) ? 未知讲师 : teacher; } }多一个getTeacherName()是为了在列表绑定数据时做空值兜底避免TextView直接显示“null”。书籍实体类同理字段换成author和isbn就好了。实际拉取JSON时Gson会根据字段名自动匹配如果字段名不一致需要加SerializedName注解。3.2 列表加载RecyclerView Adapter展示推荐列表的标准方案是RecyclerView加Adapter。下面这段代码同时支持视频和书籍通过item对象内部类型区分点击行为public class ResourceAdapter extends RecyclerView.AdapterResourceAdapter.ViewHolder { private ListResourceItem items; private OnResourceClickListener listener; Override public void onBindViewHolder(NonNull ViewHolder holder, int position) { ResourceItem item items.get(position); holder.title.setText(item.getTitle()); holder.summary.setText(item.getSummary()); Glide.with(holder.itemView.getContext()) .load(item.getCoverUrl()) .placeholder(R.drawable.img_placeholder) .into(holder.cover); holder.itemView.setOnClickListener(v - { if (listener ! null) listener.onClick(item); }); } interface OnResourceClickListener { void onClick(ResourceItem item); } }items是当前列表数据源listener由Activity或Fragment传入。Glide负责加载封面图placeholder是加载中的本地占位图coverUrl既可以是网络图片地址也可以是项目里的可绘制资源路径。不要自己写Bitmap.decodeStream在弱网场景和复杂列表里很容易OOMGlide已经处理好了采样率和缓存。3.3 数据源本地JSON模拟接口这个项目的推荐数据来源有两种可能客户端内置数据或后端REST接口。在没有后端可联调的时候我一般把返回结构写成JSON放assets目录再用Gson解析public ListVideoItem loadVideosFromJson(Context context) { String json AssetUtils.readText(context, recommend_video.json); Type type new TypeTokenListVideoItem() {}.getType(); return new Gson().fromJson(json, type); }recommend_video.json是assets下的文件名readText是读取文本的工具方法TypeToken用来保存List 的泛型类型否则Gson反序列化只能得到ArrayList 。之后切后端只要把方法内部换成OkHttp请求并返回同样的List 上层Adapter完全不用改。小经验JSON键名全部用小驼峰能省掉一半注解。3.4 列表复用与刷新时的坑RecyclerView的item复用意味着onBindViewHolder会被反复调用所以不能在绑定数据时做耗时操作比如解压缩图片、读数据库。列表项超过几百个时给RecyclerView加一句recyclerView.setHasFixedSize(true);这个标志告诉系统item尺寸不随内容变化避免重新测量。刷新加载时不要直接对items做add因为如果列表正在滑动外部直接修改ArrayList会造成IndexOutOfBoundsException。常见做法是新生成一个List复制旧数据再追加最后整体赋值ListResourceItem newList new ArrayList(oldList); newList.addAll(loadMoreData); adapter.updateList(newList);如果每次刷新都全量替换滑动位置会被重置。更高级的写法是ListAdapter配DiffUtil自动计算新增项但这个小项目里先保证列表稳定更新就够用了。4. 学习交流、收藏与评论的联动设计4.1 帖子、评论、收藏三张表之间的关系学习交流模块允许用户发布学习心得其他用户可以评论同时视频和书籍可以被收藏。“我的收藏”和“我的发布”都依赖这里的数据完整。如果只建一张帖子表后面评论、收藏就会变成孤立表查询会乱。我直接给出三张表的设计CREATE TABLE post ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, content TEXT NOT NULL, create_time TIMESTAMP DEFAULT (datetime(now, localtime)) ); CREATE TABLE comment ( id INTEGER PRIMARY KEY AUTOINCREMENT, post_id INTEGER NOT NULL, user_id INTEGER NOT NULL, content TEXT NOT NULL, create_time TIMESTAMP DEFAULT (datetime(now, localtime)) ); CREATE TABLE favorite ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, resource_type INTEGER NOT NULL, resource_id INTEGER NOT NULL, create_time TIMESTAMP DEFAULT (datetime(now, localtime)), UNIQUE(user_id, resource_type, resource_id) );post表存学习心得comment表通过post_id关联帖子user_id标识评论者。favorite表用resource_type区分视频1和书籍2resource_id指向对应主键UNIQUE约束保证一个用户对同一资源只能收藏一次。这套关系能直接支撑“我的收藏”和“我的发布”两个页面不需要额外设计中间表。4.2 发布学习心得先检查登录再写库发布心得的第一步不是拼SQL而是检查用户是否登录。未登录直接跳转登录页已登录把userId和content传入发布方法public boolean publishPost(long userId, String content) { if (TextUtils.isEmpty(content.trim())) { Toast.makeText(context, 内容不能为空, Toast.LENGTH_SHORT).show(); return false; } if (content.trim().length() 500) { Toast.makeText(context, 最多500字, Toast.LENGTH_SHORT).show(); return false; } SQLiteDatabase db dbHelper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(user_id, userId); values.put(content, content.trim()); long id db.insert(post, null, values); return id ! -1; }db.insert第二个参数null表示所有字段都通过ContentValues传入不需要指定空列。返回值-1表示插入失败。这里先做空文本和长度校验避免把空白帖写进数据库导致后面“我的发布”列表出现一堆空行。发布成功后要清空EditText并刷新列表否则用户会以为没发出去而重复提交。4.3 收藏切换先查询再决定插入还是删除收藏按钮要做成“点击一次变成已收藏再点取消”。最直观的逻辑是先查favorite表是否存在记录存在则delete不存在则insert。查询部分用rawQuery配合占位符public boolean isFavorite(long userId, int resourceType, long resourceId) { SQLiteDatabase db dbHelper.getReadableDatabase(); Cursor cursor db.rawQuery( SELECT COUNT(*) FROM favorite WHERE user_id? AND resource_type? AND resource_id?, new String[]{String.valueOf(userId), String.valueOf(resourceType), String.valueOf(resourceId)}); boolean exists false; if (cursor.moveToFirst()) { exists cursor.getInt(0) 0; } cursor.close(); return exists; }rawQuery第二参数是字符串数组对应SQL里的三个问号。这里不要用字符串拼接userId否则就是SQL注入。切换收藏时先调isFavorite再调insert或delete这样无论用户点得多快也只会有一个状态。收藏成功与否都需要刷新详情页按钮的UI否则用户会看到旧状态。4.4 评论列表联表查昵称评论页需要显示评论者的昵称而不是id如果不联表就要在Java层拿着userId再查一次user表非常低效。直接在SQL里JOIN一次SELECT c.id, c.content, c.create_time, u.nickname FROM comment c LEFT JOIN user u ON c.user_id u.id WHERE c.post_id ? ORDER BY c.create_time ASC用LEFT JOIN是因为user记录可能被清理掉但评论展示不能崩。查询结果用Cursor读取时字段索引顺序要和SELECT的列顺序一致。评论量起来后要加LIMIT和OFFSET做分页比如在SQL末尾追加LIMIT 10 OFFSET ?把当前页数作为参数传进去。分页参数不能写死否则用户永远只能看到前十条。互动模块的常用操作可以概括为下面这个表方便对照实现操作方法返回说明发布心得publishPost(userId, content)boolean空内容拒绝调用后需刷新列表收藏切换switchFavorite(userId, type, resourceId)void内部先查再插/删加载评论loadComments(postId, page)List分页查询page从1开始删除帖子deletePostWithComments(postId)void事务内删除评论和帖子4.5 联动时的状态同步收藏、发布、评论都会改变个人中心的数据。用户收藏一个视频后回到“我的收藏”页应该立刻看到更新。如果只在收藏时通知刷新那用户返回时列表和详情页状态就可能不一致。我一般会把收藏状态放在ViewModel里用一个MutableLiveDataBoolean观察每次页面onResume时重新查询一次库而不是靠startActivityForResult回传。这样详情页、列表页、收藏页读的是同一个数据库天然保持一致。5. 我的收藏、我的发布与个人信息修改的边界处理5.1 我的收藏同一用户的两类资源怎么合并“我的收藏”要同时展示收藏的视频和书籍不能做成两个分隔的列表。我通常先分别查询再合并成一个List 每个元素保留resourceType字段供图标和点击跳转使用public ListFavoriteItem getMyFavoriteList(long userId) { ListFavoriteItem result new ArrayList(); result.addAll(queryFavoritesByType(userId, 1)); result.addAll(queryFavoritesByType(userId, 2)); return result; }queryFavoritesByType内部把favorite表与对应video/book表做内联查询按收藏时间倒序。合并后建议再统一按createTime排一次序否则视频和书籍各自按时间排整体看起来没有时间概念。5.2 删除发布时一定要级联删除评论“我的发布”页允许删除自己发布的帖子如果只删除post记录comment表里会残留一堆指向不存在帖子的孤儿评论。手动事务能防止只执行一半db.beginTransaction(); try { db.delete(comment, post_id?, new String[]{String.valueOf(postId)}); db.delete(post, id?, new String[]{String.valueOf(postId)}); db.setTransactionSuccessful(); } finally { db.endTransaction(); }endTransaction()在未调用setTransactionSuccessful()时会自动回滚所以finally里只需调用一次。如果你在SQLiteOpenHelper里启用了外键也可以在建表时定义ON DELETE CASCADE但那样所有delete操作都隐式级联不够直观。手动事务的另一个好处是能顺带处理收藏表里与该帖子相关的记录避免出现无法跳转的收藏项。5.3 修改密码先验旧密码再写新摘要个人信息页的修改密码容易犯的错误是直接更新password字段没有校验旧密码。正确顺序是先sha256(oldPassword)查询user表匹配后再更新public boolean changePassword(long userId, String oldPwd, String newPwd) { String oldHash sha256(oldPwd); Cursor cursor db.rawQuery( SELECT COUNT(*) FROM user WHERE id? AND password?, new String[]{String.valueOf(userId), oldHash}); if (cursor.moveToFirst() cursor.getInt(0) 0) { ContentValues values new ContentValues(); values.put(password, sha256(newPwd)); db.update(user, values, id?, new String[]{String.valueOf(userId)}); cursor.close(); return true; } cursor.close(); return false; }验证通过后再写新摘要整个过程没有明文密码出现在日志里。方法返回值和Toast配合能区分“旧密码错误”和“修改成功”两种场景。修改成功后最好把登录态清掉强制用户重新登录避免旧会话继续持有过期的身份信息。5.4 一个建议用Room的LiveData减少手动刷新如果觉得SQLiteOpenHelper写起来繁琐最后可以把这个助手类换成Room。比如收藏表实体上加一个查询返回LiveDataDao public interface FavoriteDao { Query(SELECT * FROM favorite WHERE user_id :userId ORDER BY create_time DESC) LiveDataListFavoriteEntity getFavoritesByUser(long userId); }这个查询一旦返回LiveDataObserve到页面后只要favorite表发生增删改Room就会自动重新查询并通知界面。Android Studio里引入Room之后收藏、发布、评论三张表的增删查代码能缩减一半以上还能顺带解决Cursor忘记close的问题。不过对于这个项目我建议先自己手写一遍SQLite操作理解表之间通过外键如何关联再换Room答辩时被问到“数据库怎么设计”时手写SQL的经历会让你答得更踏实。本文还有配套的精品资源点击获取
返回列表