在之前的章节中,已经介绍过JectPack
的两个组件,都与生命周期的感知有关----Lifecycle
+ LiveData
,那么本节将会介绍跟数据绑定有关的两个组件DataBinding
+ ViewModel
,从而引出MVVM架构的设计实现。
1、MVVM介绍
跟MVP不同的是,在MVP的P层中,主要做逻辑处理,像请求数据等;在MVVM中,是通过ViewModel代替了P层,ViewModel实现了View层和Model层的双向绑定,数据的绑定就是通过DataBinding这个组件实现的。
首先,要想使用MVVM,那么就要你的项目支持DataBinding,在build.gradle中,添加一行代码
dataBinding{
enabled true
}
2、DataBinding
+ ViewModel
一般来说,在使用MVVM架构的时候,更多的是在做XML布局和DataBean,一个布局对应一个页面,该页面的数据来源,通过ViewModel来提供,使用DataBinding与数据源绑定,因此在XML布局文件中,要声明该页面的数据来源。
(1)XML布局:在XML布局中,需要设置layout
、data
标签,layout
标签用来包裹整体的布局,data
则是声明在该布局中使用到的DataBean
类,如果有多个DataBean
,那么就设置多个variable
。
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
//这个databean的别名
name="user"
//databean的全类名
type="com.example.mvvm.User" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.username}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.password}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
</layout>
(2)DataBean:在MVVM中,就是ViewModel层,跟之前的DataBean不同的是,这个DataBean类所要做的工作可是很多的,一方面需要监听数据是否变化,然后将数据更新在XML布局文件;另一方面,也会监听XML布局文件中数据的变化,去更新数据库数据,这就是双向绑定。
/**
* ViewModel
*/
public class User extends BaseObservable {
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
@Bindable
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
notifyPropertyChanged(BR.username);
}
@Bindable
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
notifyPropertyChanged(BR.password);
}
}
DataBean
要继承BaseObservable
就是一个被观察者,XML布局会查询数据,然后在get
方法需要加 @Bindable
注解,获取当前的返回数据值,与XML布局中的android:text="@{user.username}"
绑定;当数据源的数据发生变化时,在set方法中,加入notifyPropertyChanged(BR.user);
属性值变化,BR.user
就是在布局文件中设置的DataBean的别名,这样在数据发生变化时,就会同步更新到手机界面。
在完成View
层和ViewModel
层的基本工作完成之后,要在Activity
中完成DataBinding
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main);
ActivityMainBinding mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
//从Model层来
user = new User("kobe","123456");
mainBinding.setUser(user);
通过DataBindingUtil
加载布局,会根据当前Activity的名字生成一个ActivityMainBinding
类,一般来说,我们在Model层会进行网路请求,或者数据库请求,会将数据回调到View层界面,在View层做数据调用更新UI,然后在XML布局中已经设置了DataBean的种类,所以通过ActivityMainBinding
来去设置数据,就可以将数据更新在界面。
当网路数据源发生更新之后,也会同步更新在与之绑定的布局文件上。
如果是加载一张图片到ImageView
,可以通过自定义属性,来将图片加载到界面。
//自定义属性
@BindingAdapter("bind:header")
public static void getImage(ImageView view,String url){
Glide.with(view.getContext()).load(url).into(view);
}
public String getHeader() {
return header;
}
public void setHeader(String header) {
this.header = header;
}
public User(String username, String password,String header) {
this.username = username;
this.password = password;
this.header = header;
}
然后在布局文件中:
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
bind:header="@{user.header}"></ImageView>
其中内部的原理就是:在加载布局时,因为layout
和data
标签,会分割为两部分,其中一部分,将这些@属性转换为tag
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="binding_1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="binding_2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="binding_3" ></ImageView>
一些Tag的集合,然后在编译时生成的代表该Activity的DataBinding的实现类,从这些tags集合中,取出实例化,来取代findViewById
:
this.mboundView0 = (android.widget.LinearLayout) bindings[0];
this.mboundView0.setTag(null);
this.mboundView1 = (android.widget.TextView) bindings[1];
this.mboundView1.setTag(null);
this.mboundView2 = (android.widget.TextView) bindings[2];
this.mboundView2.setTag(null);
this.mboundView3 = (android.widget.ImageView) bindings[3];
this.mboundView3.setTag(null);
setRootTag(root);
然后,通过获取ViewModel中的数据,将数据填充到视图中。
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
com.example.mvvm.User user = mUser;
java.lang.String userHeader = null;
java.lang.String userUsername = null;
java.lang.String userPassword = null;
if ((dirtyFlags & 0xfL) != 0) {
if ((dirtyFlags & 0x9L) != 0) {
if (user != null) {
// read user.header
userHeader = user.getHeader();
}
}
if ((dirtyFlags & 0xbL) != 0) {
if (user != null) {
// read user.username
userUsername = user.getUsername();
}
}
if ((dirtyFlags & 0xdL) != 0) {
if (user != null) {
// read user.password
userPassword = user.getPassword();
}
}
}
// batch finished
if ((dirtyFlags & 0xbL) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, userUsername);
}
if ((dirtyFlags & 0xdL) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView2, userPassword);
}
if ((dirtyFlags & 0x9L) != 0) {
// api target 1
com.example.mvvm.User.getImage(this.mboundView3, userHeader);
}
}
除了上述的方式之外,在项目开发中,使用最多的,还是列表,无论是ListView,还是RecyclerView…涉及到适配器的有很多,我这里简单地用ListView介绍一下,其他的在后续项目中,如果用到,就再解释。
使用ListView时,最重要的还是数据(List数据),因此在数据源中,需要自定义属性,来得到所要展示的List数据。
public class ListBean {
private List<User> list;
public ListBean(List<User> list) {
this.list = list;
}
public List<User> getList() {
return list;
}
public void setList(List<User> list) {
this.list = list;
}
@BindingAdapter("app:list")
public static void getAdapter(ListView view,List<User> list){
view.setAdapter(new ListViewAdapter(view.getContext(),list));
}
}
两个参数ListView和其对应的参数,使用BindingAdapter
注解,然后在布局文件中声明数据源。
<variable
name="rank"
type="com.example.mvvm.ListBean" />
<ListView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:list="@{rank.list}"></ListView>
在Activity中,进行数据绑定。
User user1 = new User();
user1.setUsername("lili");
User user2 = new User();
user2.setUsername("lili");
List<User> datas = new ArrayList<>();
datas.add(user1);
datas.add(user2);
ListBean bean = new ListBean(datas);
mainBinding.setRank(bean);
在ListView的适配器中,同样也使用DataBinding,这个使用就比较简单了。
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.example.mvvm.User" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_rank"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.username}"
android:textSize="20dp"></TextView>
</LinearLayout>
</layout>
在适配器中,代码简直节省了不止一点!!
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// ViewHolder viewHolder = null;
// if(convertView == null){
// convertView = View.inflate(context,R.layout.item_rank,null);
// viewHolder = new ViewHolder();
// viewHolder.tv_rank = convertView.findViewById(R.id.tv_rank);
// convertView.setTag(viewHolder);
// }else{
// viewHolder = (ViewHolder) convertView.getTag();
// }
// viewHolder.tv_rank.setText(datas.get(position).getUsername());
ItemRankBinding binding = null;
if(convertView == null) {
binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.item_rank, parent, false);
}else{
binding = DataBindingUtil.getBinding(convertView);
}
binding.setUser(datas.get(position));
return binding.getRoot().getRootView();
}
我只贴出关键代码,之前使用ListView的时候,和使用的方式对比一下,代码很简洁,因为不用findViewById
,所以ViewHolder
也不必写了。
Button
点击事件
在之前使用Button单击事件时,常规的用法是:findViewById + 设置单击事件监听,然后获取数据。
btn_search.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String city = et_city.getText().toString();
//获取数据
ModelImpl.getInstance().getWeatherByCity(city,
"7****************5");
}
});
如果使用DataBinding
来处理,就简洁了许多:设置Activity
为变量之一,得到Activity
中该Button
的单击事件函数,就可以响应点击事件,不需要去findViewById
<variable
name="activity"
type="com.example.weather.view.activity.MainActivity" />
<Button
android:id="@+id/btn_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="查询"
android:textSize="20dp"
android:onClick="@{activity.onSearchClick}"
android:layout_below="@id/et_city"></Button>
//别忘记绑定数据
binding.setActivity(this);
public void onSearchClick(View view){
String city = et_city.getText().toString();
//获取数据
ModelImpl.getInstance().getWeatherByCity(city,
"7b153f32bcfb18ff064c519e5e7a6b35");
}
各类操作符的使用
算术运算符 + - / * %
字符串连接运算符 +
逻辑运算符 && ||
二元运算符 & | ^
一元运算符 + - ! ~
移位运算符 >> >>> <<
比较运算符 == > < >= <=(请注意,< 需要转义为 <)
instanceof
分组运算符 ()
字面量运算符 - 字符、字符串、数字、null
Null 合并运算符
如果左边运算数不是 null,则 Null 合并运算符 (??) 选择左边运算数,如果左边运算数为 ,则选择右边运算数。
android:text="@{user.displayName ?? user.lastName}"
这在功能上等效于:
android:text="@{user.displayName != null ? user.displayName :
user.lastName}"