简介:这是一份面向高校学生的安卓记事本项目源码,适合作为期末大作业、课程设计或安卓课设参考。项目采用原生Android实现,覆盖笔记的增删改查、列表展示、本地存储等完整模块,代码中穿插详细注释,能帮助初学者理解Activity生命周期、Adapter适配器机制、SQLite数据库操作等关键知识点,也便于教师或评审快速看懂整体设计。压缩包共77个文件,体积仅1.59MB,以26个XML配置文件、17个Java源码文件为主体,另附Gradle构建脚本、若干图片资源与README说明,工程结构清晰,导入Android Studio即可编译运行。目前已有207人学习浏览,属于可直接复用或二次扩展的课设范例。下载后简单部署就能用,既可用于期末答辩演示,也能在现有逻辑上增加提醒、搜索、标签分类等功能,省去从零搭建的时间,还方便修改界面主题来匹配个人风格,是时间紧张又希望拿高分的学生值得参考的完整项目。
1. 别小看这个记事本,它是把安卓大作业常见考点串起来的完整工程
拿到这个“安卓期末大作业记事本”的 zip 时,我原以为又是一个只放一个 MainActivity 的应付型源码,解压开才发现是完整的 android-master 工程:gradle wrapper、app/src 分层、proguard-rules.pro、settings.gradle 全都在,连 README 和截图都给你备好了。这是一个非常典型的原生安卓课程设计:数据层用 SQLite 存笔记,列表页用 RecyclerView 渲染,新增和编辑共用同一个 Activity,页面之间通过 Intent 传参和回传数据。对正在找结课大作业参考的安卓初学者来说,这套代码注释完整、模块边界清楚,改个包名就能跑;对已经工作的人而言,拆完一遍也相当于把 Activity 生命周期、SQLite 操作、RecyclerView 适配器这几块最常考的基础重新梳了一遍。
2. 项目结构与数据层:SQLite 表设计先想清楚,后面全是体力活
2.1 解压后先看这几个目录,判断工程是否完整
第一次打开这个项目,不用急着点运行,先对着文件树确认结构。一个能交差的原生安卓大作业,必然包含 app 模块、Gradle Wrapper 和资源目录。这个 zip 里的关键路径整理出来是这样:
android-master/ ├── app/ │ ├── src/main/ │ │ ├── java/com/example/notes/ │ │ │ ├── MainActivity.java # 列表页 │ │ │ ├── EditActivity.java # 新增/编辑页 │ │ │ ├── Note.java # 笔记实体类 │ │ │ ├── NoteAdapter.java # RecyclerView 适配器 │ │ │ └── NoteDbHelper.java # SQLite 帮助类 │ │ ├── res/layout/ │ │ │ ├── activity_main.xml # 列表布局 │ │ │ └── item_note.xml # 单条笔记布局 │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle/wrapper/gradle-wrapper.properties └── build.gradle一眼就能看出来它没有引入第三方库,也不依赖网络请求,完全是用安卓官方组件写出来的。点赞的地方在于NoteDbHelper单独成类,没有把建库写进 MainActivity。数据操作集中管理,这在课程答辩时是可以说一句“我遵循了单一职责”的加分项。
2.2 笔记表字段与 SQLiteOpenHelper 实现
记事本的核心功能再怎么包装,落到数据库层就是一张笔记表。这个项目里NoteDbHelper继承SQLiteOpenHelper,建表语句写在onCreate中:
public class NoteDbHelper extends SQLiteOpenHelper { private static final String DB_NAME = "note_book.db"; private static final int DB_VERSION = 1; public NoteDbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase db) { String sql = "CREATE TABLE notes (" + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + "title TEXT, " + "content TEXT, " + "update_time INTEGER DEFAULT 0)"; db.execSQL(sql); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("DROP TABLE IF EXISTS notes"); onCreate(db); } }DB_NAME是数据库文件名,DB_VERSION是版本号。版本号只在以后需要修改表结构时才起作用,大作业阶段保持 1 就够了。字段类型使用TEXT存标题和正文,用update_time存毫秒级时间戳而不是DATETIME字符串,这样排序和格式化时都更方便。
四张字段表的关系如下:
| 字段 | 类型 | 说明 |
|---|---|---|
_id | INTEGER PRIMARY KEY AUTOINCREMENT | 主键,自增,作为笔记唯一标识 |
title | TEXT | 标题,可以留空 |
content | TEXT | 正文内容 |
update_time | INTEGER | 最近修改时间,单位是毫秒 |
这里有个细节:排序时直接用update_time DESC就能让最近编辑的笔记排在最上面,不需要额外再写比较逻辑。如果用字符串存时间,排序结果可能不符合直觉,这是我在不少课程设计代码里见过的通病。
2.3 为什么用 SQLite 而不是 SharedPreferences 或文件存储
刚开始学安卓时,很多人会考虑用SharedPreferences存一个 JSON 数组,或者用内部存储写一个 txt。拆这个项目时可以看到它选择了 SQLite,这背后的取舍值得讲清楚。
| 存储方式 | 适用场景 | 这个项目里的问题 |
|---|---|---|
SharedPreferences | 保存设置项、少量键值 | 存列表需要序列化整个 JSON,数据多了性能差 |
| 普通文件 | 日志、整块文本输出 | 需要自己处理格式和增删改查 |
| SQLite | 结构化、查询条件多的数据 | 增删改查、排序、后续扩展都方便 |
课程答辩时如果被问到“为什么不用文件存”,可以说:SQLite 提供了 SQL 语句和ContentValues封装,天然支持按条件更新和删除,将来想加搜索功能也只需要一条WHERE查询,文件存储还得自己写匹配逻辑。这个回答已经足够拿捏大多数提问场景。
3. 列表页加载与刷新:RecyclerView + Adapter 写对了,界面就顺了
3.1 MainActivity 中把数据库变成列表的过程
列表页是整个记事本的门面。MainActivity里的标准套路是:先初始化RecyclerView和布局管理器,再绑定自定义Adapter,最后从数据库读取数据填充到集合中。
public class MainActivity extends AppCompatActivity { private RecyclerView recyclerView; private NoteAdapter adapter; private List<Note> noteList = new ArrayList<>(); private NoteDbHelper dbHelper; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); dbHelper = new NoteDbHelper(this); recyclerView = findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); adapter = new NoteAdapter(noteList); recyclerView.setAdapter(adapter); loadNotes(); } private void loadNotes() { noteList.clear(); SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor cursor = db.query("notes", null, null, null, null, null, "update_time DESC"); while (cursor.moveToNext()) { Note note = new Note(); note.setId(cursor.getLong(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"))); noteList.add(note); } cursor.close(); adapter.notifyDataSetChanged(); } }db.query方法有七个参数,分别是表名、要查询的列(null 表示全字段)、条件、条件参数、分组、过滤、排序。这里把排序设为update_time DESC,保证最新修改的笔记出现在顶部。Cursor用完以后必须close(),否则在循环中反复查询容易造成内存占用上涨,这一点在代码审查时经常被问到。
更稳妥的做法是把loadNotes()放到onResume()里而不是onCreate()。原因很简单:从编辑页保存后返回列表页,会重新触发onResume(),数据立刻更新;如果只放在onCreate(),就得依赖onActivityResult回传结果,多一层回调逻辑。
3.2 NoteAdapter 和 ViewHolder 的写法
RecyclerView的适配器是列表页最容易出错的环节。核心要点是让ViewHolder持有每个 item 的子控件引用,避免每次滚动都重新findViewById。
public class NoteAdapter extends RecyclerView.Adapter<NoteAdapter.ViewHolder> { private final List<Note> notes; private OnItemClickListener listener; public NoteAdapter(List<Note> notes) { this.notes = notes; } public interface OnItemClickListener { void onItemClick(int position); } public void setOnItemClickListener(OnItemClickListener listener) { this.listener = listener; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_note, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { Note note = notes.get(position); holder.title.setText(note.getTitle()); holder.content.setText(note.getContent().replace("\n", " ")); holder.time.setText(TimeUtils.formatTime(note.getUpdateTime())); holder.itemView.setOnClickListener(v -> { if (listener != null) listener.onItemClick(position); }); } @Override public int getItemCount() { return notes.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView title, content, time; ViewHolder(View itemView) { super(itemView); title = itemView.findViewById(R.id.tv_title); content = itemView.findViewById(R.id.tv_content); time = itemView.findViewById(R.id.tv_time); } } }这段代码里值得注意的一个点是content.replace("\n", " ")。数据库里的正文可能有多行,但列表 item 的高度有限,直接把换行符展示出来会导致不同条目高低不平,视觉上很乱。替换成空格后,预览时只会显示第一行内容,这是很多新手容易漏掉的细节。
点击事件通过接口回调而不是在适配器里直接启动 Activity,这样适配器不依赖具体页面,以后复用或测试都更方便。lambda 写法需要项目开启 Java 8,如果编译报错,改成匿名内部类即可。
3.3 列表刷新时容易翻车的三处细节
列表刷新方式决定了操作的交互体验。最省事的notifyDataSetChanged()在大作业里够用,但要跟老师解释清楚不同方法的差异:
| 刷新方式 | 效果 | 适用场景 |
|---|---|---|
notifyDataSetChanged() | 整体刷新,无动画,闪烁明显 | 数据量小,逻辑简单 |
notifyItemInserted(position) | 只在指定位置插入一条,有动画 | 新增了笔记 |
notifyItemRemoved(position) | 删除指定位置,带动画 | 删除了笔记 |
这里有一个常见的崩溃场景:用户删除列表最后一条笔记时,notifyItemRemoved之后还在onBindViewHolder里访问原来的位置,导致索引越界。我一般会在删除后先判断列表是否为空,再决定是刷新整页还是局部刷新。
4. 新增和编辑:同一个 EditActivity 处理两种模式
4.1 用 Intent extra 区分 add 和 edit
很多同学会写两个页面分别处理新增和编辑,这个项目只用了一个EditActivity,通过Intent附带不同参数来区分模式。列表页发起跳转时这样传:
// 新增 Intent intent = new Intent(MainActivity.this, EditActivity.class); intent.putExtra("mode", "add"); startActivityForResult(intent, REQUEST_CODE_ADD); // 编辑 intent.putExtra("mode", "edit"); intent.putExtra("note_id", note.getId()); intent.putExtra("title", note.getTitle()); intent.putExtra("content", note.getContent()); startActivityForResult(intent, REQUEST_CODE_EDIT);在EditActivity的onCreate里统一的取值逻辑:
String mode = getIntent().getStringExtra("mode"); long noteId = getIntent().getLongExtra("note_id", -1); if ("edit".equals(mode)) { mTitleEdit.setText(getIntent().getStringExtra("title")); mContentEdit.setText(getIntent().getStringExtra("content")); }这里故意用"add"和"edit"这种字符串做标记,虽然简单,但对大作业来说足够直白。如果想让代码更严谨,可以把这两个常量定义成接口或枚举。课程答辩时如果被问“为什么不用静态变量传值”,正确的回答是:静态变量在 Activity 被系统销毁重建时会丢失,而Intentextra 会跟随 Activity 状态自动保存恢复。
4.2 保存逻辑与空值校验
保存按钮的处理逻辑是新增和编辑两条分支的交汇点。判断当前是哪个模式,分别执行insert或update:
public void onSave(View view) { String title = mTitleEdit.getText().toString().trim(); String content = mContentEdit.getText().toString().trim(); if (title.isEmpty()) { title = "无标题笔记"; } long now = System.currentTimeMillis(); ContentValues values = new ContentValues(); values.put("title", title); values.put("content", content); values.put("update_time", now); SQLiteDatabase db = dbHelper.getWritableDatabase(); if ("edit".equals(mode) && noteId != -1) { db.update("notes", values, "_id=?", new String[]{String.valueOf(noteId)}); } else { db.insert("notes", null, values); } setResult(RESULT_OK); finish(); }ContentValues本质上是一个可序列化的键值对集合,键是列名,值是插入或更新的内容。update方法的第三个参数"_id=?"是条件语句,第四个参数是对应条件中的占位符。这样做比直接拼接字符串更安全,能避免用户输入单引号导致 SQL 语句报错。
一个体验细节是标题为空时自动填成“无标题笔记”。很多用户只在内容里写东西,并不想刻意输入标题,强行拦截会让操作变重,自动兜底反而更顺畅。保存后直接finish()结束编辑页,返回列表页时触发onResume()刷新数据。
4.3 删除功能与防止误触
删除操作放在编辑页比放在列表页更稳妥,尤其是没有侧滑删除手势的课程设计。长按列表项弹确认框,是另一种常见做法。这里给出编辑页内删除的实现:
new AlertDialog.Builder(this) .setTitle("删除") .setMessage("确认删除这条笔记?") .setPositiveButton("删除", (dialog, which) -> { SQLiteDatabase db = dbHelper.getWritableDatabase(); db.delete("notes", "_id=?", new String[]{String.valueOf(noteId)}); setResult(RESULT_OK); finish(); }) .setNegativeButton("取消", null) .show();删除属于不可逆操作,写成按钮直接删除再配合 Toast 提示,一旦用户手滑就只能傻眼。用AlertDialog多一步确认,表面上看是多了代码,实际是在交互逻辑上补齐一环,答辩时也能讲出设计依据。
5. 从 Gradle 配置到真机运行:交大作业前要过的三道检查
5.1 gradle.properties 和 build.gradle 里容易被盯上的配置
这个项目自带的gradle.properties里通常会有org.gradle.jvmargs和android.useAndroidX这些模板配置。新手往往原封不动搬到新电脑,结果一同步就报错。
org.gradle.jvmargs=-Xmx2048m android.useAndroidX=trueorg.gradle.jvmargs指定的是 Gradle 运行时的堆内存。如果机器只有 8G 内存,改成-Xmx2048m比模板里的 4G 更稳定。android.useAndroidX=true表示启用 AndroidX,一旦开启,依赖库里的android.support.*就要全部换成androidx.*包名,否则编译不过。
我拆项目时还发现它保留了proguard-rules.pro。开启混淆会涉及 keep 规则,大作业阶段没有必要,在build.gradle的release构建类型里把minifyEnabled设为false即可。
5.2 用命令行签名打包再检查一遍
日常都会直接点 Android Studio 的 Run 按钮,但交作业前建议在项目根目录跑一次命令行构建,能暴露不少 IDE 隐藏掉的问题:
./gradlew assembleDebugassembleDebug会执行编译、打包、生成 debug 签名 apk。如果这条命令通过,说明项目不依赖 Android Studio 的附加配置,换台电脑也能复现。如果需要正式交 apk,则用./gradlew assembleRelease,但此时需要配置签名文件,没有配置的话生成的是未签名的 release 包。
命令行构建还有个隐藏作用:能完整看到:app:compileDebugJavaWithJavac这条任务是否真的执行了增量编译。如果项目里存在无效 import 或资源引用,Android Studio 有时会闪烁后直接跑起旧版本,命令行不会给这种情面。
5.3 演示前最后 5 分钟检查清单
真机演示前把下面这张表过一遍,能避免大多数现场翻车:
| 检查项 | 操作 | 预期结果 |
|---|---|---|
| 横竖屏切换 | 在列表页旋转屏幕 | 页面不闪退,笔记仍在 |
| 中文输入 | 在编辑页输入中英文混排 | 保存后显示正常 |
| 空标题 | 只输入正文,不填标题 | 保存后出现“无标题笔记” |
| 返回键 | 编辑页按返回键 | 不保存且不崩溃 |
| 新机安装 | 关闭 USB 调试后运行 apk | 正常打开,无白屏 |
横竖屏切换这一项最容易被忽略。未配置configChanges时,旋转屏幕会让 Activity 重建,如果数据库游标没有正确处理,就会出现空列表或者崩溃。演示前多转两次屏幕,比准备任何答辩词都更有说服力。
本文还有配套的精品资源,点击获取