上一个MVVM demo改成用databinding绑定数据测试下。
app/build.gradle.kts 启用dataBinding:

布局修改下,最外层用layout标签,data标签设置变量。绑定数据和点击事件。@{}是单项绑定。即数据变化会自动刷新UI。
XML
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.mvvmdemo.WuxiaCharacterViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp"
tools:context=".MainActivity">
<!-- 人物名称 -->
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="段正淳"
android:textSize="36sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<!-- 魅力区域 -->
<TextView
android:id="@+id/tv_charm_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="魅力:"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_name"
android:layout_marginTop="20dp"/>
<!--@{} 绑定点击事件-->
<Button
android:id="@+id/btn_charm_minus"
android:layout_width="40dp"
android:layout_height="40dp"
android:onClick="@{()->viewModel.decrementCharm()}"
android:text="-"
app:layout_constraintStart_toEndOf="@id/tv_charm_label"
app:layout_constraintTop_toTopOf="@id/tv_charm_label"
app:layout_constraintBottom_toBottomOf="@id/tv_charm_label"/>
<!-- 魅力值 @{}是单向绑定,即数据变化会刷新UI -->
<TextView
android:id="@+id/tv_charm_value"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:text="@{viewModel.charm.toString()}"
android:textSize="20sp"
android:gravity="center"
app:layout_constraintStart_toEndOf="@id/btn_charm_minus"
app:layout_constraintTop_toTopOf="@id/tv_charm_label"
app:layout_constraintBottom_toBottomOf="@id/tv_charm_label"/>
<Button
android:id="@+id/btn_charm_plus"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="+"
android:onClick="@{()->viewModel.incrementCharm()}"
app:layout_constraintStart_toEndOf="@id/tv_charm_value"
app:layout_constraintTop_toTopOf="@id/tv_charm_label"
app:layout_constraintBottom_toBottomOf="@id/tv_charm_label"/>
<!-- 武力区域 -->
<TextView
android:id="@+id/tv_force_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="武力:"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_charm_label"
android:layout_marginTop="10dp"/>
<Button
android:id="@+id/btn_force_minus"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="-"
android:onClick="@{()->viewModel.decrementForce()}"
app:layout_constraintStart_toEndOf="@id/tv_force_label"
app:layout_constraintTop_toTopOf="@id/tv_force_label"
app:layout_constraintBottom_toBottomOf="@id/tv_force_label"/>
<TextView
android:id="@+id/tv_force_value"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:text="@{viewModel.force.toString()}"
android:textSize="20sp"
android:gravity="center"
app:layout_constraintStart_toEndOf="@id/btn_force_minus"
app:layout_constraintTop_toTopOf="@id/tv_force_label"
app:layout_constraintBottom_toBottomOf="@id/tv_force_label"/>
<Button
android:id="@+id/btn_force_plus"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="+"
android:onClick="@{()->viewModel.incrementForce()}"
app:layout_constraintStart_toEndOf="@id/tv_force_value"
app:layout_constraintTop_toTopOf="@id/tv_force_label"
app:layout_constraintBottom_toBottomOf="@id/tv_force_label"/>
<!-- 财富区域 -->
<TextView
android:id="@+id/tv_wealth_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="财富:"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_force_label"
android:layout_marginTop="10dp"/>
<Button
android:id="@+id/btn_wealth_minus"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="-"
android:onClick="@{()->viewModel.decrementWealth()}"
app:layout_constraintStart_toEndOf="@id/tv_wealth_label"
app:layout_constraintTop_toTopOf="@id/tv_wealth_label"
app:layout_constraintBottom_toBottomOf="@id/tv_wealth_label"/>
<TextView
android:id="@+id/tv_wealth_value"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="@{viewModel.formatWealth(viewModel.wealth)}"
android:textSize="20sp"
android:gravity="center"
app:layout_constraintStart_toEndOf="@id/btn_wealth_minus"
app:layout_constraintTop_toTopOf="@id/tv_wealth_label"
app:layout_constraintBottom_toBottomOf="@id/tv_wealth_label"/>
<Button
android:id="@+id/btn_wealth_plus"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="+"
android:onClick="@{()->viewModel.incrementWealth()}"
app:layout_constraintStart_toEndOf="@id/tv_wealth_value"
app:layout_constraintTop_toTopOf="@id/tv_wealth_label"
app:layout_constraintBottom_toBottomOf="@id/tv_wealth_label"/>
<!-- 武功 -->
<TextView
android:id="@+id/tv_skill"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="武功:一阳指"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_wealth_label"
android:layout_marginTop="10dp"/>
<!-- 介绍 -->
<TextView
android:id="@+id/tv_intro"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="介绍:少女收割机"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_skill"
android:layout_marginTop="10dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
修改MainActivity:
Kotlin
package com.example.mvvmdemo
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import com.example.mvvmdemo.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
// 武侠人物ViewModel
private lateinit var characterViewModel: WuxiaCharacterViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// databinding加载布局
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
// 注册LifecycleObserver(Activity是LifecycleOwner)
lifecycle.addObserver(MyLifecycleObserver()) // 这里没啥用
// 通过ViewModelProvider获取ViewModel(配置变化不丢失数据)
//this即Activity,实现了ViewModelStoreOwner 接口, ViewModel 会和 Activity 的生命周期绑定
characterViewModel = ViewModelProvider(this)[WuxiaCharacterViewModel::class.java]
// 绑定ViewModel到布局。
binding.viewModel = characterViewModel
// 设置生命周期所有者
binding.lifecycleOwner = this
}
}
可以看出,代码简洁了一些。删除了观察数据变化刷新UI的代码,以及删除了设置点击事件。
运行,测试ok,还是这个页面,点击加减都能修改属性值:

