MVVM在Android中的初学之路
上篇写了MVP在Android中的初学之路 https://blog.csdn.net/Ae_fring/article/details/85158579。本篇继续架构之路MVVM,记录下初学的笔记。
MVVM的模型图:
当然这里也贴上盗来的MVC和MVP的模型图:(个人感觉比较清晰)
通过图可以了解最初的MVC的结构,由于Android中纯粹作为View的XML视图功能太弱,我们大量处理View的逻辑只能写在Activity中,这样Activity就充当了View和Controller两个角色,直接导致Activity中的代码量增多。相信大多数Android开发者都遇到过一个Acitivty数以千行的代码情况吧。
MVVM优点:
1.可重用性
可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。 布局里可以进行一个视图逻辑,并且Model发生变化,View也随着发生变化。
2.低耦合
以前Activity、Fragment中需要把数据填充到View,还要进行一些视图逻辑。现在这些都可在布局中完成(具体代码请看后面) 甚至都不需要再Activity、Fragment去findViewById。这时候Activity、Fragment只需要做好的逻辑处理就可以了。
MVVM的三大角色:
View: 对应于Activity和XML,负责View的绘制以及与用户交互。
Model: 实体模型。
ViewModel: 负责完成View与Model间的交互,负责业务逻辑。
通过图我们可以看到MVVM采用一种新的方式Data Binding来作为View和Model之间的绑定关系,增强XML视图功能 减少我们对控件的findViewbyId(下面会分析)
既然知道MVVM是一种架构模式,而DataBinding是一个实现数据和UI绑定的框架,是构建MVVM模式的一个工具。下面记录下代码实现部分:
app的build.gradle:
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
defaultConfig {
applicationId "com.ken.mvvmdemo"
minSdkVersion 14
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
dataBinding {
enabled true
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation 'it.sephiroth.android.library.picasso:picasso:2.5.2.4b'
implementation 'com.squareup.okhttp3:okhttp:3.6.0'
}
最主要的 activity_main:
<?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">
<data>
<variable
name="user"
type="com.ken.mvvmdemo.User" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
app:headimg="@{user.headimg}" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@{user.name}" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@{user.password}" />
</LinearLayout>
</layout>
User类
public class User extends BaseObservable {
private String name;
private String password;
private String headimg;
public User(String name, String password, String img) {
this.name = name;
this.password = password;
this.headimg = img;
}
@BindingAdapter("bind:headimg")
public static void getHeader(ImageView view, String url) {
Picasso.with(view.getContext()).load(url).into(view);
}
@Bindable
public String getName() {
return name;
}
public void setName(String name) {
notifyPropertyChanged(BR.name);
this.name = name;
}
@Bindable
public String getPassword() {
return password;
}
public void setPassword(String password) {
notifyPropertyChanged(BR.password);
this.password = password;
}
public String getHeadimg() {
return headimg;
}
public void setHeadimg(String headimg) {
this.headimg = headimg;
}
}
可以看到根布局不在是我们以前常见的五大布局中的其中之一 而是layout 接着<data>包裹这是DataBing的表达式写法
<variable
name="user"
type="com.ken.mvvmdemo.User" />
name:引用名
type:对应需要使用到的类包名
文字显示使用@{user.name}方式:
android:text="@{user.name}"
图片显示使用 app:headimg="@{user.headimg}" 后面分析源码
MainActivity代码:
public class MainActivity extends AppCompatActivity {
Handler handler = new Handler();
UserField userField = new UserField();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setContentView(R.layout.activity_main);
ActivityMainBinding mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
final User user = new User("张三", "123456", "http://pic6.huitu.com/res/20130116/84481_20130116142820494200_1.jpg");
mainBinding.setUser(user);
mainBinding.setField(userField);
handler.postDelayed(new Runnable() {
@Override
public void run() {
// user.setName("李四");
// user.setPassword("123");
userField.name.set("李四");
userField.password.set("123");
}
}, 2000);
}
}
现在结合MainActivity的代码分析下:首先变化的是setContentView
ActivityMainBinding mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
源码
/**
* Set the Activity's content view to the given layout and return the associated binding.
* The given layout resource must not be a merge layout.
*
* @param <T> Type of the generated binding class.
* @param activity The Activity whose content View should change.
* @param layoutId The resource ID of the layout to be inflated, bound, and set as the
* Activity's content.
* @return The binding associated with the inflated content view or {@code null} if the
* layoutId is not a data binding layout.
*/
// @Nullable don't annotate with Nullable. It is unlikely to be null and makes using it from
// kotlin really ugly. We cannot make it NonNull w/o breaking backward compatibility.
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
int layoutId) {
return setContentView(activity, layoutId, sDefaultComponent);
}
/**
* Set the Activity's content view to the given layout and return the associated binding.
* The given layout resource must not be a merge layout.
*
* @param <T> Type of the generated binding class.
* @param bindingComponent The DataBindingComponent to use in data binding.
* @param activity The Activity whose content View should change.
* @param layoutId The resource ID of the layout to be inflated, bound, and set as the
* Activity's content.
* @return The binding associated with the inflated content view or {@code null} if the
* layoutId is not a data binding layout.
*/
// @Nullable don't annotate with Nullable. It is unlikely to be null and makes using it from
// kotlin really ugly. We cannot make it NonNull w/o breaking backward compatibility.
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
int layoutId, @Nullable DataBindingComponent bindingComponent) {
activity.setContentView(layoutId);
View decorView = activity.getWindow().getDecorView();
ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}
通过源码我们了解到其实DataBind底层还是走到了之前的setContentView 不过加入了一个ViewDataBinding进来,其次返回了一个ActivityMainBinding。
由于DataBinding是编译时的工具。看到最终要执行的文件分析得出ViewDataBinding 实现了一个监听对layout的每一个控件实现Tag标识 在getBinding()后getTag()每一个控件
ActivityMainBindingImpl
private ActivityMainBindingImpl(android.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
super(bindingComponent, root, 3
);
this.mboundView0 = (android.widget.LinearLayout) bindings[0];
this.mboundView0.setTag(null);
this.mboundView1 = (android.widget.ImageView) bindings[1];
this.mboundView1.setTag(null);
this.mboundView2 = (android.widget.TextView) bindings[2];
this.mboundView2.setTag(null);
this.mboundView3 = (android.widget.TextView) bindings[3];
this.mboundView3.setTag(null);
setRootTag(root);
// listeners
invalidateAll();
}
@Override
public void invalidateAll() {
synchronized(this) {
mDirtyFlags = 0x10L;
}
requestRebind();
}
ViewDataBinding
/**
* @hide
*/
protected void requestRebind() {
if (mContainingBinding != null) {
mContainingBinding.requestRebind();
} else {
synchronized (this) {
if (mPendingRebind) {
return;
}
mPendingRebind = true;
}
if (mLifecycleOwner != null) {
Lifecycle.State state = mLifecycleOwner.getLifecycle().getCurrentState();
if (!state.isAtLeast(Lifecycle.State.STARTED)) {
return; // wait until lifecycle owner is started
}
}
if (USE_CHOREOGRAPHER) {
mChoreographer.postFrameCallback(mFrameCallback);
} else {
mUIThreadHandler.post(mRebindRunnable);
}
}
}
最终在UI线程执行Runable----->executePendingBindings();通过源码可以看到最终让子类去实现了一个抽象的方法,所以这就解释了为什么在User类中实现加载图片为何这样写成static了。
下面记录下在ListView中使用方法
public class ComonAdapter<T> extends BaseAdapter {
private Context context;
private LayoutInflater inflater;
private int layoutId;
private int variableId;
private List<T> list;
public ComonAdapter(Context context, int layoutId, int variableId, List<T> list) {
this.context = context;
this.inflater = LayoutInflater.from(context);
this.layoutId = layoutId;
this.variableId = variableId;
this.list = list;
}
@Override
public int getCount() {
return list.size();
}
@Override
public Object getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewDataBinding viewDataBinding;
if (convertView == null) {
viewDataBinding = DataBindingUtil.inflate(inflater, layoutId, parent, false);
} else {
viewDataBinding = DataBindingUtil.getBinding(convertView);
}
//数据部分
viewDataBinding.setVariable(variableId, list.get(position));
return viewDataBinding.getRoot().getRootView();
}
}
这里就写下Adapter的方法,Activity还是和最开始的写法一样,主要是适配器的getView()方法,还是以ViewDataBinding为核心 最终还是通过一系列的调用去拿到数据和前面的分析差不多的。
先记录到此附上下载源码:https://github.com/eternityzqf/MvvmDemo
谢谢!