尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Android记事本源码实战:SQLite+RecyclerView+Intent完整实现

Android记事本源码实战:SQLite+RecyclerView+Intent完整实现 简介这是一套基于 Android Studio 开发的安卓记事本 App 完整项目资源面向安卓入门学习者、课程设计或毕业设计需求者帮助其快速理解一个具备登录注册、记事增删改查与本地存储的完整应用实现。压缩包共 55 个文件约 4.39MB以 12 个 Java 源文件、16 个 XML 布局、12 张 png 图片及 gradle 构建脚本为主另含 apk 安装包、演示视频、说明文档与运行环境说明覆盖从源码到可运行成品的完整链路。功能上包含登录、注册、记事本列表、添加与修改记事并通过 SQLite 数据库存储记录时间适合对照学习 Activity 跳转、列表适配与数据库操作等核心知识点。目前已有 9309 人学习下载读者可借助源码、录屏与文档快速跑通项目理解安卓本地数据持久化与界面布局的配合方式也可在此基础上二次开发或作为课程作业参考。1. 从一份能跑起来的 Android 记事本源码说起Android Studio 里新建一个 Empty Activity 模板到能真正跑出一个能增删改查、能持久化保存的记事本 app中间隔着的不是几百行代码而是一整套关于 SQLite 建表、RecyclerView 适配器、Activity 跳转传值的工程习惯。这份基于 Android Studio 开发的安卓记事本 app 源码包解决的就是这个断层——它不是教你写 Hello World而是给出一套结构完整、能直接编译安装的记事本实现适合刚学完 Java 基础、想拿一个真实项目练手的安卓开发新手也适合需要快速搭一个本地笔记原型的老手拿来改。我拆过不少学生交上来的课程设计最常见的翻车不是功能写不出来而是数据存不住、列表刷新不及时、编辑和新建共用一个 Activity 时传参混乱。这份源码的价值就在于它把这几件事都落地了SQLite 做本地存储RecyclerView 做列表展示Intent 带 id 在列表页和编辑页之间来回传值。下面我按「先看懂结构、再动手改、最后避坑」的顺序把它拆成能照着复现的步骤。2. 工程结构与 SQLite 数据层先搞懂数据存哪、怎么读2.1 目录结构和技术栈定位拿到源码包第一件事不是急着点 Run而是先看目录。一个标准的记事本工程app/src/main/java/包名/下面通常会有这么几类文件MainActivity列表页、EditActivity新建/编辑页、NoteDbHelper继承 SQLiteOpenHelper、NoteDao增删改查封装、NoteAdapterRecyclerView 适配器、Note实体类。res/layout/下对应activity_main.xml、activity_edit.xml、item_note.xml三个布局。技术栈上这份源码走的是原生路线Java SQLite RecyclerView Intent。没有引入 Room、没有用 Jetpack Compose这对新手反而是好事——所有逻辑都摊在明面上你能看到 SQL 语句长什么样能看到 Cursor 怎么一行行读出来。常见做法是新手先用原生 SQLite 把数据流跑通等理解了「建表 → 插入 → 查询 → 游标遍历」这条链路再迁移到 Room 会顺很多。提示如果你的 Android Studio 版本较新Gradle 插件版本可能和源码里的不一致导入后先让它自动同步一次报错再手动改build.gradle里的compileSdk和targetSdk。2.2 建表语句与 DbHelper 的写法数据层的核心是NoteDbHelper。它继承SQLiteOpenHelper在onCreate里建表在onUpgrade里处理版本升级。下面是我一般会用的建表写法字段设计上留了create_time和update_time方便列表按时间倒序排public class NoteDbHelper extends SQLiteOpenHelper { private static final String DB_NAME note.db; private static final int DB_VERSION 1; public static final String TABLE_NOTE note; public NoteDbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { // 主键自增标题和内容允许为空时间字段用于排序 String sql CREATE TABLE TABLE_NOTE ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, content TEXT, create_time INTEGER, update_time INTEGER); db.execSQL(sql); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 简单粗暴版本变了就重建正式项目里要写迁移逻辑 db.execSQL(DROP TABLE IF EXISTS TABLE_NOTE); onCreate(db); } }逻辑说明onCreate只在数据库第一次创建时调用所以建表语句写在这里。AUTOINCREMENT保证 id 不重复create_time和update_time用INTEGER存时间戳比存字符串好排序。onUpgrade里直接 DROP 再重建是课程设计里最常见的偷懒写法代价是升级会丢数据——正式项目里得改成ALTER TABLE加字段。参数说明DB_VERSION从 1 开始你改了表结构就把它加 1否则onUpgrade不会触发。TABLE_NOTE抽成常量避免在 Dao 里手写表名字符串写错。2.3 Dao 层的增删改查封装有了 Helper接下来把增删改查封到一个NoteDao里Activity 只调方法不直接碰 Cursor。这样列表页和编辑页都能复用同一套数据操作public class NoteDao { private final NoteDbHelper helper; public NoteDao(Context context) { helper new NoteDbHelper(context); } // 新增返回新插入行的 id public long insert(Note note) { SQLiteDatabase db helper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(title, note.getTitle()); values.put(content, note.getContent()); values.put(create_time, System.currentTimeMillis()); values.put(update_time, System.currentTimeMillis()); long id db.insert(NoteDbHelper.TABLE_NOTE, null, values); db.close(); return id; } // 查询全部按更新时间倒序 public ListNote queryAll() { ListNote list new ArrayList(); SQLiteDatabase db helper.getReadableDatabase(); Cursor cursor db.query(NoteDbHelper.TABLE_NOTE, null, null, null, null, null, update_time DESC); while (cursor.moveToNext()) { Note note new Note(); note.setId(cursor.getInt(cursor.getColumnIndexOrThrow(id))); note.setTitle(cursor.getString(cursor.getColumnIndexOrThrow(title))); note.setContent(cursor.getString(cursor.getColumnIndexOrThrow(content))); note.setUpdateTime(cursor.getLong(cursor.getColumnIndexOrThrow(update_time))); list.add(note); } cursor.close(); db.close(); return list; } }逻辑说明insert用ContentValues组装数据比拼 SQL 字符串安全能防注入。queryAll里db.query的最后一个参数是排序update_time DESC让最新编辑的笔记排最前。getColumnIndexOrThrow比getColumnIndex好列名写错会直接抛异常而不是悄悄返回 -1。参数说明db.insert第二个参数是nullColumnHack当values为空时用它指定一个允许为空的列一般传null就行。cursor.moveToNext()返回 false 表示遍历结束别写成while(cursor.moveToNext())之外的死循环。3. 列表页与编辑页RecyclerView 适配器和 Intent 传值怎么接3.1 RecyclerView 适配器的三个必写方法列表页用 RecyclerView适配器是绕不开的。一个能用的NoteAdapter必须实现onCreateViewHolder、onBindViewHolder、getItemCount三个方法再加一个点击回调public class NoteAdapter extends RecyclerView.AdapterNoteAdapter.VH { private ListNote data new ArrayList(); private OnItemClickListener listener; public interface OnItemClickListener { void onClick(Note note); } public void setOnItemClickListener(OnItemClickListener l) { this.listener l; } public void setData(ListNote list) { this.data list; notifyDataSetChanged(); // 简单场景够用数据量大时用 DiffUtil } NonNull Override public VH onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View v LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_note, parent, false); return new VH(v); } Override public void onBindViewHolder(NonNull VH holder, int position) { Note note data.get(position); holder.tvTitle.setText(note.getTitle()); holder.tvContent.setText(note.getContent()); holder.itemView.setOnClickListener(v - { if (listener ! null) listener.onClick(note); }); } Override public int getItemCount() { return data.size(); } static class VH extends RecyclerView.ViewHolder { TextView tvTitle, tvContent; VH(NonNull View itemView) { super(itemView); tvTitle itemView.findViewById(R.id.tv_title); tvContent itemView.findViewById(R.id.tv_content); } } }逻辑说明onCreateViewHolder负责把item_note.xml渲染成 ViewonBindViewHolder负责把数据填进去getItemCount告诉 RecyclerView 有多少条。点击事件通过接口回调抛给 Activity适配器本身不处理跳转职责分离。参数说明notifyDataSetChanged()会刷新整个列表数据量小无所谓上百条时会有卡顿常见做法是换成DiffUtil。parent和false这两个参数在inflate里不能省否则 item 的宽高会失效。3.2 列表页加载数据与跳转编辑MainActivity的职责很清晰onResume里查一次数据库把结果丢给适配器点击 item 时带 id 跳到编辑页public class MainActivity extends AppCompatActivity { private NoteDao dao; private NoteAdapter adapter; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); dao new NoteDao(this); RecyclerView rv findViewById(R.id.rv_note); rv.setLayoutManager(new LinearLayoutManager(this)); adapter new NoteAdapter(); rv.setAdapter(adapter); adapter.setOnItemClickListener(note - { Intent intent new Intent(this, EditActivity.class); intent.putExtra(note_id, note.getId()); startActivity(intent); }); findViewById(R.id.btn_add).setOnClickListener(v - startActivity(new Intent(this, EditActivity.class))); } Override protected void onResume() { super.onResume(); // 从编辑页返回时重新查保证列表是最新的 adapter.setData(dao.queryAll()); } }逻辑说明把查询放在onResume而不是onCreate是因为从编辑页返回时onCreate不会再走列表就不会刷新——这是新手最常踩的坑之一。点击 item 时只传note_id编辑页自己按 id 去查完整数据避免传整个对象序列化。参数说明putExtra(note_id, note.getId())的 key 要和编辑页getIntExtra的 key 一致写错就是默认值 0编辑页会以为你在新建。LinearLayoutManager是竖向列表要横向就传HORIZONTAL。3.3 编辑页区分新建和修改EditActivity要同时处理「新建」和「编辑」两种状态靠的就是 Intent 里有没有note_idpublic class EditActivity extends AppCompatActivity { private NoteDao dao; private int noteId -1; // -1 表示新建 Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_edit); dao new NoteDao(this); EditText etTitle findViewById(R.id.et_title); EditText etContent findViewById(R.id.et_content); noteId getIntent().getIntExtra(note_id, -1); if (noteId ! -1) { // 编辑模式按 id 查出原数据回填 Note note dao.queryById(noteId); if (note ! null) { etTitle.setText(note.getTitle()); etContent.setText(note.getContent()); } } findViewById(R.id.btn_save).setOnClickListener(v - { String title etTitle.getText().toString().trim(); String content etContent.getText().toString().trim(); if (title.isEmpty() content.isEmpty()) { Toast.makeText(this, 内容不能为空, Toast.LENGTH_SHORT).show(); return; } Note note new Note(); note.setTitle(title); note.setContent(content); if (noteId -1) { dao.insert(note); } else { note.setId(noteId); dao.update(note); } finish(); // 返回列表页触发 onResume 刷新 }); } }逻辑说明noteId默认 -1getIntExtra取不到就保持 -1走新建分支。保存后调finish()回到列表页列表页的onResume会自动重新查询数据就刷新了。这个「编辑页只管存列表页只管查」的分工比在编辑页里手动通知列表刷新要干净。参数说明getIntExtra的第二个参数是默认值必须和新建状态的判断值一致。trim()去掉首尾空格避免用户误输入空格导致标题看起来是空的。4. 避坑与排查这几个翻车点我几乎每次都见4.1 列表返回后不刷新现象新建或编辑完返回列表页数据没变要杀掉 app 重进才看得到。 原因查询逻辑写在了onCreate里而onCreate在 Activity 生命周期里只走一次从编辑页返回走的是onResume。 解决把adapter.setData(dao.queryAll())挪到onResume或者用startActivityForResult在回调里刷新。前者更简单推荐。4.2 数据库版本没升导致表结构不生效现象给Note加了新字段建表语句也改了但运行起来查询报「no such column」。 原因DB_VERSION还是 1onCreate不会重新执行旧表结构没变。 解决把DB_VERSION加 1让onUpgrade触发。如果onUpgrade里是 DROP 重建记得提前备份数据或者改成ALTER TABLE note ADD COLUMN xxx。4.3 Cursor 用完没关导致资源泄漏现象反复进出列表页日志里出现「CursorWindowAllocationException」或者 app 越来越卡。 原因db.query返回的 Cursor 没调close()底层窗口没释放。 解决在while循环结束后立刻cursor.close()db.close()也别忘了。更稳的写法是用 try-finally 包住。4.4 Intent 传 id 的 key 写错现象点某条笔记进去编辑页显示的是空白保存后多出一条新笔记。 原因列表页putExtra(note_id, ...)编辑页getIntExtra(id, -1)key 不一致取到默认值 -1走了新建分支。 解决把 key 抽成常量两边引用同一个。这种错误编译器不报只能靠自查。4.5 RecyclerView 没设 LayoutManager现象app 不崩溃但列表一片空白什么都不显示。 原因RecyclerView 必须设置 LayoutManager 才能布局忘了写就什么都不渲染。 解决rv.setLayoutManager(new LinearLayoutManager(this))这行不能漏横向列表换成GridLayoutManager或StaggeredGridLayoutManager。5. 进阶改造从能跑到好用我一般会加这几样源码能跑通只是起点真正拿去当课程设计或者原型还得补几个体验上的短板。第一个是搜索功能在列表页顶部加一个SearchViewonQueryTextChange里按标题模糊查public ListNote search(String keyword) { ListNote list new ArrayList(); SQLiteDatabase db helper.getReadableDatabase(); Cursor cursor db.query(NoteDbHelper.TABLE_NOTE, null, title LIKE ? OR content LIKE ?, new String[]{% keyword %, % keyword %}, null, null, update_time DESC); // 遍历逻辑同 queryAll cursor.close(); db.close(); return list; }LIKE ?配合%keyword%做模糊匹配两个问号分别对应标题和内容。注意%要拼在 Java 字符串里不能写进 SQL 占位符否则会被当成字面量。第二个是长按删除给适配器再加一个OnItemLongClickListener弹AlertDialog确认后调dao.delete(id)再刷新。删除的 SQL 是db.delete(TABLE_NOTE, id ?, new String[]{String.valueOf(id)})同样用占位符防注入。第三个是时间显示数据库存的是时间戳列表里直接显示一串数字很难看。我一般写个工具方法格式化成「今天 14:30」「昨天」「3 天前」这种相对时间用SimpleDateFormat配合Calendar判断即可。改造项涉及文件关键 API搜索MainActivity、NoteDaoSearchView、LIKE 查询长按删除NoteAdapter、NoteDaosetOnLongClickListener、AlertDialog时间格式化工具类、NoteAdapterSimpleDateFormat、Calendar最后说个我自己的习惯每次改完表结构我都会先把DB_VERSION加 1再手动跑一遍「新建 → 编辑 → 返回 → 删除」这条完整链路确认列表每次都刷新。这个动作花不了两分钟但能挡掉八成「数据对不上」的玄学问题。从那以后我每次动数据库相关代码都强制走一遍这条链路希望帮到你。本文还有配套的精品资源点击获取
返回列表