再试下双向绑定, 即实现修改UI能自动修改viewModel中的数据。
修改viewModel,加一个健康指数的字段:

修改布局,添加textView显示健康指数,旁边一个输入框可以修改值,并双向绑定数据:
XML
<!--健康指数-->
<TextView
android:id="@+id/tv_health_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="健康:"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_wealth_label"
android:layout_marginTop="10dp"/>
<TextView
android:id="@+id/tv_health"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.health.toString()}"
android:textSize="20sp"
android:gravity="center"
app:layout_constraintStart_toEndOf="@id/tv_health_label"
app:layout_constraintTop_toTopOf="@id/tv_health_label"
app:layout_constraintBottom_toBottomOf="@id/tv_health_label"
android:layout_marginStart="10dp"/>
<EditText
android:id="@+id/et_health"
android:text="@={viewModel.health.toString()}"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/tv_health"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tv_health"
app:layout_constraintBottom_toBottomOf="@id/tv_health"
android:layout_marginStart="10dp"
android:hint="请修改健康指数" />
但是绑定数据编译不过,报错:The expression 'viewModel.health.toString()' cannot be inverted, so it cannot be used in a two-way bindin
原因是viewModel.health.toString() 是数据正向传递,即从viewModel -->UI, 缺少反向传递,即从UI -->viewModel.
数据是int,显示需要string,折腾了一圈数据转换,各种报错。找了一个折衷方案,监听输入框内容变化时,调用一个函数修改viewModel中的数据,这样实现反向绑定。改完后如下:
activity_main 布局, 健康值health的反向绑定是增加了android:onTextChanged:
XML
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.example.mvvmdemo.WuxiaCharacterViewModel" />
<variable
name="viewModel"
type="com.example.mvvmdemo.WuxiaCharacterViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp"
tools:context=".MainActivity">
<!-- 人物名称 -->
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="段正淳"
android:textSize="36sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<!-- 魅力区域 -->
<TextView
android:id="@+id/tv_charm_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="魅力:"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_name"
android:layout_marginTop="20dp"/>
<!--@{} 绑定点击事件-->
<Button
android:id="@+id/btn_charm_minus"
android:layout_width="40dp"
android:layout_height="40dp"
android:onClick="@{()->viewModel.decrementCharm()}"
android:text="-"
app:layout_constraintStart_toEndOf="@id/tv_charm_label"
app:layout_constraintTop_toTopOf="@id/tv_charm_label"
app:layout_constraintBottom_toBottomOf="@id/tv_charm_label"/>
<!-- 魅力值 @{}是单向绑定,即数据变化会刷新UI -->
<TextView
android:id="@+id/tv_charm_value"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:text="@{viewModel.charm.toString()}"
android:textSize="20sp"
android:gravity="center"
app:layout_constraintStart_toEndOf="@id/btn_charm_minus"
app:layout_constraintTop_toTopOf="@id/tv_charm_label"
app:layout_constraintBottom_toBottomOf="@id/tv_charm_label"/>
<Button
android:id="@+id/btn_charm_plus"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="+"
android:onClick="@{()->viewModel.incrementCharm()}"
app:layout_constraintStart_toEndOf="@id/tv_charm_value"
app:layout_constraintTop_toTopOf="@id/tv_charm_label"
app:layout_constraintBottom_toBottomOf="@id/tv_charm_label"/>
<!-- 武力区域 -->
<TextView
android:id="@+id/tv_force_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="武力:"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_charm_label"
android:layout_marginTop="10dp"/>
<Button
android:id="@+id/btn_force_minus"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="-"
android:onClick="@{()->viewModel.decrementForce()}"
app:layout_constraintStart_toEndOf="@id/tv_force_label"
app:layout_constraintTop_toTopOf="@id/tv_force_label"
app:layout_constraintBottom_toBottomOf="@id/tv_force_label"/>
<TextView
android:id="@+id/tv_force_value"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:text="@{viewModel.force.toString()}"
android:textSize="20sp"
android:gravity="center"
app:layout_constraintStart_toEndOf="@id/btn_force_minus"
app:layout_constraintTop_toTopOf="@id/tv_force_label"
app:layout_constraintBottom_toBottomOf="@id/tv_force_label"/>
<Button
android:id="@+id/btn_force_plus"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="+"
android:onClick="@{()->viewModel.incrementForce()}"
app:layout_constraintStart_toEndOf="@id/tv_force_value"
app:layout_constraintTop_toTopOf="@id/tv_force_label"
app:layout_constraintBottom_toBottomOf="@id/tv_force_label"/>
<!-- 财富区域 -->
<TextView
android:id="@+id/tv_wealth_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="财富:"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_force_label"
android:layout_marginTop="10dp"/>
<Button
android:id="@+id/btn_wealth_minus"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="-"
android:onClick="@{()->viewModel.decrementWealth()}"
app:layout_constraintStart_toEndOf="@id/tv_wealth_label"
app:layout_constraintTop_toTopOf="@id/tv_wealth_label"
app:layout_constraintBottom_toBottomOf="@id/tv_wealth_label"/>
<TextView
android:id="@+id/tv_wealth_value"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="@{viewModel.formatWealth(viewModel.wealth)}"
android:textSize="20sp"
android:gravity="center"
app:layout_constraintStart_toEndOf="@id/btn_wealth_minus"
app:layout_constraintTop_toTopOf="@id/tv_wealth_label"
app:layout_constraintBottom_toBottomOf="@id/tv_wealth_label"/>
<Button
android:id="@+id/btn_wealth_plus"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="+"
android:onClick="@{()->viewModel.incrementWealth()}"
app:layout_constraintStart_toEndOf="@id/tv_wealth_value"
app:layout_constraintTop_toTopOf="@id/tv_wealth_label"
app:layout_constraintBottom_toBottomOf="@id/tv_wealth_label"/>
<!--健康指数-->
<TextView
android:id="@+id/tv_health_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="健康:"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_wealth_label"
android:layout_marginTop="10dp"/>
<TextView
android:id="@+id/tv_health"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(viewModel.health)}"
android:textSize="20sp"
android:gravity="center"
app:layout_constraintStart_toEndOf="@id/tv_health_label"
app:layout_constraintTop_toTopOf="@id/tv_health_label"
app:layout_constraintBottom_toBottomOf="@id/tv_health_label"
android:layout_marginStart="10dp"/>
<EditText
android:id="@+id/et_health"
android:inputType="number"
android:text="@{String.valueOf(viewModel.health)}"
android:onTextChanged="@{(s, start, before, count) -> viewModel.onHealthTextChanged(s)}"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/tv_health"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tv_health"
app:layout_constraintBottom_toBottomOf="@id/tv_health"
android:layout_marginStart="10dp"
android:hint="请修改健康指数" />
<!-- 武功 -->
<TextView
android:id="@+id/tv_skill"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="武功:一阳指"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_health_label"
android:layout_marginTop="10dp"/>
<!-- 介绍 -->
<TextView
android:id="@+id/tv_intro"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="介绍:少女收割机"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_skill"
android:layout_marginTop="10dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
viewModel 增加了一个onHealthTextChanged函数实现方向绑定,代码:
Kotlin
package com.example.mvvmdemo
import androidx.databinding.PropertyChangeRegistry
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
// 武侠人物ViewModel,存储所有可调整的属性。
// 业务逻辑 + 数据管理
class WuxiaCharacterViewModel : ViewModel() {
// 用于管理属性变化通知的工具类
private val callbacks = PropertyChangeRegistry()
// 魅力值(初始99)
private val _charm = MutableLiveData<Int>(99) // MutableLiveData可变。 这里只在内部修改数据
val charm: LiveData<Int> = _charm // LiveData 不可变。 暴露给外部,只读
// 武力值(初始85)
private val _force = MutableLiveData<Int>(85)
val force: LiveData<Int> = _force
// 财富值(初始1亿,用Long存储)
private val _wealth = MutableLiveData<Long>(100000000)
val wealth: LiveData<Long> = _wealth
// 健康指数(初始99)
val health = MutableLiveData<Int>(99)
// 新增:手动处理文本变化
fun onHealthTextChanged(s: CharSequence?) {
val newValue = s?.toString()?.toIntOrNull() ?: 0
// 只有当新值确实不同时才更新,防止死循环
if (health.value != newValue) {
health.value = newValue
}
}
// 调整魅力值
fun incrementCharm() {
_charm.value = (_charm.value ?: 99) + 1
}
fun decrementCharm() {
val current = _charm.value ?: 99
if (current > 0) {
_charm.value = current - 1
}
}
// 调整武力值
fun incrementForce() {
_force.value = (_force.value ?: 85) + 1
}
fun decrementForce() {
val current = _force.value ?: 85
if (current > 0) {
_force.value = current - 1
}
}
// 调整财富值(每次增减100万)
fun incrementWealth() {
_wealth.value = (_wealth.value ?: 100000000) + 1000000
}
fun decrementWealth() {
val current = _wealth.value ?: 100000000
if (current > 1000000) { // 至少保留100万
_wealth.value = current - 1000000
}
}
// 格式化财富显示(转成"X亿"或"X万")
fun formatWealth(wealth: Long): String {
return when {
wealth >= 100000000 -> "${wealth / 100000000.0}亿"
wealth >= 10000 -> "${wealth / 10000}万"
else -> "$wealth"
}
}
// 加载状态(告诉UI是否在加载中,比如显示加载动画)
// private val _isLoading = MutableLiveData<Boolean>(false)
// val isLoading: LiveData<Boolean> = _isLoading
// 错误信息(新增:请求失败时提示用户)
// private val _errorMsg = MutableLiveData<String>("")
// val errorMsg: LiveData<String> = _errorMsg
fun refreshCharacterData() {
// 模拟从服务端获取数据
// 显示加载状态(UI会感知到,显示加载动画)
// _isLoading.value = true
// _errorMsg.value = ""
// viewModelScope启动协程(自动跟随ViewModel生命周期,避免内存泄漏)
// viewModelScope.launch {
// // 调用model层代码,获取数据。。。
// }
}
}
MainActivity ,延迟2每秒修改健康值,测试下输入框的数字是否改变:
Kotlin
package com.example.mvvmdemo
import android.os.Bundle
import android.os.Handler
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import com.example.mvvmdemo.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
// 武侠人物ViewModel
private lateinit var characterViewModel: WuxiaCharacterViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// databinding加载布局
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
// 注册LifecycleObserver(Activity是LifecycleOwner)
lifecycle.addObserver(MyLifecycleObserver()) // 这里没啥用
// 通过ViewModelProvider获取ViewModel(配置变化不丢失数据)
//this即Activity,实现了ViewModelStoreOwner 接口, ViewModel 会和 Activity 的生命周期绑定
characterViewModel = ViewModelProvider(this)[WuxiaCharacterViewModel::class.java]
// 绑定ViewModel到布局。
binding.viewModel = characterViewModel
// 设置生命周期所有者
binding.lifecycleOwner = this
Handler().postDelayed({
characterViewModel.health.value = 10000
}, 2000)
}
}
ok. 数据变化,输入框值也修改。修改输入框,数据也改动。实现了双向绑定。 还有个方案是数据用string类型,和输入框需要的数据类型一致。
