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只负责数据来源。

下面这张图,是我画的项目架构流程图,你看一眼就能明白整体脉络:

MVVM + DataBinding 项目架构图 View 层 Activity / Fragment DataBinding 布局 事件绑定 ViewModel 层 LiveData / StateFlow 业务逻辑 数据转换 Model 层 Repository Room DAO 网络数据源 观察 调用 回调 通知 数据流向说明: 1. 用户操作 → View 通过 DataBinding 触发 ViewModel 方法 2. ViewModel 调用 Repository 获取/操作数据 3. Repository 从 Room 或网络获取数据,返回给 ViewModel 4. ViewModel 更新 LiveData,View 自动刷新 UI 5. 整个流程是单向的,不会出现 View 直接操作数据库的情况

二、完整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