☰
Android毕设记事本源码:Room+Material3+ViewModel全链路实现
2026/10/10 8:04:00 网站建设 项目流程

简介:这是一份面向计算机专业本科生的安卓应用开发实战项目源码,专为毕业设计与期末大作业打造,适用于Android Studio初学者进阶实践。项目实现了一个功能完整、界面规范的本地记事本App,涵盖增删改查、数据持久化(SQLite)、主题切换、音效反馈等核心模块,代码结构清晰、注释充分,经导师评审获98分高分,已通过本地全量编译与多轮调试,确保开箱即用。资源包共115个文件,含60个XML布局与资源文件、19个Java业务逻辑类、15个PNG图标及适配图、6个MP3提示音效,辅以Gradle构建脚本、Git配置及说明文档,整体压缩后仅17.56MB,轻量易导入。目前已有483人学习下载,适合需要真实项目练手、理解Android组件生命周期与MVC分层设计的学习者,可直接用于课程设计答辩或作为二次开发基础模板。

1. 这不是“Hello World”式记事本:一个能过答辩、能跑真机、带数据库和UI动效的安卓毕设源码包

你手头那份写着“Android记事本”的课程设计文档,是不是还在用TextView硬塞文字、点保存就弹个Toast、连删除都靠finish()模拟?别急——这份基于 Android Studio 开发的高分记事本 App 源码,是我在三所高校毕业答辩现场真实见过的「稳过型」项目:它用Room替代裸写 SQLite,支持夜间模式切换、笔记分类标签、搜索高亮、长按多选删除,甚至加了SharedViewModel实现列表页与编辑页状态同步。它不是教学 Demo,而是按《Android 应用开发规范(2023 教育版)》结构组织的完整工程:app/src/main/java/com/example/notepad/下清晰划分data/、ui/、domain/三层,build.gradle中已预置kotlin-kapt、androidx.lifecycle、material3等 12 项必考依赖。适合正在赶工本科毕设、期末大作业或需要快速交付可演示安卓 App 的开发者——你 clone 下来改个包名、连上真机,5 分钟内就能展示「新增-编辑-分类-搜索-夜间切换」全链路,答辩老师问到 MVVM 或 Room 原理时,源码里每行注释都是你的底气。


2. 从零跑通:环境配置、真机调试与 Gradle 兼容性处理

2.1 环境准备:避开 Android Studio 版本陷阱的实操清单

这份源码基于Android Studio Giraffe | 2022.3.1 Patch 2构建,但实际测试覆盖Flamingo(2022.2.1)至 Hedgehog(2023.1.1)三个主流版本。关键不是装最新版,而是避开两个高发雷区:

  • JDK 版本必须为 17:Giraffe 及之后版本默认捆绑 JDK 17,若你本地是 JDK 8 或 11,编译会直接报Unsupported class file major version 61。
  • Gradle 插件与 Gradle 版本需严格匹配:源码中gradle/wrapper/gradle-wrapper.properties指定distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip,对应build.gradle(Project 级)中plugins { id 'com.android.application' version '8.0.2' }。若强行升级插件到8.1.2,Room注解处理器会静默失效,导致NoteDao_Impl.java不生成。

提示:打开 Android Studio 后,先点击File > Project Structure > SDK Location,确认 JDK location 指向jbr-17目录;再进入Project Structure > Modules > app > Properties,检查 Compile SDK 和 Build Tools Version 是否为API 33 (Tiramisu)和33.0.2——这是源码预设的稳定组合,比盲目追新更省时间。

2.2 真机部署:绕过 USB 调试白名单与 ADB 权限黑盒

很多同学卡在「设备已连接但 Android Studio 不识别」,本质是厂商对 ADB 协议做了定制拦截。实测有效的三步法:

  1. 开启开发者选项:在手机设置中连续点击「关于手机 > 版本号」7 次;
  2. 强制启用 OEM 解锁与 USB 调试(安全设置):进入「开发者选项」,务必打开「OEM 解锁」(华为/小米需额外登录云账号授权)和「USB 调试(安全设置)」(非仅「USB 调试」);
  3. ADB 设备信任固化:首次连接时,手机弹出「允许 USB 调试吗?」对话框,勾选「始终允许」并点确定——此步骤漏掉会导致后续每次重连都需手动确认,且部分机型(如 OPPO ColorOS 13)不勾选则adb devices返回空列表。

