上一个MVVM demo改成用databinding绑定数据测试下。app/build.gradle.kts 启用dataBinding布局修改下最外层用layout标签data标签设置变量。绑定数据和点击事件。{}是单项绑定。即数据变化会自动刷新UI。?xml version1.0 encodingutf-8? layout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto xmlns:toolshttp://schemas.android.com/tools data variable nameviewModel typecom.example.mvvmdemo.WuxiaCharacterViewModel/ /data androidx.constraintlayout.widget.ConstraintLayout android:layout_widthmatch_parent android:layout_heightmatch_parent android:padding20dp tools:context.MainActivity !-- 人物名称 -- TextView android:idid/tv_name android:layout_widthwrap_content android:layout_heightwrap_content android:text段正淳 android:textSize36sp android:textStylebold app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toTopOfparent/ !-- 魅力区域 -- TextView android:idid/tv_charm_label android:layout_widthwrap_content android:layout_heightwrap_content android:text魅力 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_name android:layout_marginTop20dp/ !--{} 绑定点击事件-- Button android:idid/btn_charm_minus android:layout_width40dp android:layout_height40dp android:onClick{()-viewModel.decrementCharm()} android:text- app:layout_constraintStart_toEndOfid/tv_charm_label app:layout_constraintTop_toTopOfid/tv_charm_label app:layout_constraintBottom_toBottomOfid/tv_charm_label/ !-- 魅力值 {}是单向绑定即数据变化会刷新UI -- TextView android:idid/tv_charm_value android:layout_width60dp android:layout_heightwrap_content android:text{viewModel.charm.toString()} android:textSize20sp android:gravitycenter app:layout_constraintStart_toEndOfid/btn_charm_minus app:layout_constraintTop_toTopOfid/tv_charm_label app:layout_constraintBottom_toBottomOfid/tv_charm_label/ Button android:idid/btn_charm_plus android:layout_width40dp android:layout_height40dp android:text android:onClick{()-viewModel.incrementCharm()} app:layout_constraintStart_toEndOfid/tv_charm_value app:layout_constraintTop_toTopOfid/tv_charm_label app:layout_constraintBottom_toBottomOfid/tv_charm_label/ !-- 武力区域 -- TextView android:idid/tv_force_label android:layout_widthwrap_content android:layout_heightwrap_content android:text武力 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_charm_label android:layout_marginTop10dp/ Button android:idid/btn_force_minus android:layout_width40dp android:layout_height40dp android:text- android:onClick{()-viewModel.decrementForce()} app:layout_constraintStart_toEndOfid/tv_force_label app:layout_constraintTop_toTopOfid/tv_force_label app:layout_constraintBottom_toBottomOfid/tv_force_label/ TextView android:idid/tv_force_value android:layout_width60dp android:layout_heightwrap_content android:text{viewModel.force.toString()} android:textSize20sp android:gravitycenter app:layout_constraintStart_toEndOfid/btn_force_minus app:layout_constraintTop_toTopOfid/tv_force_label app:layout_constraintBottom_toBottomOfid/tv_force_label/ Button android:idid/btn_force_plus android:layout_width40dp android:layout_height40dp android:text android:onClick{()-viewModel.incrementForce()} app:layout_constraintStart_toEndOfid/tv_force_value app:layout_constraintTop_toTopOfid/tv_force_label app:layout_constraintBottom_toBottomOfid/tv_force_label/ !-- 财富区域 -- TextView android:idid/tv_wealth_label android:layout_widthwrap_content android:layout_heightwrap_content android:text财富 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_force_label android:layout_marginTop10dp/ Button android:idid/btn_wealth_minus android:layout_width40dp android:layout_height40dp android:text- android:onClick{()-viewModel.decrementWealth()} app:layout_constraintStart_toEndOfid/tv_wealth_label app:layout_constraintTop_toTopOfid/tv_wealth_label app:layout_constraintBottom_toBottomOfid/tv_wealth_label/ TextView android:idid/tv_wealth_value android:layout_width100dp android:layout_heightwrap_content android:text{viewModel.formatWealth(viewModel.wealth)} android:textSize20sp android:gravitycenter app:layout_constraintStart_toEndOfid/btn_wealth_minus app:layout_constraintTop_toTopOfid/tv_wealth_label app:layout_constraintBottom_toBottomOfid/tv_wealth_label/ Button android:idid/btn_wealth_plus android:layout_width40dp android:layout_height40dp android:text android:onClick{()-viewModel.incrementWealth()} app:layout_constraintStart_toEndOfid/tv_wealth_value app:layout_constraintTop_toTopOfid/tv_wealth_label app:layout_constraintBottom_toBottomOfid/tv_wealth_label/ !-- 武功 -- TextView android:idid/tv_skill android:layout_widthwrap_content android:layout_heightwrap_content android:text武功一阳指 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_wealth_label android:layout_marginTop10dp/ !-- 介绍 -- TextView android:idid/tv_intro android:layout_widthwrap_content android:layout_heightwrap_content android:text介绍少女收割机 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_skill android:layout_marginTop10dp/ /androidx.constraintlayout.widget.ConstraintLayout /layout修改MainActivitypackage 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) // 注册LifecycleObserverActivity是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显示健康指数旁边一个输入框可以修改值并双向绑定数据!--健康指数-- TextView android:idid/tv_health_label android:layout_widthwrap_content android:layout_heightwrap_content android:text健康 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_wealth_label android:layout_marginTop10dp/ TextView android:idid/tv_health android:layout_widthwrap_content android:layout_heightwrap_content android:text{viewModel.health.toString()} android:textSize20sp android:gravitycenter app:layout_constraintStart_toEndOfid/tv_health_label app:layout_constraintTop_toTopOfid/tv_health_label app:layout_constraintBottom_toBottomOfid/tv_health_label android:layout_marginStart10dp/ EditText android:idid/et_health android:text{viewModel.health.toString()} android:layout_width0dp android:layout_heightwrap_content app:layout_constraintStart_toEndOfid/tv_health app:layout_constraintEnd_toEndOfparent app:layout_constraintTop_toTopOfid/tv_health app:layout_constraintBottom_toBottomOfid/tv_health android:layout_marginStart10dp 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 version1.0 encodingutf-8? layout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto xmlns:toolshttp://schemas.android.com/tools data import typecom.example.mvvmdemo.WuxiaCharacterViewModel / variable nameviewModel typecom.example.mvvmdemo.WuxiaCharacterViewModel/ /data androidx.constraintlayout.widget.ConstraintLayout android:layout_widthmatch_parent android:layout_heightmatch_parent android:padding20dp tools:context.MainActivity !-- 人物名称 -- TextView android:idid/tv_name android:layout_widthwrap_content android:layout_heightwrap_content android:text段正淳 android:textSize36sp android:textStylebold app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toTopOfparent/ !-- 魅力区域 -- TextView android:idid/tv_charm_label android:layout_widthwrap_content android:layout_heightwrap_content android:text魅力 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_name android:layout_marginTop20dp/ !--{} 绑定点击事件-- Button android:idid/btn_charm_minus android:layout_width40dp android:layout_height40dp android:onClick{()-viewModel.decrementCharm()} android:text- app:layout_constraintStart_toEndOfid/tv_charm_label app:layout_constraintTop_toTopOfid/tv_charm_label app:layout_constraintBottom_toBottomOfid/tv_charm_label/ !-- 魅力值 {}是单向绑定即数据变化会刷新UI -- TextView android:idid/tv_charm_value android:layout_width60dp android:layout_heightwrap_content android:text{viewModel.charm.toString()} android:textSize20sp android:gravitycenter app:layout_constraintStart_toEndOfid/btn_charm_minus app:layout_constraintTop_toTopOfid/tv_charm_label app:layout_constraintBottom_toBottomOfid/tv_charm_label/ Button android:idid/btn_charm_plus android:layout_width40dp android:layout_height40dp android:text android:onClick{()-viewModel.incrementCharm()} app:layout_constraintStart_toEndOfid/tv_charm_value app:layout_constraintTop_toTopOfid/tv_charm_label app:layout_constraintBottom_toBottomOfid/tv_charm_label/ !-- 武力区域 -- TextView android:idid/tv_force_label android:layout_widthwrap_content android:layout_heightwrap_content android:text武力 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_charm_label android:layout_marginTop10dp/ Button android:idid/btn_force_minus android:layout_width40dp android:layout_height40dp android:text- android:onClick{()-viewModel.decrementForce()} app:layout_constraintStart_toEndOfid/tv_force_label app:layout_constraintTop_toTopOfid/tv_force_label app:layout_constraintBottom_toBottomOfid/tv_force_label/ TextView android:idid/tv_force_value android:layout_width60dp android:layout_heightwrap_content android:text{viewModel.force.toString()} android:textSize20sp android:gravitycenter app:layout_constraintStart_toEndOfid/btn_force_minus app:layout_constraintTop_toTopOfid/tv_force_label app:layout_constraintBottom_toBottomOfid/tv_force_label/ Button android:idid/btn_force_plus android:layout_width40dp android:layout_height40dp android:text android:onClick{()-viewModel.incrementForce()} app:layout_constraintStart_toEndOfid/tv_force_value app:layout_constraintTop_toTopOfid/tv_force_label app:layout_constraintBottom_toBottomOfid/tv_force_label/ !-- 财富区域 -- TextView android:idid/tv_wealth_label android:layout_widthwrap_content android:layout_heightwrap_content android:text财富 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_force_label android:layout_marginTop10dp/ Button android:idid/btn_wealth_minus android:layout_width40dp android:layout_height40dp android:text- android:onClick{()-viewModel.decrementWealth()} app:layout_constraintStart_toEndOfid/tv_wealth_label app:layout_constraintTop_toTopOfid/tv_wealth_label app:layout_constraintBottom_toBottomOfid/tv_wealth_label/ TextView android:idid/tv_wealth_value android:layout_width100dp android:layout_heightwrap_content android:text{viewModel.formatWealth(viewModel.wealth)} android:textSize20sp android:gravitycenter app:layout_constraintStart_toEndOfid/btn_wealth_minus app:layout_constraintTop_toTopOfid/tv_wealth_label app:layout_constraintBottom_toBottomOfid/tv_wealth_label/ Button android:idid/btn_wealth_plus android:layout_width40dp android:layout_height40dp android:text android:onClick{()-viewModel.incrementWealth()} app:layout_constraintStart_toEndOfid/tv_wealth_value app:layout_constraintTop_toTopOfid/tv_wealth_label app:layout_constraintBottom_toBottomOfid/tv_wealth_label/ !--健康指数-- TextView android:idid/tv_health_label android:layout_widthwrap_content android:layout_heightwrap_content android:text健康 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_wealth_label android:layout_marginTop10dp/ TextView android:idid/tv_health android:layout_widthwrap_content android:layout_heightwrap_content android:text{String.valueOf(viewModel.health)} android:textSize20sp android:gravitycenter app:layout_constraintStart_toEndOfid/tv_health_label app:layout_constraintTop_toTopOfid/tv_health_label app:layout_constraintBottom_toBottomOfid/tv_health_label android:layout_marginStart10dp/ EditText android:idid/et_health android:inputTypenumber android:text{String.valueOf(viewModel.health)} android:onTextChanged{(s, start, before, count) - viewModel.onHealthTextChanged(s)} android:layout_width0dp android:layout_heightwrap_content app:layout_constraintStart_toEndOfid/tv_health app:layout_constraintEnd_toEndOfparent app:layout_constraintTop_toTopOfid/tv_health app:layout_constraintBottom_toBottomOfid/tv_health android:layout_marginStart10dp android:hint请修改健康指数 / !-- 武功 -- TextView android:idid/tv_skill android:layout_widthwrap_content android:layout_heightwrap_content android:text武功一阳指 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_health_label android:layout_marginTop10dp/ !-- 介绍 -- TextView android:idid/tv_intro android:layout_widthwrap_content android:layout_heightwrap_content android:text介绍少女收割机 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_skill android:layout_marginTop10dp/ /androidx.constraintlayout.widget.ConstraintLayout /layoutviewModel 增加了一个onHealthTextChanged函数实现方向绑定代码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 MutableLiveDataInt(99) // MutableLiveData可变。 这里只在内部修改数据 val charm: LiveDataInt _charm // LiveData 不可变。 暴露给外部只读 // 武力值初始85 private val _force MutableLiveDataInt(85) val force: LiveDataInt _force // 财富值初始1亿用Long存储 private val _wealth MutableLiveDataLong(100000000) val wealth: LiveDataLong _wealth // 健康指数初始99 val health MutableLiveDataInt(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 MutableLiveDataBoolean(false) // val isLoading: LiveDataBoolean _isLoading // 错误信息新增请求失败时提示用户 // private val _errorMsg MutableLiveDataString() // val errorMsg: LiveDataString _errorMsg fun refreshCharacterData() { // 模拟从服务端获取数据 // 显示加载状态UI会感知到显示加载动画 // _isLoading.value true // _errorMsg.value // viewModelScope启动协程自动跟随ViewModel生命周期避免内存泄漏 // viewModelScope.launch { // // 调用model层代码获取数据。。。 // } } }MainActivity 延迟2每秒修改健康值测试下输入框的数字是否改变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) // 注册LifecycleObserverActivity是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类型和输入框需要的数据类型一致。
Android databinding
上一个MVVM demo改成用databinding绑定数据测试下。app/build.gradle.kts 启用dataBinding布局修改下最外层用layout标签data标签设置变量。绑定数据和点击事件。{}是单项绑定。即数据变化会自动刷新UI。?xml version1.0 encodingutf-8? layout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto xmlns:toolshttp://schemas.android.com/tools data variable nameviewModel typecom.example.mvvmdemo.WuxiaCharacterViewModel/ /data androidx.constraintlayout.widget.ConstraintLayout android:layout_widthmatch_parent android:layout_heightmatch_parent android:padding20dp tools:context.MainActivity !-- 人物名称 -- TextView android:idid/tv_name android:layout_widthwrap_content android:layout_heightwrap_content android:text段正淳 android:textSize36sp android:textStylebold app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toTopOfparent/ !-- 魅力区域 -- TextView android:idid/tv_charm_label android:layout_widthwrap_content android:layout_heightwrap_content android:text魅力 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_name android:layout_marginTop20dp/ !--{} 绑定点击事件-- Button android:idid/btn_charm_minus android:layout_width40dp android:layout_height40dp android:onClick{()-viewModel.decrementCharm()} android:text- app:layout_constraintStart_toEndOfid/tv_charm_label app:layout_constraintTop_toTopOfid/tv_charm_label app:layout_constraintBottom_toBottomOfid/tv_charm_label/ !-- 魅力值 {}是单向绑定即数据变化会刷新UI -- TextView android:idid/tv_charm_value android:layout_width60dp android:layout_heightwrap_content android:text{viewModel.charm.toString()} android:textSize20sp android:gravitycenter app:layout_constraintStart_toEndOfid/btn_charm_minus app:layout_constraintTop_toTopOfid/tv_charm_label app:layout_constraintBottom_toBottomOfid/tv_charm_label/ Button android:idid/btn_charm_plus android:layout_width40dp android:layout_height40dp android:text android:onClick{()-viewModel.incrementCharm()} app:layout_constraintStart_toEndOfid/tv_charm_value app:layout_constraintTop_toTopOfid/tv_charm_label app:layout_constraintBottom_toBottomOfid/tv_charm_label/ !-- 武力区域 -- TextView android:idid/tv_force_label android:layout_widthwrap_content android:layout_heightwrap_content android:text武力 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_charm_label android:layout_marginTop10dp/ Button android:idid/btn_force_minus android:layout_width40dp android:layout_height40dp android:text- android:onClick{()-viewModel.decrementForce()} app:layout_constraintStart_toEndOfid/tv_force_label app:layout_constraintTop_toTopOfid/tv_force_label app:layout_constraintBottom_toBottomOfid/tv_force_label/ TextView android:idid/tv_force_value android:layout_width60dp android:layout_heightwrap_content android:text{viewModel.force.toString()} android:textSize20sp android:gravitycenter app:layout_constraintStart_toEndOfid/btn_force_minus app:layout_constraintTop_toTopOfid/tv_force_label app:layout_constraintBottom_toBottomOfid/tv_force_label/ Button android:idid/btn_force_plus android:layout_width40dp android:layout_height40dp android:text android:onClick{()-viewModel.incrementForce()} app:layout_constraintStart_toEndOfid/tv_force_value app:layout_constraintTop_toTopOfid/tv_force_label app:layout_constraintBottom_toBottomOfid/tv_force_label/ !-- 财富区域 -- TextView android:idid/tv_wealth_label android:layout_widthwrap_content android:layout_heightwrap_content android:text财富 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_force_label android:layout_marginTop10dp/ Button android:idid/btn_wealth_minus android:layout_width40dp android:layout_height40dp android:text- android:onClick{()-viewModel.decrementWealth()} app:layout_constraintStart_toEndOfid/tv_wealth_label app:layout_constraintTop_toTopOfid/tv_wealth_label app:layout_constraintBottom_toBottomOfid/tv_wealth_label/ TextView android:idid/tv_wealth_value android:layout_width100dp android:layout_heightwrap_content android:text{viewModel.formatWealth(viewModel.wealth)} android:textSize20sp android:gravitycenter app:layout_constraintStart_toEndOfid/btn_wealth_minus app:layout_constraintTop_toTopOfid/tv_wealth_label app:layout_constraintBottom_toBottomOfid/tv_wealth_label/ Button android:idid/btn_wealth_plus android:layout_width40dp android:layout_height40dp android:text android:onClick{()-viewModel.incrementWealth()} app:layout_constraintStart_toEndOfid/tv_wealth_value app:layout_constraintTop_toTopOfid/tv_wealth_label app:layout_constraintBottom_toBottomOfid/tv_wealth_label/ !-- 武功 -- TextView android:idid/tv_skill android:layout_widthwrap_content android:layout_heightwrap_content android:text武功一阳指 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_wealth_label android:layout_marginTop10dp/ !-- 介绍 -- TextView android:idid/tv_intro android:layout_widthwrap_content android:layout_heightwrap_content android:text介绍少女收割机 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_skill android:layout_marginTop10dp/ /androidx.constraintlayout.widget.ConstraintLayout /layout修改MainActivitypackage 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) // 注册LifecycleObserverActivity是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显示健康指数旁边一个输入框可以修改值并双向绑定数据!--健康指数-- TextView android:idid/tv_health_label android:layout_widthwrap_content android:layout_heightwrap_content android:text健康 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_wealth_label android:layout_marginTop10dp/ TextView android:idid/tv_health android:layout_widthwrap_content android:layout_heightwrap_content android:text{viewModel.health.toString()} android:textSize20sp android:gravitycenter app:layout_constraintStart_toEndOfid/tv_health_label app:layout_constraintTop_toTopOfid/tv_health_label app:layout_constraintBottom_toBottomOfid/tv_health_label android:layout_marginStart10dp/ EditText android:idid/et_health android:text{viewModel.health.toString()} android:layout_width0dp android:layout_heightwrap_content app:layout_constraintStart_toEndOfid/tv_health app:layout_constraintEnd_toEndOfparent app:layout_constraintTop_toTopOfid/tv_health app:layout_constraintBottom_toBottomOfid/tv_health android:layout_marginStart10dp 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 version1.0 encodingutf-8? layout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto xmlns:toolshttp://schemas.android.com/tools data import typecom.example.mvvmdemo.WuxiaCharacterViewModel / variable nameviewModel typecom.example.mvvmdemo.WuxiaCharacterViewModel/ /data androidx.constraintlayout.widget.ConstraintLayout android:layout_widthmatch_parent android:layout_heightmatch_parent android:padding20dp tools:context.MainActivity !-- 人物名称 -- TextView android:idid/tv_name android:layout_widthwrap_content android:layout_heightwrap_content android:text段正淳 android:textSize36sp android:textStylebold app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toTopOfparent/ !-- 魅力区域 -- TextView android:idid/tv_charm_label android:layout_widthwrap_content android:layout_heightwrap_content android:text魅力 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_name android:layout_marginTop20dp/ !--{} 绑定点击事件-- Button android:idid/btn_charm_minus android:layout_width40dp android:layout_height40dp android:onClick{()-viewModel.decrementCharm()} android:text- app:layout_constraintStart_toEndOfid/tv_charm_label app:layout_constraintTop_toTopOfid/tv_charm_label app:layout_constraintBottom_toBottomOfid/tv_charm_label/ !-- 魅力值 {}是单向绑定即数据变化会刷新UI -- TextView android:idid/tv_charm_value android:layout_width60dp android:layout_heightwrap_content android:text{viewModel.charm.toString()} android:textSize20sp android:gravitycenter app:layout_constraintStart_toEndOfid/btn_charm_minus app:layout_constraintTop_toTopOfid/tv_charm_label app:layout_constraintBottom_toBottomOfid/tv_charm_label/ Button android:idid/btn_charm_plus android:layout_width40dp android:layout_height40dp android:text android:onClick{()-viewModel.incrementCharm()} app:layout_constraintStart_toEndOfid/tv_charm_value app:layout_constraintTop_toTopOfid/tv_charm_label app:layout_constraintBottom_toBottomOfid/tv_charm_label/ !-- 武力区域 -- TextView android:idid/tv_force_label android:layout_widthwrap_content android:layout_heightwrap_content android:text武力 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_charm_label android:layout_marginTop10dp/ Button android:idid/btn_force_minus android:layout_width40dp android:layout_height40dp android:text- android:onClick{()-viewModel.decrementForce()} app:layout_constraintStart_toEndOfid/tv_force_label app:layout_constraintTop_toTopOfid/tv_force_label app:layout_constraintBottom_toBottomOfid/tv_force_label/ TextView android:idid/tv_force_value android:layout_width60dp android:layout_heightwrap_content android:text{viewModel.force.toString()} android:textSize20sp android:gravitycenter app:layout_constraintStart_toEndOfid/btn_force_minus app:layout_constraintTop_toTopOfid/tv_force_label app:layout_constraintBottom_toBottomOfid/tv_force_label/ Button android:idid/btn_force_plus android:layout_width40dp android:layout_height40dp android:text android:onClick{()-viewModel.incrementForce()} app:layout_constraintStart_toEndOfid/tv_force_value app:layout_constraintTop_toTopOfid/tv_force_label app:layout_constraintBottom_toBottomOfid/tv_force_label/ !-- 财富区域 -- TextView android:idid/tv_wealth_label android:layout_widthwrap_content android:layout_heightwrap_content android:text财富 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_force_label android:layout_marginTop10dp/ Button android:idid/btn_wealth_minus android:layout_width40dp android:layout_height40dp android:text- android:onClick{()-viewModel.decrementWealth()} app:layout_constraintStart_toEndOfid/tv_wealth_label app:layout_constraintTop_toTopOfid/tv_wealth_label app:layout_constraintBottom_toBottomOfid/tv_wealth_label/ TextView android:idid/tv_wealth_value android:layout_width100dp android:layout_heightwrap_content android:text{viewModel.formatWealth(viewModel.wealth)} android:textSize20sp android:gravitycenter app:layout_constraintStart_toEndOfid/btn_wealth_minus app:layout_constraintTop_toTopOfid/tv_wealth_label app:layout_constraintBottom_toBottomOfid/tv_wealth_label/ Button android:idid/btn_wealth_plus android:layout_width40dp android:layout_height40dp android:text android:onClick{()-viewModel.incrementWealth()} app:layout_constraintStart_toEndOfid/tv_wealth_value app:layout_constraintTop_toTopOfid/tv_wealth_label app:layout_constraintBottom_toBottomOfid/tv_wealth_label/ !--健康指数-- TextView android:idid/tv_health_label android:layout_widthwrap_content android:layout_heightwrap_content android:text健康 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_wealth_label android:layout_marginTop10dp/ TextView android:idid/tv_health android:layout_widthwrap_content android:layout_heightwrap_content android:text{String.valueOf(viewModel.health)} android:textSize20sp android:gravitycenter app:layout_constraintStart_toEndOfid/tv_health_label app:layout_constraintTop_toTopOfid/tv_health_label app:layout_constraintBottom_toBottomOfid/tv_health_label android:layout_marginStart10dp/ EditText android:idid/et_health android:inputTypenumber android:text{String.valueOf(viewModel.health)} android:onTextChanged{(s, start, before, count) - viewModel.onHealthTextChanged(s)} android:layout_width0dp android:layout_heightwrap_content app:layout_constraintStart_toEndOfid/tv_health app:layout_constraintEnd_toEndOfparent app:layout_constraintTop_toTopOfid/tv_health app:layout_constraintBottom_toBottomOfid/tv_health android:layout_marginStart10dp android:hint请修改健康指数 / !-- 武功 -- TextView android:idid/tv_skill android:layout_widthwrap_content android:layout_heightwrap_content android:text武功一阳指 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_health_label android:layout_marginTop10dp/ !-- 介绍 -- TextView android:idid/tv_intro android:layout_widthwrap_content android:layout_heightwrap_content android:text介绍少女收割机 android:textSize20sp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toBottomOfid/tv_skill android:layout_marginTop10dp/ /androidx.constraintlayout.widget.ConstraintLayout /layoutviewModel 增加了一个onHealthTextChanged函数实现方向绑定代码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 MutableLiveDataInt(99) // MutableLiveData可变。 这里只在内部修改数据 val charm: LiveDataInt _charm // LiveData 不可变。 暴露给外部只读 // 武力值初始85 private val _force MutableLiveDataInt(85) val force: LiveDataInt _force // 财富值初始1亿用Long存储 private val _wealth MutableLiveDataLong(100000000) val wealth: LiveDataLong _wealth // 健康指数初始99 val health MutableLiveDataInt(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 MutableLiveDataBoolean(false) // val isLoading: LiveDataBoolean _isLoading // 错误信息新增请求失败时提示用户 // private val _errorMsg MutableLiveDataString() // val errorMsg: LiveDataString _errorMsg fun refreshCharacterData() { // 模拟从服务端获取数据 // 显示加载状态UI会感知到显示加载动画 // _isLoading.value true // _errorMsg.value // viewModelScope启动协程自动跟随ViewModel生命周期避免内存泄漏 // viewModelScope.launch { // // 调用model层代码获取数据。。。 // } } }MainActivity 延迟2每秒修改健康值测试下输入框的数字是否改变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) // 注册LifecycleObserverActivity是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类型和输入框需要的数据类型一致。