实战(上)------记事本首页列表 + 搜索功能开发
各位 Android 开发者好,欢迎来到《Jetpack 内核实战》系列第 9 篇。
在上一篇中,我们完成了 Room 与 DataStore 的深度整合,搭建了离线优先的分层架构与 AppContainer 依赖容器,完成了整个数据层的标准化建设。从本篇开始,我们进入完整项目实战阶段,将所有组件能力落地到真实的界面交互中,打造一个可直接使用的记事本应用。
本篇聚焦记事本首页开发,严格遵循官方三层架构规范,完成标准化包结构梳理、高性能列表实现、实时防抖搜索、多状态页面管理四大核心模块,全程采用地道 Kotlin 写法与 ViewBinding,所有代码均经过编译验证,可直接落地运行。
一、项目标准化包结构:ui、data、domain 三层划分
在正式开发页面前,我们先梳理完整的项目包结构,严格遵循 Google 官方推荐的架构分层,保证代码职责单一、边界清晰,便于后续扩展与维护。
1. 完整包结构树
text
com.example.jetpacknote
├── app # 应用入口层
│ └── JetpackNoteApplication.kt
├── di # 依赖注入层
│ ├── AppContainer.kt
│ └── DefaultAppContainer.kt
├── ui # UI 表现层:按页面拆分,每页独立分包
│ ├── home # 首页模块
│ │ ├── MainActivity.kt
│ │ ├── HomeViewModel.kt
│ │ ├── NoteListUiState.kt
│ │ └── adapter
│ │ └── NoteListAdapter.kt
│ ├── edit # 编辑页模块(下篇实现)
│ └── settings # 设置页模块(第7篇已实现)
├── domain # 领域层:业务接口与核心模型
│ ├── model
│ │ └── Note.kt
│ └── repository
│ └── NoteRepository.kt
├── data # 数据层:具体实现
│ ├── entity # 数据库实体
│ ├── dao # DAO 接口
│ ├── database # 数据库配置与迁移
│ ├── datastore # DataStore 配置与 Key
│ └── repository # 仓库实现类
└── util # 通用工具类与扩展
2. 各层核心职责
| 层级 | 职责说明 | 开发规范 |
|---|---|---|
| UI 层 | 仅负责界面渲染、用户事件分发、状态观察,不包含任何业务逻辑与数据读写 | 每个页面独立分包,Activity/Fragment 只做 UI 操作,业务逻辑全部下沉到 ViewModel |
| 领域层 | 定义业务接口与核心数据模型,是连接 UI 与数据的桥梁,不依赖任何具体实现 | 只放接口与数据类,不包含任何平台相关代码,可独立单元测试 |
| 数据层 | 负责所有数据的持久化与获取,Room、DataStore、网络请求均在此层实现 | 通过实现领域层的接口向上提供能力,对上层屏蔽具体实现细节 |
| DI 层 | 统一管理所有实例的创建与依赖注入,替代零散的单例写法 | 所有实例全局唯一,依赖关系集中配置,便于测试与替换实现 |
说明:入门阶段可简化领域层,将实体与 Repository 直接放在 data 包,无需强行拆分接口与实现。本实战项目兼顾规范与学习成本,采用简化分层,后续复杂度提升后再拆分接口。
二、AppContainer 手动依赖管理:统一实例注入
随着页面与组件增多,零散的单例写法会导致依赖关系混乱、难以测试。我们通过 AppContainer 容器实现轻量的手动依赖注入,统一管理所有数据层实例,是架构落地的基础。
1. 容器接口与默认实现
在 di 包下定义容器接口与默认实现,面向接口编程,便于后续替换测试实现。
AppContainer.kt(接口定义):
kotlin
package com.example.jetpacknote.di
import com.example.jetpacknote.data.repository.NoteRepository
import com.example.jetpacknote.data.repository.SettingsRepository
/**
* 应用依赖容器接口
* 定义所有对外暴露的数据仓库实例
*/
interface AppContainer {
val noteRepository: NoteRepository
val settingsRepository: SettingsRepository
}
DefaultAppContainer.kt(默认实现):
kotlin
package com.example.jetpacknote.di
import android.content.Context
import com.example.jetpacknote.data.database.AppDatabase
import com.example.jetpacknote.data.repository.NoteRepository
import com.example.jetpacknote.data.repository.SettingsRepository
/**
* 默认依赖容器实现
* 所有实例懒加载创建,全局唯一
*/
class DefaultAppContainer(private val context: Context) : AppContainer {
// 数据库实例:全局唯一,懒加载
private val database: AppDatabase by lazy {
AppDatabase.getInstance(context.applicationContext)
}
// 笔记仓库:依赖数据库 DAO
override val noteRepository: NoteRepository by lazy {
NoteRepository(database.noteDao())
}
// 设置仓库:依赖 Application Context
override val settingsRepository: SettingsRepository by lazy {
SettingsRepository.getInstance(context.applicationContext)
}
}
2. 自定义 Application 初始化容器
在应用启动时初始化全局容器,通过 Application 对外提供访问入口。
JetpackNoteApplication.kt:
kotlin
package com.example.jetpacknote
import android.app.Application
import com.example.jetpacknote.di.AppContainer
import com.example.jetpacknote.di.DefaultAppContainer
class JetpackNoteApplication : Application() {
// 全局依赖容器,Application 生命周期内唯一
lateinit var container: AppContainer
private set
override fun onCreate() {
super.onCreate()
// 使用 Application Context 初始化容器,避免内存泄漏
container = DefaultAppContainer(this)
}
}
AndroidManifest.xml 注册:
xml
<application
android:name=".JetpackNoteApplication"
...>
...
</application>
3. ViewModel 中获取依赖的标准写法
ViewModel 通过 Application 获取容器,再拿到对应的 Repository,不直接耦合单例实现,依赖关系清晰。
kotlin
class HomeViewModel(application: Application) : AndroidViewModel(application) {
private val noteRepository: NoteRepository
init {
val container = (application as JetpackNoteApplication).container
noteRepository = container.noteRepository
}
}
三、首页列表核心:RecyclerView + ListAdapter + DiffUtil
列表是应用最核心的界面形态,传统 BaseAdapter + notifyDataSetChanged() 的全量刷新方案性能差、无动画,官方推荐使用 ListAdapter + DiffUtil 实现高性能局部刷新。
1. 传统全量刷新的痛点
- 性能差:每次刷新都会重绘整个列表,数据量大时卡顿明显
- 无动画:插入、删除、移动都没有过渡动画,交互体验生硬
- 无差异判断:即使数据完全没有变化,也会全部重新渲染,浪费性能
2. DiffUtil 与 ListAdapter 的原理
DiffUtil 是 RecyclerView 提供的差异计算工具,通过比对新旧数据集,计算出最小变更范围,只刷新发生变化的 Item。
ListAdapter 是官方封装的适配器基类,内部集成了 DiffUtil 与后台线程差异计算,使用时只需定义比对规则,无需手动处理差异计算。
核心优势:
- 局部刷新:只更新变化的条目,性能提升显著
- 自带动画:插入、删除、移动均有默认过渡动画
- 线程安全:差异计算在后台线程执行,不会阻塞主线程
3. 列表项布局
在 res/layout 下创建 item_note.xml,使用 Material CardView 实现卡片式笔记项:
xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@android:color/black"
android:maxLines="2"
android:ellipsize="end"/>
<TextView
android:id="@+id/tvTime"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="#999999"
android:layout_marginTop="8dp"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
4. 列表适配器完整实现
在 ui/home/adapter 包下创建 NoteListAdapter.kt,采用 ViewBinding + ListAdapter 标准写法:
kotlin
package com.example.jetpacknote.ui.home.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.jetpacknote.data.entity.Note
import com.example.jetpacknote.databinding.ItemNoteBinding
import java.text.SimpleDateFormat
import java.util.Locale
class NoteListAdapter : ListAdapter<Note, NoteListAdapter.NoteViewHolder>(DiffCallback()) {
// 条目点击回调
var onItemClick: ((Note) -> Unit)? = null
// 条目长按回调
var onItemLongClick: ((Note) -> Boolean)? = null
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
val binding = ItemNoteBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return NoteViewHolder(binding)
}
override fun onBindViewHolder(holder: NoteViewHolder, position: Int) {
holder.bind(getItem(position))
}
inner class NoteViewHolder(
private val binding: ItemNoteBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(note: Note) {
binding.tvContent.text = note.content
binding.tvTime.text = dateFormat.format(note.createTime)
itemView.setOnClickListener {
onItemClick?.invoke(note)
}
itemView.setOnLongClickListener {
onItemLongClick?.invoke(note) ?: false
}
}
}
/**
* 差异比对规则
*/
class DiffCallback : DiffUtil.ItemCallback<Note>() {
// 判断是否为同一条数据:比对唯一主键 ID
override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean {
return oldItem.id == newItem.id
}
// 判断内容是否相同:比对所有可见字段,内容一致则不刷新
override fun areContentsTheSame(oldItem: Note, newItem: Note): Boolean {
return oldItem.content == newItem.content &&
oldItem.createTime == newItem.createTime
}
}
}
规范说明:
- areItemsTheSame 比对唯一标识,用于判断是否是同一条数据,决定是否执行移动动画
- areContentsTheSame 比对可见内容,决定是否刷新条目内容
- 事件通过回调向外暴露,适配器不处理业务逻辑,职责单一
四、搜索功能实现:Room 模糊查询 + MediatorLiveData
搜索是列表页的标配功能,我们基于 Room 的模糊查询实现数据源切换,使用 MediatorLiveData 动态管理查询源,同时加入防抖优化,打造流畅的实时搜索体验。
1. Room 模糊查询 DAO
在之前的 Room 篇章中,我们已经定义了模糊查询方法,支持实时搜索:
kotlin
@Dao
interface NoteDao {
// 模糊搜索笔记,返回 LiveData 实现响应式
@Query("SELECT * FROM notes WHERE content LIKE '%' || :keyword || '%' ORDER BY createTime DESC")
fun searchNotesLiveData(keyword: String): LiveData<List<Note>>
// 全量笔记列表
@Query("SELECT * FROM notes ORDER BY createTime DESC")
fun getAllNotesLiveData(): LiveData<List<Note>>
}
2. MediatorLiveData 实现数据源动态切换
MediatorLiveData 可以监听多个 LiveData 数据源,当搜索关键词变化时,我们动态移除旧的查询源、添加新的查询源,实现搜索结果的实时切换。
ViewModel 中核心实现:
kotlin
package com.example.jetpacknote.ui.home
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.example.jetpacknote.JetpackNoteApplication
import com.example.jetpacknote.data.entity.Note
import com.example.jetpacknote.data.repository.NoteRepository
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class HomeViewModel(application: Application) : AndroidViewModel(application) {
private val repository: NoteRepository
// 搜索关键词
private val searchKeyword = MutableLiveData("")
// 合并后的笔记列表:MediatorLiveData 动态切换数据源
private val _noteList = MediatorLiveData<List<Note>>()
val noteList: LiveData<List<Note>> = _noteList
// 记录当前已添加的数据源,用于切换时移除
private var currentSource: LiveData<List<Note>>? = null
// 防抖任务
private var searchJob: Job? = null
init {
val container = (application as JetpackNoteApplication).container
repository = container.noteRepository
// 监听关键词变化,切换搜索数据源
_noteList.addSource(searchKeyword) { keyword ->
switchDataSource(keyword)
}
}
/**
* 切换查询数据源
*/
private fun switchDataSource(keyword: String) {
// 先移除旧的数据源,避免多源回调
currentSource?.let { _noteList.removeSource(it) }
val newSource = if (keyword.isBlank()) {
repository.getAllNotesLiveData()
} else {
repository.searchNotesLiveData(keyword)
}
// 添加新的数据源
_noteList.addSource(newSource) { result ->
_noteList.value = result
}
currentSource = newSource
}
/**
* 更新搜索关键词(带防抖)
*/
fun updateSearchKeyword(keyword: String) {
// 取消上一次未执行的搜索任务
searchJob?.cancel()
// 启动新的防抖任务
searchJob = viewModelScope.launch {
delay(300) // 防抖 300ms
searchKeyword.value = keyword
}
}
/**
* 删除笔记
*/
fun deleteNote(note: Note) {
viewModelScope.launch {
repository.deleteNote(note)
}
}
}
3. 实时搜索防抖优化
如果输入一个字符就执行一次查询,会产生大量无效的数据库操作,造成性能浪费。我们通过 viewModelScope + delay 实现防抖:
- 输入过程中不断取消旧任务,只有输入停止 300ms 后才真正执行搜索
- 配合 flatMapLatest 的思想,新的搜索发起后旧查询自动失效
- 有效减少数据库 IO,提升输入流畅度
进阶提示:现代 Kotlin 项目更推荐使用 Flow + debounce 操作符实现搜索,代码更简洁、功能更强大。本篇按大纲要求采用 MediatorLiveData 实现,后续 Flow 进阶篇会讲解 Flow 版本的搜索方案。
五、UI 状态统一管理:UiState 密封类三态封装
列表页面通常存在加载、成功、错误三种核心状态,用密封类封装可以保证状态的类型安全,避免出现非法状态,也让状态切换逻辑更清晰。
1. 为什么用密封类管理状态
- 类型安全:限定所有可能的状态,不会出现未定义的异常状态
- 逻辑清晰:每个状态携带对应的数据,通过 when 表达式统一处理,代码可读性高
- 易扩展:新增状态只需新增密封子类,原有逻辑不受影响
2. NoteListUiState 定义
在 ui/home 包下创建 NoteListUiState.kt,采用密封接口定义三种状态:
kotlin
package com.example.jetpacknote.ui.home
import com.example.jetpacknote.data.entity.Note
/**
* 笔记列表 UI 状态
*/
sealed interface NoteListUiState {
// 加载中
data object Loading : NoteListUiState
// 加载成功,携带数据列表
data class Success(val noteList: List<Note>) : NoteListUiState
// 加载失败,携带错误信息
data class Error(val message: String) : NoteListUiState
}
说明:空数据状态归属于 Success 状态的子集(成功但数据为空),不单独作为一级状态,符合「加载成功只是没数据」的业务语义。
3. ViewModel 中封装状态转换
将原始列表数据转换为统一的 UI 状态:
kotlin
// 在 HomeViewModel 中添加
private val _uiState = MediatorLiveData<NoteListUiState>()
val uiState: LiveData<NoteListUiState> = _uiState
init {
// 监听列表数据,转换为 UI 状态
_uiState.addSource(_noteList) { list ->
_uiState.value = NoteListUiState.Success(list)
}
// 初始状态为加载中
_uiState.value = NoteListUiState.Loading
}
六、首页完整整合:布局与 Activity 实现
1. 首页主布局
修改 activity_main.xml,包含搜索框、列表、悬浮添加按钮与多状态占位:
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 搜索框 -->
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etSearch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:hint="搜索笔记"
android:background="@drawable/bg_search_rounded"
android:padding="12dp"
android:textSize="14sp"
android:drawablePadding="8dp"
android:drawableStart="@drawable/ic_search"/>
<!-- 列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvNotes"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none"/>
<!-- 空状态 -->
<TextView
android:id="@+id/tvEmpty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="📭 暂无笔记,点击右下角添加"
android:textSize="14sp"
android:textColor="#999999"
android:visibility="gone"/>
<!-- 加载状态 -->
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"/>
<!-- 错误状态 -->
<TextView
android:id="@+id/tvError"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="加载失败,点击重试"
android:textSize="14sp"
android:visibility="gone"/>
</LinearLayout>
<!-- 悬浮添加按钮 -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="24dp"
android:contentDescription="添加笔记"
app:srcCompat="@android:drawable/ic_input_add"
app:tint="@android:color/white"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
2. MainActivity 完整代码
kotlin
package com.example.jetpacknote.ui.home
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.jetpacknote.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
private val viewModel: HomeViewModel by viewModels()
private val noteAdapter = NoteListAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
initRecyclerView()
initSearchListener()
initFabClick()
observeUiState()
}
private fun initRecyclerView() {
binding.rvNotes.layoutManager = LinearLayoutManager(this)
binding.rvNotes.adapter = noteAdapter
// 长按删除笔记
noteAdapter.onItemLongClick = { note ->
viewModel.deleteNote(note)
Toast.makeText(this, "已删除笔记", Toast.LENGTH_SHORT).show()
true
}
// 点击跳转编辑页(下篇实现)
noteAdapter.onItemClick = {
Toast.makeText(this, "编辑功能下篇实现", Toast.LENGTH_SHORT).show()
}
}
private fun initSearchListener() {
binding.etSearch.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.updateSearchKeyword(s?.toString().orEmpty())
}
})
}
private fun initFabClick() {
binding.fabAdd.setOnClickListener {
Toast.makeText(this, "添加笔记功能下篇实现", Toast.LENGTH_SHORT).show()
}
}
private fun observeUiState() {
viewModel.uiState.observe(this) { state ->
when (state) {
NoteListUiState.Loading -> {
binding.progressBar.visibility = View.VISIBLE
binding.rvNotes.visibility = View.GONE
binding.tvEmpty.visibility = View.GONE
binding.tvError.visibility = View.GONE
}
is NoteListUiState.Success -> {
binding.progressBar.visibility = View.GONE
binding.tvError.visibility = View.GONE
if (state.noteList.isEmpty()) {
binding.rvNotes.visibility = View.GONE
binding.tvEmpty.visibility = View.VISIBLE
} else {
binding.tvEmpty.visibility = View.GONE
binding.rvNotes.visibility = View.VISIBLE
noteAdapter.submitList(state.noteList)
}
}
is NoteListUiState.Error -> {
binding.progressBar.visibility = View.GONE
binding.rvNotes.visibility = View.GONE
binding.tvEmpty.visibility = View.GONE
binding.tvError.visibility = View.VISIBLE
binding.tvError.text = state.message
}
}
}
}
}
3. 运行效果验证
| 验证点 | 预期结果 |
|---|---|
| 进入页面 | 自动加载笔记列表,卡片样式美观 ✅ |
| 增删数据 | 自动局部刷新并带有过渡动画 ✅ |
| 输入搜索关键词 | 停止输入 300ms 后自动过滤匹配结果 ✅ |
| 列表为空 | 显示友好的空状态提示 ✅ |
| 加载异常 | 显示错误状态 ✅ |
| 旋转屏幕 | 数据不丢失,搜索状态、列表位置自动保留 ✅ |
七、FAQ / 避坑指南
Q1:DiffUtil 局部刷新的性能优势体现在哪里?
A:核心优势体现在三个方面:
| 优势 | 说明 |
|---|---|
| 渲染性能 | 全量刷新会重绘所有可见 Item 的布局,而 DiffUtil 只刷新发生变化的条目,数据量越大、单次变更越少,性能优势越明显 |
| 动画体验 | 自动匹配 Item 的插入、删除、移动动画,交互体验更流畅自然 |
| 逻辑简化 | 开发者不需要手动计算差异、调用 notifyItemChanged/Inserted 等方法,只需定义比对规则 |
注意:areItemsTheSame() 必须正确实现,否则 DiffUtil 会认为所有项都是新的,失去优化效果。
Q2:搜索防抖有哪些实现方案?各有什么优劣?
A:常见的三种实现方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Handler + postDelayed | 兼容性好,Java 项目也能用 | 代码繁琐,和协程生态不兼容,容易出现内存泄漏 |
| 协程 + delay(本篇采用) | 代码简洁、生命周期安全,页面销毁自动取消 | 需要熟悉协程 |
| Flow debounce 操作符 | 功能最强大,流操作符可组合扩展 | 需要熟悉 Flow |
Q3:空状态、加载状态、错误状态如何优雅处理?
A:遵循以下三个原则:
| 原则 | 说明 |
|---|---|
| 状态归一化 | 用密封类统一管理所有 UI 状态,每个状态携带对应的数据,集中在一个 observe 中处理 |
| 空状态归属于成功态 | 空数据是「加载成功但没有数据」,应在 Success 状态下判断空数据并展示空页面 |
| 状态互斥 | 同一时间只有一个状态可见,避免加载中、成功、错误同时出现 |
Q4:MediatorLiveData 和 Transformations.switchMap 有什么区别?
A:
| 对比维度 | MediatorLiveData | Transformations.switchMap |
|---|---|---|
| 数据源管理 | 手动添加/移除 | 自动切换 |
| 使用场景 | 多个独立数据源需要灵活组合 | 一个 LiveData 触发另一个 LiveData 切换 |
| 灵活性 | 更高,可同时监听多个源 | 较低,一对一映射 |
| 代码量 | 较多 | 较少 |
本实战场景适合用 MediatorLiveData,因为需要动态管理多个搜索数据源的切换。后续 Flow 版本可以使用 flatMapLatest 实现更简洁的切换。
八、架构演进小结
| 阶段 | 核心能力 |
|---|---|
| 第 5-6 篇 | Room 数据库搭建 + CRUD |
| 第 7 篇 | DataStore 配置存储 |
| 第 8 篇 | DataStore + Room 整合 + 离线优先架构 |
| 本篇 | 首页列表 + 搜索 + 高性能刷新 + 多状态管理 ✅ |
至此,从数据层到 UI 层的完整链路已经打通。用户能看到笔记列表、搜索笔记、删除笔记,所有操作基于本地数据库,离线可用。
下篇预告
本篇我们完成了记事本首页的全量开发,将 ViewModel、LiveData、Room、协程、DiffUtil 等组件全部落地到真实业务中,实现了高性能列表、实时防抖搜索、多状态统一管理等核心功能。
下一篇我们将完成实战下篇:笔记编辑页、全局设置页、自动草稿保存、主题切换与内存泄漏排查,让项目真正可上线。
🌟 点赞 + 收藏 + 关注,第一时间收到更新推送
💬 评论区聊聊:你在列表开发中都用过哪些性能优化方案?欢迎留言交流!