验证是否成功:终端执行

adb devices -l

正常输出应类似:

List of devices attached 3A4Y17C090123456 device product:OP53 model:OPPO_Reno10_Pro_5G device:OP53 transport_id:1

若显示unauthorized,说明未点「始终允许」;若无任何输出,检查 USB 线是否为数据线(非仅充电线),或换 USB 接口(优先使用主板后置接口)。

2.3 Gradle 同步失败排查:国内网络下的替代方案

源码中build.gradle(Module: app)引用了mavenCentral()和google()仓库,但国内直连常超时。不要删仓库或降级依赖——正确做法是配置阿里云镜像:

在gradle/settings.gradle文件顶部添加:

dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://maven.aliyun.com/repository/google' } } }

同时,在gradle/wrapper/gradle-wrapper.properties中,将distributionUrl改为阿里云镜像地址:

distributionUrl=https\://maven.aliyun.com/repository/public/gradle/gradle-8.0-bin.zip

注意:修改后需点击 Android Studio 右上角「Sync Now」,而非重启 IDE。若仍失败,观察Build Output窗口末尾报错——90% 是Failed to resolve: androidx.room:room-runtime:2.6.0类错误,此时手动下载room-runtime-2.6.0.aar(从 maven.aliyun.com 搜索下载)放入~/.gradle/caches/modules-2/files-2.1/androidx.room/room-runtime/2.6.0/对应哈希目录,再 Sync 即可破局。


3. 核心功能拆解:Room 数据库建模、Material 3 UI 组件与 ViewModel 状态管理

3.1 Room 数据库:从 Entity 到 DAO 的四层封装逻辑

源码中data/database/目录下完整实现了 Room 的三层结构,但比官方文档多了一层业务抽象——NoteRepository。这不是画蛇添足,而是为应对毕设答辩中「为什么不用直接调 DAO?」的灵魂拷问:

  • NoteEntity.kt:定义主键@PrimaryKey(autoGenerate = true)、标题title、内容content、创建时间createdAt(@ColumnInfo(defaultValue = "CURRENT_TIMESTAMP"))、分类category(String,默认"default");
  • NoteDao.kt:声明@Insert、@Update、@Delete基础操作,以及关键的@Query("SELECT * FROM note WHERE category = :category ORDER BY createdAt DESC")分类查询;
  • NoteDatabase.kt:继承RoomDatabase,声明abstract fun noteDao(): NoteDao,并在build.gradle中通过kapt 'androidx.room:room-compiler:2.6.0'启用注解处理器;
  • NoteRepository.kt:这才是重点——它持有一个NoteDao实例,并暴露getNotesByCategory(category: String): Flow<List<Note>>方法,内部用flow { emit(dao.getNotesByCategory(category)) }封装,为 UI 层提供响应式数据流。

关键参数说明:Flow<List<Note>>而非LiveData,是因为源码采用kotlinx.coroutines协程架构,Flow可自然集成viewModelScope.launch,避免LiveData的生命周期感知冗余。答辩时若被问及「Room 与 SQLiteOpenHelper 区别」,可指着NoteEntity中@ColumnInfo(defaultValue = "CURRENT_TIMESTAMP")说:“Room 编译期校验 SQL 语法,而 SQLiteOpenHelper 需手动拼接字符串,易出错且无类型安全”。

3.2 Material 3 UI:Navigation Rail + TopAppBar 的双导航实践

源码摒弃了过时的BottomNavigationView,采用 Material 3 规范的NavigationRailView(左侧垂直导航栏)搭配TopAppBar(顶部操作栏),这不仅是视觉升级,更是为答辩加分的细节:

  • activity_main.xml中NavigationRailView包含 4 个MenuItem:首页(笔记列表)、分类(CategoryFragment)、搜索(SearchFragment)、设置(SettingsFragment);
  • 每个Fragment的布局均使用androidx.compose.material3.TopAppBar,右侧图标动态变化:列表页显示放大镜(跳转搜索),编辑页显示保存(触发viewModel.saveNote());
  • 夜间模式通过AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)绑定系统设置,themes.xml中Theme.App定义colorSurface、colorOnSurface等 12 个色值,确保深色模式下文字对比度达标(WCAG AA 标准)。

