第三十节:综合实战项目——笔记App开发、需求分析、架构设计、完整实现、发布准备
终于到了这一步。说实话,每次我带团队做项目,最兴奋的就是这个阶段——把所有零散的知识点串起来,做一个真正能跑、能用的东西。笔记App这个选题,我选了很久。为什么?因为它够典型,够实用,而且几乎涵盖了Room数据库开发的所有核心场景。
你想想看,一个笔记App需要什么?增删改查、搜索、排序、数据同步……这些恰恰是我们在实际开发中最常遇到的需求。我个人习惯把这类项目叫做“试金石”——能把这个做明白,其他数据库项目基本都能拿下。
需求分析:别急着写代码
很多新手一上来就开干,结果做到一半发现需求没想清楚,回头改代码,那叫一个痛苦。我曾经在一个项目里吃过这个亏,后来学乖了——先花半小时把需求理清楚。
我们的笔记App,核心功能就这几个:
- 笔记列表:按时间倒序展示,支持搜索
- 笔记编辑:标题+内容,支持保存草稿
- 笔记删除:单条删除和批量删除
- 笔记分类:给笔记打标签,按标签筛选
- 数据导出:把笔记导出为文本文件
嗯,这里要注意——功能不是越多越好。我见过有人给笔记App加了语音输入、图片插入、Markdown渲染……结果项目拖了三个月还没上线。MVP(最小可行产品)思维,在个人项目里同样适用。
核心原则:先跑通核心流程,再考虑锦上添花。
架构设计:MVVM + Repository 模式
我个人比较推崇MVVM架构,配合Repository模式。为什么?因为职责清晰,测试方便,而且Google官方也在推这套。
来看一下整体架构图:
这张图我建议你保存下来。每次写代码前看一眼,问问自己:我当前写的这个类,属于哪一层?职责是否清晰?
数据库设计:Entity 和 DAO
先定义笔记的实体类。我记得第一次设计数据库时,把字段写得特别复杂,结果查询性能一塌糊涂。后来学乖了——够用就好。
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val title: String,
val content: String,
val category: String = "默认",
val createdAt: Long = System.currentTimeMillis(),
val updatedAt: Long = System.currentTimeMillis(),
val isDeleted: Boolean = false // 软删除标记
)
这里有个小细节——isDeleted字段。为什么要做软删除?说白了,就是给用户一个“后悔药”。我曾经在项目里直接物理删除,结果用户误删了重要笔记,找不回来,那叫一个崩溃。
再来看DAO接口:
@Dao
interface NoteDao {
@Query("SELECT * FROM notes WHERE isDeleted = 0 ORDER BY updatedAt DESC")
fun getAllNotes(): LiveData<List<Note>>
@Query("SELECT * FROM notes WHERE isDeleted = 0 AND (title LIKE '%' || :query || '%' OR content LIKE '%' || :query || '%')")
fun searchNotes(query: String): LiveData<List<Note>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertNote(note: Note): Long
@Update
suspend fun updateNote(note: Note)
@Query("UPDATE notes SET isDeleted = 1 WHERE id = :noteId")
suspend fun softDeleteNote(noteId: Long)
@Query("DELETE FROM notes WHERE isDeleted = 1")
suspend fun cleanDeletedNotes()
}
小提示:搜索功能用LIKE语句实现,虽然简单,但数据量大时性能会下降。如果笔记超过1万条,建议考虑FTS4全文搜索。
ViewModel 实现:状态管理
ViewModel的核心职责,就是管理UI状态。我个人习惯用一个密封类来表示不同的加载状态:
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
class NoteViewModel(private val repository: NoteRepository) : ViewModel() {
private val _notes = MutableLiveData<UiState<List<Note>>>()
val notes: LiveData<UiState<List<Note>>> = _notes
fun loadNotes() {
viewModelScope.launch {
_notes.value = UiState.Loading
try {
repository.getAllNotes().collect { noteList ->
_notes.value = UiState.Success(noteList)
}
} catch (e: Exception) {
_notes.value = UiState.Error(e.message ?: "未知错误")
}
}
}
}
你可能会问:为什么不用StateFlow?嗯,都可以。LiveData在Android生态里兼容性更好,StateFlow在Kotlin原生场景下更灵活。我个人偏好——如果项目是纯Kotlin,用StateFlow;如果还要跟Java代码混编,用LiveData。
完整实现:从数据库到界面
接下来是数据库的初始化。我建议用Application类来做:
class NoteApplication : Application() {
val database: NoteDatabase by lazy {
Room.databaseBuilder(
applicationContext,
NoteDatabase::class.java,
"note_database"
).build()
}
}
然后在Activity里获取ViewModel:
class MainActivity : AppCompatActivity() {
private val viewModel: NoteViewModel by viewModels {
NoteViewModelFactory((application as NoteApplication).database.noteDao())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.notes.observe(this) { state ->
when (state) {
is UiState.Loading -> showLoading()
is UiState.Success -> showNotes(state.data)
is UiState.Error -> showError(state.message)
}
}
}
}
注意:千万不要在Activity里直接操作数据库。我曾经见过有人直接在UI线程调用Room的插入操作,结果ANR(应用无响应)频发。Room默认不允许在主线程做数据库操作,除非你调用allowMainThreadQueries()——但千万别这么干。
发布准备:别忘了这些事
代码写完了,离发布还差几步。我整理了一个清单,每次发布前对照检查:
| 检查项 | 说明 | 优先级 |
|---|---|---|
| 数据库迁移 | 如果修改了Entity,必须写Migration,否则用户升级会崩溃 | 高 |
| 混淆规则 | Room的Entity和DAO不能被混淆,需要添加proguard规则 | 高 |
| 权限声明 | 如果涉及文件导出,需要声明存储权限 | 中 |
| 日志清理 | 发布前去掉所有Log.d/e,避免泄露信息 | 中 |
| 版本号更新 | versionCode和versionName要递增 | 高 |
说到混淆规则,我踩过一个坑——忘了给Room的Entity加@Keep注解,结果发布后用户反馈笔记打不开。查了半天,发现是混淆把字段名改了,Room反序列化时找不到对应的列。从那以后,我养成了习惯:所有Entity类都加上@Keep。
避坑指南:那些年我踩过的坑
最后分享几个实战中容易忽略的点:
- LiveData的粘性事件:如果你用LiveData传递一次性事件(比如Snackbar提示),可能会遇到重复消费的问题。我建议用EventWrapper包装一下。
- 数据库版本升级:测试阶段可以fallbackToDestructiveMigration(),但发布前一定要写正式的Migration。
- 协程作用域:在ViewModel里用viewModelScope,在Activity里用lifecycleScope,别混用。
嗯,差不多就这些了。笔记App这个项目,说难不难,说简单也不简单。关键是把每一步想清楚,代码写扎实。你按照这个思路走一遍,Room数据库开发这块基本就稳了。