
简介这是一份面向高校学生的安卓记事本项目源码适合作为期末大作业、课程设计或安卓课设参考。项目采用原生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字符串这样排序和格式化时都更方便。四张字段表的关系如下字段类型说明_idINTEGER PRIMARY KEY AUTOINCREMENT主键自增作为笔记唯一标识titleTEXT标题可以留空contentTEXT正文内容update_timeINTEGER最近修改时间单位是毫秒这里有个细节排序时直接用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 ListNote 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.AdapterNoteAdapter.ViewHolder { private final ListNote notes; private OnItemClickListener listener; public NoteAdapter(ListNote 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或updatepublic 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.useAndroidXtrueorg.gradle.jvmargs指定的是 Gradle 运行时的堆内存。如果机器只有 8G 内存改成-Xmx2048m比模板里的 4G 更稳定。android.useAndroidXtrue表示启用 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 重建如果数据库游标没有正确处理就会出现空列表或者崩溃。演示前多转两次屏幕比准备任何答辩词都更有说服力。本文还有配套的精品资源点击获取