验证夜间模式:在手机「设置 > 显示 > 深色模式」中切换,App 无需重启即实时响应——这背后是MaterialTheme的dynamicColor支持,源码中res/values-night/colors.xml已预置深色系色值,比手动if (isNight) {}判断更符合现代安卓开发范式。

3.3 ViewModel 与状态管理:SharedViewModel 解决跨 Fragment 数据同步

传统记事本常把编辑逻辑写在EditActivity,导致列表页无法实时刷新。本项目用SharedViewModel彻底解决:

  • NoteListViewModel.kt与NoteEditViewModel.kt共享同一个NoteViewModel实例(通过by activityViewModels()获取);
  • NoteViewModel内部持有MutableStateFlow<NoteUiState>,其中NoteUiState是密封类,包含Loading、Success(List<Note>)、Error(String)三种状态;
  • 当NoteEditFragment调用viewModel.updateNote(note)后,NoteListViewModel的uiState.collectAsStateWithLifecycle()自动触发重组,列表页LazyColumn刷新——全程无startActivityForResult()或EventBus等过时方案。

参数说明:collectAsStateWithLifecycle()是androidx.lifecycle:lifecycle-runtime-compose:2.6.2提供的安全收集 API,相比collectAsState(),它自动在ON_STOP时暂停收集,避免内存泄漏。答辩时可强调:“我们用 Compose 的状态驱动 UI,而非findViewById().setText()的命令式写法”。


4. 避坑指南:那些让毕设答辩翻车的 5 个隐蔽问题与血泪解法

4.1 现象:App 启动闪退,Logcat 报java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/lifecycle/ViewModelProvider$Factory;

原因:build.gradle(Module: app)中遗漏implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2',或版本号与androidx.activity:activity-compose:1.7.2不兼容。
解决:检查dependencies块,确保包含以下三行(版本号必须完全一致):

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'

若已存在但版本不同,统一改为2.6.2并 Sync。

4.2 现象:新建笔记后列表不刷新,需手动下拉才显示

原因:NoteRepository中insertNote()方法未调用refreshNotes()或未触发Flow重发射,或NoteListViewModel的uiState未用mutableStateFlow正确更新。
解决:定位NoteRepository.insertNote(),确认其内部调用了dao.insert(note)后,立即执行notesFlow.value = notesFlow.value(强制刷新 Flow);同时检查NoteListViewModel中init { loadNotes() }是否在构造函数中正确初始化。

4.3 现象:真机上夜间模式无效,始终显示浅色主题

原因:AndroidManifest.xml中<application>标签缺少android:theme="@style/Theme.App",或themes.xml中Theme.App未继承Theme.Material3.DayNight。
解决:打开AndroidManifest.xml,确认<application>标签含android:theme="@style/Theme.App";打开res/values/themes.xml,检查<style name="Theme.App" parent="Theme.Material3.DayNight">—— 若 parent 是Theme.Material3.Light,则夜间模式必然失效。

4.4 现象:搜索功能输入文字后无高亮,或搜索结果为空但数据库有数据

原因:NoteDao中@Query("SELECT * FROM note WHERE title LIKE :query OR content LIKE :query")的:query参数未添加%通配符,或NoteRepository.searchNotes(query: String)方法中未做"%$query%"拼接。
解决:在NoteRepository.searchNotes()中,将传入的query处理为"%$query%",再传给 DAO:

fun searchNotes(query: String): Flow<List<Note>> { return flow { emit(dao.searchNotes("%$query%")) } }

4.5 现象:长按笔记进入多选模式后,点击删除按钮无反应

原因:NoteListFragment中onCreateActionMode()返回false,或ActionMode.Callback的onActionItemClicked()未调用viewModel.deleteSelectedNotes(selectedIds)。
解决:检查NoteListFragment的ActionMode.Callback实现,确认onActionItemClicked()中有:

override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { return when (item?.itemId) { R.id.action_delete -> { viewModel.deleteSelectedNotes(selectedIds) mode?.finish() true } else -> false } }

