MVVM开发模式与DataBinding
前言
MVVM(Model-View-ViewModel)本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。
结构
- Model: 业务数据模型
- View: 界面中的布局和外观
- ViewModel: 逻辑控制层,负责处理数据和处理View层中的业务逻辑
DataBinding简介
DataBinding是Google官方推出的数据绑定器,作用是将数据和View绑定起来,然后数据改变的时候View会自动刷新。
这里需要注意的是MVVM是一种设计思想,而DataBinding是一种工具。二者有本质的差别。
引入dataBinding,在build.gradle中添加以下代码。
android{
....
dataBinding{
enabled true
}
}
通过改变XML布局文件来使用dataBinding。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 定义该布局需要绑定的数据名称和类型 -->
<data>
<variable
name="loginViewModel"
type="com.mvvm.vm.LoginViewModel" />
<!--<variable-->
<!--name="user"-->
<!--type="com.mvvm.model.UserInfo" />-->
</data>
<!-- 下部分内容和平时布局文件一样 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:addTextChangedListener="@{loginViewModel.nameInputListener}"
android:hint="请输入账户"
android:text="@={loginViewModel.userInfo.name}" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:addTextChangedListener="@{loginViewModel.pwdInputListener}"
android:hint="请输入密码"
android:text="@={loginViewModel.userInfo.pwd}" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:onClick="@{loginViewModel.loginClickListener}"
android:text="登录" />
</LinearLayout>
</layout>
Model层依旧是一个javaBean文件,这里使用的是ObservableField来定义的:
public class UserInfo {
// 必须是public修饰符,因为是DataBinding的规范
public ObservableField<String> name = new ObservableField<>();
public ObservableField<String> pwd = new ObservableField<>();
}
VM(ViewModel)层通过TextWatcher和OnClickListener来实现与View的绑定,极大的精简了Activity里面的代码,
public class LoginViewModel {
public UserInfo userInfo;
public LoginViewModel(ActivityMvvmLoginBinding binding) {
userInfo = new UserInfo();
// 将ViewModel和View进行绑定,通过DataBinding工具
binding.setLoginViewModel(this);
}
public TextWatcher nameInputListener = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
userInfo.name.set(String.valueOf(charSequence));
}
@Override
public void afterTextChanged(Editable editable) {
}
};
public TextWatcher pwdInputListener = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// View层接收到用户的输入,改变Model层的javabean属性
userInfo.pwd.set(String.valueOf(s));
}
@Override
public void afterTextChanged(Editable s) {
}
};
public View.OnClickListener loginClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
// 模拟网络请求
new Thread(new Runnable() {
@Override
public void run() {
// Model层属性的变更,改变View层的显示
userInfo.name.set("zjjj");
userInfo.pwd.set("654321");
if ("xzzz".equals(userInfo.name.get()) && "123456".equals(userInfo.pwd.get())) {
Log.e("xzzz >>> ", "登录成功!");
} else {
Log.e("xzzz >>> ", "登录失败!");
}
}
}).start();
}
};
}
而Activity中,只有两行代码,极大的精简了Activity中的逻辑。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMvvmLoginBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_mvvm_login);
new LoginViewModel(binding);
}
}
MVVM模式的优点
1.很好做到数据的一致性。双向绑定技术,当Model变化时,View-Model会自动更新,View也会自动变化。
2.降低了项目的耦合度。一个ViewModel层可以绑定不同的View层,当Model变化时View可以不变。
3.增加的代码的可重用性。可以把一些视图逻辑放在ViewModel层中,让很多的View重用这些视图逻辑,极大的精简了代码。
MVVM模式的缺点
1.数据绑定使得 Bug 很难被调试。你看到界面异常了,由于数据绑定的原因,定位原始问题的出处就变得不那么容易了。
2.内存消耗过大。
DataBinding内存消耗过大的原因
1.对于每个XML文件都需要一个额外的数组,造成额外的数组开销。
2.ViewDataBinding中的静态代码块中会有个OnAttachStateChangeListener方法会根据Activity个数新建监听的线程。
3.频繁调用调用 invalidateAll()方法刷新界面。
1.在ActivityMvvmLoginBindingImpl中我们可以看到有数组的存在,而在ViewDataBinding中使用mapBindings()方法根据每个XML文件生成binding_下标的数组对应每个控件。
private ActivityMvvmLoginBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
super(bindingComponent, root, 2
);
this.mboundView0 = (android.widget.LinearLayout) bindings[0];
this.mboundView0.setTag(null);
this.mboundView1 = (android.widget.EditText) bindings[1];
this.mboundView1.setTag(null);
this.mboundView2 = (android.widget.EditText) bindings[2];
this.mboundView2.setTag(null);
this.mboundView3 = (android.widget.Button) bindings[3];
this.mboundView3.setTag(null);
setRootTag(root);
// listeners
invalidateAll();
}
private static void mapBindings(DataBindingComponent bindingComponent, View view, Object[] bindings, ViewDataBinding.IncludedLayouts includes, SparseIntArray viewsWithIds, boolean isRoot) {
ViewDataBinding existingBinding = getBinding(view);
if (existingBinding == null) {
Object objTag = view.getTag();
String tag = objTag instanceof String ? (String)objTag : null;
boolean isBound = false;
int indexInIncludes;
int id;
int count;
if (isRoot && tag != null && tag.startsWith("layout")) {
id = tag.lastIndexOf(95);
if (id > 0 && isNumeric(tag, id + 1)) {
count = parseTagInt(tag, id + 1);
if (bindings[count] == null) {
bindings[count] = view;
}
indexInIncludes = includes == null ? -1 : count;
isBound = true;
} else {
indexInIncludes = -1;
}
} else if (tag != null && tag.startsWith("binding_")) {
id = parseTagInt(tag, BINDING_NUMBER_START);
if (bindings[id] == null) {
bindings[id] = view;
}
isBound = true;
indexInIncludes = includes == null ? -1 : id;
} else {
indexInIncludes = -1;
}
if (!isBound) {
id = view.getId();
if (id > 0 && viewsWithIds != null && (count = viewsWithIds.get(id, -1)) >= 0 && bindings[count] == null) {
bindings[count] = view;
}
}
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup)view;
count = viewGroup.getChildCount();
int minInclude = 0;
for(int i = 0; i < count; ++i) {
View child = viewGroup.getChildAt(i);
boolean isInclude = false;
if (indexInIncludes >= 0 && child.getTag() instanceof String) {
String childTag = (String)child.getTag();
if (childTag.endsWith("_0") && childTag.startsWith("layout") && childTag.indexOf(47) > 0) {
int includeIndex = findIncludeIndex(childTag, minInclude, includes, indexInIncludes);
if (includeIndex >= 0) {
isInclude = true;
minInclude = includeIndex + 1;
int index = includes.indexes[indexInIncludes][includeIndex];
int layoutId = includes.layoutIds[indexInIncludes][includeIndex];
int lastMatchingIndex = findLastMatching(viewGroup, i);
if (lastMatchingIndex == i) {
bindings[index] = DataBindingUtil.bind(bindingComponent, child, layoutId);
} else {
int includeCount = lastMatchingIndex - i + 1;
View[] included = new View[includeCount];
for(int j = 0; j < includeCount; ++j) {
included[j] = viewGroup.getChildAt(i + j);
}
bindings[index] = DataBindingUtil.bind(bindingComponent, included, layoutId);
i += includeCount - 1;
}
}
}
}
if (!isInclude) {
mapBindings(bindingComponent, child, bindings, includes, viewsWithIds, false);
}
}
}
}
}
2.在静态代码块中,当版本大于19的时候,会有一个OnAttachStateChangeListener监听,在每个Activity发生改变时,会新建一个mRebindRunnable的线程,当Activity数量很多的时候,对应的线程也会增加,由此增加内存的消耗。
static {
...
if (VERSION.SDK_INT < 19) {
ROOT_REATTACHED_LISTENER = null;
} else {
ROOT_REATTACHED_LISTENER = new OnAttachStateChangeListener() {
@TargetApi(19)
public void onViewAttachedToWindow(View v) {
ViewDataBinding binding = ViewDataBinding.getBinding(v);
binding.mRebindRunnable.run();
v.removeOnAttachStateChangeListener(this);
}
public void onViewDetachedFromWindow(View v) {
}
};
}
}
private final Runnable mRebindRunnable = new Runnable() {
public void run() {
synchronized(this) {
ViewDataBinding.this.mPendingRebind = false;
}
ViewDataBinding.processReferenceQueue();
if (VERSION.SDK_INT >= 19 && !ViewDataBinding.this.mRoot.isAttachedToWindow()) {
ViewDataBinding.this.mRoot.removeOnAttachStateChangeListener(ViewDataBinding.ROOT_REATTACHED_LISTENER);
ViewDataBinding.this.mRoot.addOnAttachStateChangeListener(ViewDataBinding.ROOT_REATTACHED_LISTENER);
} else {
ViewDataBinding.this.executePendingBindings();
}
}
};
3.一但Model层数据发生变化,会调用 invalidateAll()方法刷新界面,频繁使用Handlerf发送消息。
@Override
public void invalidateAll() {
synchronized(this) {
mDirtyFlags = 0x8L;
}
requestRebind();
}
protected void requestRebind() {
if (mContainingBinding != null) {
mContainingBinding.requestRebind();
} else {
final LifecycleOwner owner = this.mLifecycleOwner;
if (owner != null) {
Lifecycle.State state = owner.getLifecycle().getCurrentState();
if (!state.isAtLeast(Lifecycle.State.STARTED)) {
return; // wait until lifecycle owner is started
}
}
synchronized (this) {
if (mPendingRebind) {
return;
}
mPendingRebind = true;
}
if (USE_CHOREOGRAPHER) {
mChoreographer.postFrameCallback(mFrameCallback);
} else {
mUIThreadHandler.post(mRebindRunnable);
}
}
}