30、DataBinding实战项目:MVVM架构搭建、完整CRUD实现、搜索与过滤、性能监控与优化
终于到了这一章。说实话,前面讲了那么多DataBinding和ViewBinding的理论,很多朋友可能已经手痒了——到底这些东西凑在一起,能做出个什么样的项目?
这一章,我们就来干一票真的。我会带着你从零搭建一个基于MVVM架构的完整App,包含增删改查、搜索过滤,以及性能监控。嗯,这些都是我在实际商业项目中反复打磨过的套路,你直接拿去用就行。
一、项目结构:MVVM怎么拆?
先说说我的习惯。我一般会把项目拆成四个层级:
- Model层:数据实体 + 数据仓库(Repository)
- ViewModel层:持有LiveData/StateFlow,处理业务逻辑
- View层:Activity/Fragment + DataBinding布局
- Data层:Room数据库 + 网络请求(如果有)
你想想看,这样拆的好处是什么?每个模块各司其职,ViewModel不知道View是谁,View也不直接操作数据库。说白了,就是解耦。
核心原则:View只负责展示和事件传递,ViewModel只负责数据和状态,Model只负责数据来源。
下面这张图,是我画的项目架构流程图,你看一眼就能明白整体脉络:
二、完整CRUD实现
我们以一个「笔记App」为例。每条笔记有id、标题、内容、创建时间四个字段。先看实体类:
// Note.kt
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val title: String,
val content: String,
val createdAt: Long = System.currentTimeMillis()
)
然后是DAO层。这里我用了协程,因为Room从2.1开始就原生支持挂起函数了:
// NoteDao.kt
@Dao
interface NoteDao {
@Query("SELECT * FROM notes ORDER BY createdAt DESC")
fun getAllNotes(): LiveData<List<Note>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(note: Note)
@Update
suspend fun update(note: Note)
@Delete
suspend fun delete(note: Note)
@Query("SELECT * FROM notes WHERE title LIKE '%' || :query || '%' OR content LIKE '%' || :query || '%'")
fun searchNotes(query: String): LiveData<List<Note>>
}
小提示:注意看searchNotes方法,我用的是LIKE模糊匹配。如果数据量超过10万条,建议改用FTS4全文搜索,性能会好很多。我在一个项目中遇到过搜索卡顿的问题,后来换成FTS4,搜索速度提升了近10倍。
接下来是Repository层。它负责协调数据来源,这里我们只有本地数据库:
// NoteRepository.kt
class NoteRepository(private val noteDao: NoteDao) {
val allNotes: LiveData<List<Note>> = noteDao.getAllNotes()
suspend fun insert(note: Note) = noteDao.insert(note)
suspend fun update(note: Note) = noteDao.update(note)
suspend fun delete(note: Note) = noteDao.delete(note)
fun search(query: String): LiveData<List<Note>> {
return if (query.isBlank()) allNotes else noteDao.searchNotes(query)
}
}
三、ViewModel:核心业务逻辑
ViewModel是MVVM的心脏。它持有LiveData,暴露给View层观察。同时它负责处理所有业务逻辑:
// NoteViewModel.kt
class NoteViewModel(application: Application) : AndroidViewModel(application) {
private val repository: NoteRepository
val allNotes: LiveData<List<Note>>
// 搜索相关
private val _searchQuery = MutableLiveData<String>("")
val searchQuery: LiveData<String> = _searchQuery
// 当前编辑的笔记
private val _currentNote = MutableLiveData<Note?>(null)
val currentNote: LiveData<Note?> = _currentNote
init {
val db = AppDatabase.getInstance(application)
repository = NoteRepository(db.noteDao())
allNotes = repository.allNotes
}
fun insert(title: String, content: String) {
viewModelScope.launch {
repository.insert(Note(title = title, content = content))
}
}
fun update(note: Note) {
viewModelScope.launch {
repository.update(note)
}
}
fun delete(note: Note) {
viewModelScope.launch {
repository.delete(note)
}
}
fun setCurrentNote(note: Note?) {
_currentNote.value = note
}
fun search(query: String) {
_searchQuery.value = query
}
}
你可能会问:为什么searchQuery要用LiveData?因为我想在搜索框输入时,实时触发搜索。这个在DataBinding里做双向绑定非常方便。
四、DataBinding布局:双向绑定与事件处理
这是DataBinding最出彩的地方。看这个列表项的布局:
<!-- item_note.xml -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="note" type="com.example.app.Note" />
<variable name="viewModel" type="com.example.app.NoteViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:onClick="@{() -> viewModel.setCurrentNote(note)}">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@{note.title}"
android:textSize="18sp"
android:textStyle="bold" />
<ImageButton
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_delete"
android:onClick="@{() -> viewModel.delete(note)}" />
</LinearLayout>
</layout>
看到了吗?点击事件直接绑定到ViewModel的方法上,不需要写任何findViewById或setOnClickListener。这就是DataBinding的魅力——布局即代码。
注意:在onClick中直接调用viewModel.delete(note)时,如果note对象被修改了,可能会引发并发问题。我曾经踩过这个坑——列表滑动时删除了错误的条目。解决方案是在点击时传递note的id,而不是整个对象。
五、搜索与过滤:实时响应
搜索功能我用了「防抖」策略。什么意思?就是用户停止输入300毫秒后才触发搜索,避免每次按键都查数据库:
// 在Activity中
binding.searchEditText.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
// 取消之前的延迟任务
searchHandler.removeCallbacksAndMessages(null)
// 300ms后执行搜索
searchHandler.postDelayed({
viewModel.search(s?.toString() ?: "")
}, 300)
}
// 其他方法省略
})
然后在ViewModel中,我们监听searchQuery的变化,动态切换数据源:
// 在NoteViewModel中
val displayNotes: LiveData<List<Note>> = Transformations.switchMap(_searchQuery) { query ->
repository.search(query)
}
Transformations.switchMap是LiveData的利器。它会在searchQuery变化时,自动切换到新的数据源。说白了,就是「观察者模式」的升级版。
六、性能监控与优化
项目做完了,但性能怎么样?我一般会在App里埋几个监控点:
| 监控项 | 监控方式 | 优化建议 |
|---|---|---|
| 列表滑动帧率 | Choreographer + 自定义FrameCallback | 减少布局层级,使用DiffUtil |
| 数据库查询耗时 | Room的@RawQuery + 日志 | 加索引,避免N+1查询 |
| DataBinding绑定次数 | 自定义BindingComponent + 计数器 | 减少不必要的Observable字段 |
| 内存泄漏 | LeakCanary + 手动GC日志 | 注意ViewModel中不要持有View引用 |
举个例子,监控DataBinding绑定次数:
// 自定义BindingComponent
class MonitoringBindingComponent : DataBindingComponent {
override fun getMyAdapter(): MyAdapter {
return MonitoringAdapter()
}
}
// 在Application中设置
DataBindingUtil.setDefaultComponent(MonitoringBindingComponent())
这样你就能知道每个布局绑定了多少次,哪些布局绑得过于频繁。我在一个项目中就发现某个列表项每秒绑定了60多次,后来发现是LiveData频繁触发导致的,改成distinctUntilChanged后,绑定次数降到了个位数。
七、避坑指南
最后,分享几个我踩过的坑:
- 不要在ViewModel中持有Activity的Context。用AndroidViewModel或者ApplicationContext。
- 注意DataBinding的null安全。如果变量可能为null,记得用@={viewModel.note?.title}这种写法。
- 小心双向绑定导致的无限循环。比如EditText的text和ViewModel的LiveData互相触发,加个防抖或者用onTextChanged代替。
- 记得在onCleared()中取消协程,避免内存泄漏。
总结一下:MVVM + DataBinding + Room这套组合拳,打好了就是「降龙十八掌」,打不好就是「七伤拳」。关键在于理解数据流向,保持单向依赖。你只要记住:View只看ViewModel,ViewModel只看Repository,Repository只看数据源。这样你的代码就会像流水一样清晰。
好了,这一章的内容就到这里。代码都在上面了,你直接复制到项目里就能跑。如果遇到问题,欢迎在评论区留言,我看到会回复。
公众号:蓝海资料掘金营,微信deep3321