5. 进阶技巧:为答辩增色的 3 个可演示亮点与自定义组件封装

5.1 亮点一:用 Compose Canvas 实现手写笔记草图功能(非核心但吸睛)

源码中feature/note-draw/模块隐藏了一个彩蛋:在编辑页底部添加「手写」Tab,点击后进入DrawScreen,使用Canvas绘制贝塞尔曲线路径。这不是摆设——它展示了rememberSaveable保存笔迹状态、Modifier.pointerInput捕获触摸事件、Path动态绘制的完整链路:

@Composable fun DrawScreen() { var path by rememberSaveable { mutableStateOf(Path()) } var points by rememberSaveable { mutableStateOf(mutableListOf<Offset>()) } Canvas( modifier = Modifier .fillMaxSize() .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consumeAllChanges() points.add(change.position) // 贝塞尔插值平滑路径 if (points.size > 2) { path.reset() path.moveTo(points[0].x, points[0].y) for (i in 1 until points.size - 1) { val midX = (points[i].x + points[i + 1].x) / 2 val midY = (points[i].y + points[i + 1].y) / 2 path.quadraticBezierTo( points[i].x, points[i].y, midX, midY ) } } } } ) { drawPath(path, brush = SolidColor(Color.Black), style = Stroke(width = 4f)) } }

为什么值得演示?答辩老师看到「手写」二字会立刻抬头——这超越了基础 CRUD,证明你理解 Compose 的底层绘图机制。只需在NoteEditFragment的TabRow中添加Tab(text = { Text("手写") }, selected = tab == 2, onClick = { tab = 2 }),再嵌入DrawScreen()即可。

5.2 亮点二:自定义TagChipGroup组件实现笔记分类标签

源码ui/components/TagChipGroup.kt封装了一个可复用的标签组,支持动态添加/删除、颜色区分、点击筛选:

属性类型说明
tagsList<Tag>标签数据列表,含id: Long、name: String、color: Int
onTagClick(Tag) -> Unit点击标签时回调,用于筛选笔记
onTagRemove(Tag) -> Unit点击删除图标时回调

使用方式简洁:

TagChipGroup( tags = listOf( Tag(1, "工作", Color.Green), Tag(2, "学习", Color.Blue), Tag(3, "生活", Color.Orange) ), onTagClick = { tag -> viewModel.filterByTag(tag.id) }, onTagRemove = { tag -> viewModel.removeTag(tag.id) } )

这个组件的价值在于:它把「分类」从静态字符串变成了可交互 UI 元素,且代码复用率高——答辩时可指着TagChipGroup.kt说:“我们遵循单一职责原则,将标签逻辑抽离为独立 Composable,降低NoteListScreen的复杂度”。

5.3 亮点三:用WorkManager实现后台笔记备份(轻量级但显专业)

源码work/BackupWorker.kt实现了一个每日凌晨 2 点自动备份笔记到Context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)的后台任务:

class BackupWorker( private val context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { return try { val notes = getNotesFromRoom() // 从 Room 查询所有笔记 val backupFile = File( context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "notepad_backup_${System.currentTimeMillis()}.json" ) backupFile.writeText(Json.encodeToString(notes)) Result.success() } catch (e: Exception) { Result.failure() } } }

注册方式在Application.onCreate()中:

val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setTriggerContentMaxDelay(15, TimeUnit.MINUTES) .build() val backupRequest = PeriodicWorkRequestBuilder<BackupWorker>(24, TimeUnit.HOURS) .setConstraints(constraints) .build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( "backup_work", ExistingPeriodicWorkPolicy.KEEP, backupRequest )

为什么加分?因为WorkManager是安卓官方推荐的后台任务解决方案,替代了已被废弃的AlarmManager。答辩时可强调:“我们用PeriodicWorkRequestBuilder设置 24 小时周期,配合ExistingPeriodicWorkPolicy.KEEP避免重复注册,确保备份任务稳定运行”。

从那以后我每次交付毕设源码,都会在README.md里加一行:「演示要点:1. 手写 Tab 的贝塞尔平滑效果;2. 标签组的动态筛选;3. 后台备份的日志截图」——这三处不是炫技,而是把「我会用技术解决问题」具象成老师一眼能看懂的动作。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询