1、简介
MVVM将 MVP中的Presenter改为ViewModel,类似于MVP模式,不同的是ViewModel跟View和Model进行双向绑定:当View发生改变时,ViewModel通知Model进行更新数据;同理Model数据更新后,ViewModel通知View更新。
MVVM的结构如下图:
MVVM是一种模式,而DataBinding是一个框架,它是实现MVVM模式的一个工具,而MVVM模式中的ViewModel和View可以通过DataBinding来实现双向绑定。
2、DataBinding的使用
在DataBinding之前,我们需要大量的findViewById()、setText()和setOnClickListener()代码。通过Databinding,我们可以以在布局文件中进行逻辑和视图的绑定,这样就可以省略大量的模板代码。要想使用DataBinding需要在Module的gradle中进行如下配置:
android{
dataBinding {
enabled true
}
}
2.1、DataBinding的基础使用
首先创建一个Model类
data class Student(val name: String, val age: Int)
下面编写xml文件
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="student"
type="com.example.mvvmtest.bean.Student" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{student.name}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(student.age)}" />
</LinearLayout>
</layout>
根节点是一个layout
,layout中包含了data节点和传统的视图,在data中定义了variable节点,name表示变量的名称,type表示变量的类型。variable
中定义的每一个变量,都会在DataBinding辅助类中生成getter和setter方法,我们可以使用DataBinding辅助类对象调用响应的set方法进行赋值。接着使用@{student.name}
和@{String.valueOf(student.age)}
给text进行赋值。最后在MainActivity中进行实体类和布局的绑定。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
val stu = Student("肥肥", 300)
binding.student = stu
}
}
2.2、事件的处理
事件的处理有两种方式:
- 方式一:
在xml中定义一个Button
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!--..... -->
<Button
android:layout_width="match_parent"
android:layout_height="44dp"
android:text="点击"
android:id="@+id/btn_click"/>
<!--..... -->
</layout>
在MainActivity中调用它
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
//....
binding.btnClick.setOnClickListener {
"按钮被点击了".showToast(this)
}
}
}
btnClick
就是id为btn_click
的Button,当我们给控件每设定一个id,就会在binding的辅助类中生成一个响应的public final字段,以供调用。
-
方式二
在xml中定义一个名为mOnclickListener
,类型为android.view.View.OnClickListener
的变量,给Button的onClick
属性赋值为@{mOnclickListener}
。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="mOnclickListener"
type="android.view.View.OnClickListener" />
</data>
....
<Button
android:layout_width="match_parent"
android:layout_height="44dp"
android:text="点击"
android:onClick="@{mOnclickListener}"/>
....
</layout>
在MainActivity中调用如下:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.setMOnclickListener {
"按钮被点击了".showToast(this)
}
}
}
- 方式三
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.databindingmodule.MainViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:layout_width="match_parent"
android:layout_height="50dp"
android:onClick="@{v->viewModel.openActivity(v)}"
android:text="打开新的页面" />
</LinearLayout>
</layout>
这样在点击按钮的时候就能回调MainViewModel中的方法openActivity(v)
class MainViewModel {
fun openActivity(v:View) {
weakContext?.get()?.let {
val intent = Intent(it, JetpackActivity::class.java)
it.startActivity(intent)
}
}
}
2.3、import的使用
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View.OnClickListener"/>
<import type="com.example.mvvmtest.bean.Student"/>
<variable
name="student"
type="Student" />
<variable
name="mOnclickListener"
type="OnClickListener" />
</data>
</layout>
2.4、变量的定义
上面我们学习了如何在XML中定义实体类,下面我们看下基本类型变量和集合如何在XML中定义。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<!--基本类型的定义-->
<variable
name="name"
type="String" />
<variable
name="age"
type="int" />
<variable
name="isMan"
type="boolean" />
<!--集合的定义-->
<variable
name="list"
type="java.util.ArrayList<String>" />
<variable
name="map"
type="java.util.HashMap<String,String>" />
<variable
name="arrays"
type="String[]" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{name}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(age)}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(isMan)}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{list.get(0)}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{map.get(`key`)}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{arrays[0]}" />
</LinearLayout>
</layout>
在MainActivity中调用
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.name = "LILEI"
binding.age = 100
binding.isMan = true
val list = ArrayList<String>()
list.add("list one")
binding.list = list
val hashMap = HashMap<String, String>()
hashMap["key"] = "map"
binding.map = hashMap
val array = arrayOf("数组")
binding.arrays = array
}
}
2.5、静态方法的使用
在布局文件中调用静态方法可以实现数据转换的效果,首先定义一个静态方法
class Utils {
companion object {
@JvmStatic
fun getName(stu: Student) = stu.name
}
}
然后在布局文件中引入我们使用的类
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="student"
type="com.example.mvvmtest.bean.Student" />
<import type="com.example.mvvmtest.util.Utils" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{Utils.getName(student)}" />
</LinearLayout>
</layout>
在TextView中我们调用了静态方法@{Utils.getName(student)}
设置文本,所以需要在Activity中传入Student对象
val stu = Student("王晶1", 1400)
binding.student = stu
2.6、支持表达式
在XML中支持如下表达式
- 数学表达式:+-*/%
- 字符串拼接:+
- 逻辑表达式:&& ||
- 位操作符:& | ^
- 位移操作符:>> >>> <<
- 比较操作符:== > < >= <=
- instanceof
- 强转 方法调用
- 字段访问
- 三元操作符:?:
下面举个简单的例子
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{`Age is `+String.valueOf(student.age)}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="显示与否"
android:visibility="@{booleanValue?View.VISIBLE:View.GONE}" />
3、动态更新
前面学习的Databinding的例子中,如果实体类的内容发生了改变,那么UI界面是不会动态更新的。Databinding提供了三种动态更新机制,根据Model实体类的内容动态更新UI,分别是类(Observable)、字段(ObservableField)、集合类型(Observable容器类),下面我们逐一分析:
3.1、使用Observable
创建实体类继承BaseObservable
来实现动态更新(Kotlin中不知到如何实现,以后学习到了再说)
public class Student extends BaseObservable {
private String name;
private int age;
@Bindable
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name);
}
@Bindable
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
notifyPropertyChanged(BR.age);
}
}
在getter方法上使用注解@Bindable
,在setter中通知更新就可以了。其中BR
是编译时生成的类,用@Bindable标记过的getter方法会在BR中生成一个相应的字段。在setter中调用notifyPropertyChanged(BR.age)
通知BR.age这个字段数据发生变化并更新UI。我们在Activity中使用如下:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
val stu = Student()
stu.name = "王晶"
stu.age = 120
binding.student = stu
binding.setMOnClickListener {
when (it.id) {
//更新Model中的数据,UI也会相应的更新
R.id.btn_update-> stu.age = 100
}
}
}
3.2、使用ObservableField
除了使用继承BaseObservable来实现动态更新外,还可以使用基本数据类型对应的Observable类,比如ObservableInt、ObservableFloat、ObservableBoolean等。又或者使用基本数据类型和引用数据类型通用的ObservableField类,它们都继承自BaseObservable,现在以ObservableField类为例:
public class Teacher {
private ObservableField<String> t_name =new ObservableField<String>();
private ObservableField<Integer> t_age=new ObservableField<>();
public ObservableField<String> getT_name() {
return t_name;
}
public void setT_name(ObservableField<String> t_name) {
this.t_name = t_name;
}
public ObservableField<Integer> getT_age() {
return t_age;
}
public void setT_age(ObservableField<Integer> t_age) {
this.t_age = t_age;
}
}
然后与布局文件进行绑定
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="mOnClickListener"
type="android.view.View.OnClickListener" />
<variable
name="teacher"
type="com.example.mvvmtest.bean.Teacher" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:id="@+id/btn_update"
android:layout_width="match_parent"
android:layout_height="44dp"
android:onClick="@{mOnClickListener}"
android:text="更新数据"
android:textAllCaps="false" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{teacher.t_name}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(teacher.t_age)}" />
</LinearLayout>
</layout>
最后在Activity中更新数据
val teacher=Teacher()
teacher.t_name.set("勺勺")
teacher.t_age.set(23)
binding.teacher=teacher
binding.setMOnClickListener {
when (it.id) {
R.id.btn_update-> {
teacher.t_age.set(1000)
}
}
}
3.3、使用Observable容器类
如果有多个Swordman类型的数据要更新,就需要使用Observable容器类ObservableArrayList和ObservableArrayMap
,这时无需创建符合更新机制的Model实体类,只需要在代码中应用ObservableArrayList即可。代码如下:
data class Swordsman(var name:String,var level:String)
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
val swordsmanList=ObservableArrayList<Swordsman>()
val swordsman1=Swordsman("张无忌","S")
val swordsman2=Swordsman("周芷若","B")
swordsmanList.add(swordsman1)
swordsmanList.add(swordsman2)
binding.swordmanList=swordsmanList
binding.setMOnClickListener {
when (it.id) {
R.id.btn_update-> {
swordsman1.level="S++++"
//记得调用swordsmanList.add()否则不能更新
swordsmanList.add(swordsman1)
}
}
}
然后绑定XML布局文件
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.example.mvvmtest.bean.Swordsman"/>
<variable
name="swordmanList"
type="androidx.databinding.ObservableArrayList<Swordsman>" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{swordmanList.get(0).level}" />
</LinearLayout>
</layout>
4、双向绑定
从MVVM模式的角度来讲,双向绑定就是Model和View通过ViewModel进行双向动态更新。前面讲了Model变化,UI会动态更新,反过来如果View变化,Model该如何自动更新呢?下面举个简单的例子来实现双向绑定,双向绑定的前提是需要结合动态更新机制。
使用ObservableField创建Model实体类并实现动态更新机制
public class Man {
public ObservableField<String> name=new ObservableField<>();
public ObservableField<String> getName() {
return name;
}
public void setName(ObservableField<String> name) {
this.name = name;
}
}
然后在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="man"
type="com.example.mvvmtest.bean.Man" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="@{man.name}" />
<EditText
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="20dp"
android:text="@={man.name}" />
<Button
android:id="@+id/btn_reset"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="重置"
android:layout_marginTop="20dp" />
</LinearLayout>
</layout>
在布局文件中定义EditText来改变Man中的name字段,关键是将@{man.name}改成了@={man.name}
,定义TextView来动态显示Man中name的动态变化。
在Activity中完成调用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityBindingsBinding viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_bindings);
Man man=new Man();
man.name.set("任我行");
viewDataBinding.setMan(man);
viewDataBinding.btnReset.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View view) {
man.getName().set("田玉琴");
}
});
}
运行程序你会发现,当EditText中的文字发生变化时,TextView中的内容也会做相应的变化,当通过按钮进行重置Man中的name时EditText和Text也会变化,这样就实现了一个双向绑定。
5、常用注解
5.1、@BindAdapter
@BindAdapter
注解的作用是扩展属性。在xml中的控件属性不能满足时 ,可以使用此注解进行属性的扩展。比如:我们在给ImageView加载网络图片时,ImageView原有的属性是无法满足的。下面举例说明如何使用注解进行属性的扩展。
- 首先创建一个名为ViewBindingAdapter的文件,在其中定义一个顶层方法
@BindingAdapter("imgUrl")
fun loadPicForImgView(iv:ImageView,url:String){
Glide.with(view).load(url).into(iv)
}
然后在xml布局中增加一个ImageView控件
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
imgUrl="@{student.imgUrl}"/>
注意:@BindingAdapter
注解修饰的方法必须是静态方法,而且方法中的第一个参数必须是需扩展属性的空间,所以这里传入了ImageView。
@BindingAdapter注解中传入的有两个参数:
- 第一个参数是字符串数组,代表的要扩展的属性,如果只需扩展一个属性,那么只需传一个字符串即可。如果需要扩展多个属性,则传入一个字符串数组,此时就需要第二个参数了。
- 第二个参数是一个boolean类型的,为true则表示第一个参数中所有属性都必须在控件中设置,否则则不需要。
// 当为false时
@BindingAdapter(value = ["imgUrl", "bgRes"], requireAll = false)
fun setImgUrl(view: ImageView, url: String, res: Int) {
Glide.with(view).load(url).into(view)
view.setBackgroundResource(res)
}
<!-- xml文件代码 -->
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
imgUrl="@{viewModel.imgUrl}"/>
// 当为true时
@BindingAdapter(value = ["imgUrl", "bgRes"], requireAll = true)
fun setImgUrl(view: ImageView, url: String, res: Int) {
Glide.with(view).load(url).into(view)
view.setBackgroundResource(res)
}
<!-- xml文件代码 -->
<!-- 这段请放在data标签中 -->
<import type="top.cyixlq.test.R"/>
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
imgUrl="@{viewModel.imgUrl}"
bgRes="@{R.mipmap.ic_launcher}"/>
5.2、@BindingConversion
将不符合控件属性的值的类型转换成符合的类型。比如Text的text需要的是String类型的,但是设置给text的却是Long型的,这时候就可以使用注解进行转换了。具体使用如下:
首先创建一个静态方法,并用注解修饰
@SuppressLint("SimpleDateFormat")
@BindingConversion
fun convertIntToString(value: Long): String {
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
return formatter.format(value)
}
然后在XML文件中TextView的text传入Long型
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{System.currentTimeMillis()}"/>
这样就能将Long型转换成String类型了。但是需要注意的是,一旦使用@BindingConversion注解,那么所有需要Long型转换成String类型的控件,都会生效,这样使用起来就不太灵活,我们完全可以使用静态方法来代替。
5.3、@InverseMethod
在开发中我们遇到某个数字或字符串代表某种状态。比如1代表男,2代表女,在用户选择好之后,在回传到后台时还需要进行手动转换,如果不想手动转换就可以使用这个注解。下面看下具体的使用:
// 这个注解参数为反转的方法名,意味着一个这个注解需要两个方法才能完成
@InverseMethod("sexToNum")
fun numToSex(num: Int): String {
return when (num) {
0 -> "女"
1 -> "男"
else -> "未知性别"
}
}
fun sexToNum(sex: String): Int {
return when(sex) {
"女" ->0
"男" -> 1
else -> 2
}
}
这个注解主要用于双向绑定时使用,所以需要定义两个方法,需要有来回转换的两个方法。而且要转换的数据需要具有动态更新的能力。
在布局文件中添加一个TextView和一个EditText,TextView用来观察num这个值的变化,EditText用来展示转换好之后的数据:
<!-- 这里别忘了导入,导入了下面的EditText才能用这个类 -->
<import type="top.cyixlq.test.ViewInverseMethodsKt"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(viewModel.num)}"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={ViewInverseMethodsKt.numToSex(viewModel.num)}"/>
这时候编译运行,EditText直接显示了男,如果我们删除男,EditText立马显示了未知性别。因为空字符串会对应到sexToNum中的else,所以num的值瞬间变成2,而2又对应numToSex中的else,所以EditText就会显示未知性别。当我们把未知性别几个字全部删除,输入1,或者2也好,num都是2,因为字符串-"1",字符串-"2"都是对应sexToNum方法中的else。但是如果我们在输入框中输入男,num就变成1了,输入女num就变成0了。
6、在RecyclerView中的使用
DataBinding除了在在Activity中使用,还可以在RecyclerView中使用。具体使用如下:
首先创建Activity的布局文件
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recy_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</layout>
给RecyclerView设定了id,这样就可以在Data Binding来使用RecyclerView的控件了。接着定义Item的布局文件
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="student"
type="com.example.jetpacktest.Student" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="120dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center"
android:text="@{student.name}" />
<TextView
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center"
android:text="@{String.valueOf(student.age)}" />
</LinearLayout>
</layout>
下面看下Adapter
class MyAdapter(val context: Context, var mData: ArrayList<Student>) :
RecyclerView.Adapter<MyAdapter.MyHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyHolder {
val binding = DataBindingUtil.inflate<ItemListBinding>(
LayoutInflater.from(context),
R.layout.item_list,
parent,
false
)
return MyHolder(binding)
}
override fun onBindViewHolder(holder: MyHolder, position: Int) {
holder.binding.student = mData[position]
holder.itemView.setOnClickListener {
mData[position].name = "张无忌${Random.nextInt(1, 1000)}"
mData[position].age = Random.nextInt(1, 1000)
}
}
override fun getItemCount(): Int = mData.size
inner class MyHolder(val binding: ItemListBinding) : RecyclerView.ViewHolder(binding.root)
}
和普通Adapter使用的不同的是:创建ViewHolder时并没有传入View而是传入了ItemListBinding,在ViewHolder中我们没有通过findViewById查找相应的控件,而是通过返回ItemListBinding来实现控件的查找。
接着看下在Activity中的使用:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.recyList.layoutManager = LinearLayoutManager(this)
binding.recyList.adapter = MyAdapter(this,initDataList())
}
private fun initDataList(): ArrayList<Student> {
val mDataList = ArrayList<Student>()
for (i in 0 until 50) {
val stu = Student("姓名$i", i)
mDataList.add(stu)
}
return mDataList
}
}
到此为止MVVM的支持库Data Binding就讲完了。
网友评论