26
2017
09

基于MVVM架构的BaseActivity封装

* 本文同步发表在简书,转载请注明出处。

刚入了职新公司,公司项目采用的MVVM架构。之前对MVVM架构也有耳闻,但是一直没有时间去研究一番。看了几天项目代码发现MVVM确实挺好用,使用双向绑定,简化了不少Activity中的逻辑。不过代码中还是有不少重复的地方,比如Toolbar部分,由于几乎每个Activity都会有Toolbar,因此每个Activity布局文件中都会include进来toolbar的布局,造成了大量的重复代码。如果用MVC架构我们可以很简单的在BaseActivity中对Toolbar进行封装(可参考之前写过的一片文章BaseActivity中封装通用的Toolbar)但是对于MVVM架构我们是不是也可以参考在MVC中对Toolbar进行封装的方法呢?有想法就开撸!
下面先来看看撸出来的效果图:
这里写图片描述
上图中包括Toolbar在内的大部分代码都是在BaseActivity中实现的,子Activity中只有少量代码用于设定数据。来看看两个子Activity中的代码。
MainActivity代码如下:

public class MainActivity extends BaseActivity {

    @Override
    protected int getLayout() {
        return R.layout.activity_main;
    }


    @Override
    protected void init(Bundle savedInstanceState) {
    }


    @Override
    protected boolean isShowBacking() {
        return false;
    }

    public void goTest(View view) {
        Intent intent = new Intent(this, TestActivity.class);
        startActivity(intent);
    }
}

MainActivity布局文件代码:

<layout>

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.seven.mvvmdemo.MainActivity">

        <Button  android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:layout_gravity="center" android:onClick="goTest" android:text="Click Me" />

    </RelativeLayout>
</layout>

相信自己的眼睛,没有看错的!MainActivity中只重写了isShowBacking方法控制是否显示Toolbar的返回图标和goTest()方法跳转到TestActivity页面!
TestActivity更是简单,只有两行代码设置Title和SubTitle,如下:

public class TestActivity extends BaseActivity {

    @Override
    protected int getLayout() {
        return R.layout.activity_test;
    }

    @Override
    protected void init(Bundle savedInstanceState) {
        setToolBarTitle("Test");
        setToolSubTitle("subTitle");
    }
}

TestActivity布局文件代码:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.seven.mvvmdemo.TestActivity">

    <ImageView  android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:background="@mipmap/ic_launcher"/>

</RelativeLayout>

如果单从上边两个Activity来看,我们似乎看不出来这是基于MVVM的代码,而更像是基于MVC架构写的。我也不知道这样写是不是已经偏离了MVVM的思想。不过在BaseActivity中确实是通过Data Binding实现的。
那么接下来就来看看如何处理BaseActivity才能达到如此效果。
首先要明确我们要做的重点内容:将子Activity中的布局文件放到BaseActivity的布局文件中。 如何实现呢?我们不妨参考下之前文章BaseActivity中封装通用的Toolbar的实现方案,在这篇文章中我们是将BaseActivity的布局文件跟子Activity中的布局文件同时放到了一个LinearLayout中,然后把重写setContentView()方法把该LinearLayout放到了android.R.id.content中,以此实现了我们想要的效果。本篇文章实现思想其实与上一篇一致,只不过采用MVVM架构的代码不能像以前那样写罢了。
一、开始之前先来了解下DecorView,这将有助于我们理解下面的内容。
DecorView可以理解为顶级View,它的内部包含一个竖直方向的LinearLayout,在这个LinearLayout中包含两部分内容,上面是标题栏,下面是内容区。如下图所示:
这里写图片描述
我们在Activity中调用setContentView方法其实就是将布局文件添加到了内容区。关于DecorView我们不做详细讲解,到这里已经足够我们进行下面的内容了。如果想更深入的了解DecorView以及Window可以参考这篇系列文章
二、MVVM绑定布局文件分析
在MVVM架构中我们是通过DataBindingUtil.setContentView(Activity activity, int layoutId)来关联布局文件的,这句代码具体做了什么操作,我们不妨进入源码看看:

/**
     * 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 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.
     */
    public static <T extends ViewDataBinding> T setContentView(Activity activity, int layoutId) {
 return setContentView(activity, layoutId, sDefaultComponent);
    }

该方法继续调用了有三个参数的setContentView,我们继续跟进代码:

public static <T extends ViewDataBinding> T setContentView(Activity activity, int layoutId,
            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);
    }

可以看到这里调用了activity.setContentView(layoutId)也就是调用到了我们的Activity的setContentView方法。接着获取到了该Activity对应的内容区contentView,接下来去调用了bindToAddedViews方法。

 private static <T extends ViewDataBinding> T bindToAddedViews(android.databinding.DataBindingComponent component,
                                                                  ViewGroup parent,int startChildren,int layoutId){
        final int endChildren=parent.getChildCount();
        final int childrenAdded=endChildren-startChildren;
        if(childrenAdded==1){
            final View childView=parent.getChildAt(endChildren-1);
            return bind(component,childView,layoutId);
        }else {
            final View[] children=new View[childrenAdded];
            for (int i=0;i<childrenAdded;i++){
                children[i] =parent.getChildAt(i+startChildren);
            }
            return bind(component,children,layoutId);
        }
    }

 static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
            int layoutId) {
        return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
    }

我们可以看到bind方法中调用了sMapper.getDataBinder(bindingComponent, root, layoutId);我们跟踪到DataBinderMapper中看getDataBinder()方法:

public android.databinding.ViewDataBinding getDataBinder(android.databinding.DataBindingComponent bindingComponent, android.view.View view, int layoutId) {
        switch(layoutId) {
                case com.example.seven.mvvmdemo.R.layout.activity_main:
                    return com.example.seven.mvvmdemo.databinding.ActivityMainBinding.bind(view, bindingComponent);
                case com.example.seven.mvvmdemo.R.layout.toolbar_layout:
                    return com.example.seven.mvvmdemo.databinding.ToolbarLayoutBinding.bind(view, bindingComponent);
                case com.example.seven.mvvmdemo.R.layout.activity_base:
                    return com.example.seven.mvvmdemo.databinding.ActivityBaseBinding.bind(view, bindingComponent);
        }
        return null;
    }

在这个方法中其实调用了我们ActivityBinding中的bind方法,ActivityBinding类是由系统生成的。
三、了解了MVVM绑定的过程后就可以根据我们自己的需要来完成我们要实现的功能了。我们知道DataBindingUtil.setContentView(Activity activity, int layoutId) 内部其实还是调用了Activity的setContentView方法,因此我们还可以通过在基类中重写setContentView方法来实现!BaseActivity具体实现如下:

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_base);

        toolBarBean = new ToolBarBean(this);
        mDataBinding.setToolbarBean(toolBarBean);

        init(savedInstanceState);
    }

/** * 重写setContentView方法,子类ActivityBinding会调用该方法 * * @param layoutResID 子类布局文件的id */
    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        super.setContentView(layoutResID);
        View decorView = getWindow().getDecorView();
        LinearLayout linearLayout = decorView.findViewById(R.id.ll_base);
        LayoutInflater.from(this).inflate(getLayout(), linearLayout, true);
    }

上面代码DataBindingUtil.setContentView(this, R.layout.activity_base)一句其实调用的是我们重写的setContentView的方法。setContentView方法中参数layoutResID其实就是BaseActivity的布局文件activity_base.xml。在重写的setContentView中首先调用了父类的代码super.setContentView(layoutResID);
此时activity_base.xml其实已经被放到了decorView中的内容区了!那么接下来我们便可以拿到decorView并通过findViewById拿到activity_base.xml中id为ll_base的LinearLayout,此时我们再把子类的布局文件添加到activity_base.xml里边即可实现我们的需求了!试着跑下程序,木有问题,两个Activity布局都正常显示出来了。最后贴下BaseActivity中的全部代码:

public abstract class BaseActivity extends AppCompatActivity {
    protected ActivityBaseBinding mDataBinding;
    private ToolBarBean toolBarBean;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_base);

        toolBarBean = new ToolBarBean(this);
        mDataBinding.setToolbarBean(toolBarBean);

        init(savedInstanceState);
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (isShowBacking()) {
            showBack();
        }
    }

    protected void setToolbarTitle(String title){
        toolBarBean.setTitle(title);
    }

    protected void setToolSubTitle(String subTitle){
        mDataBinding.baseToolbar.toolbarSubtitle.setVisibility(View.VISIBLE);
        toolBarBean.setSubTitle(subTitle);
    }

    protected void isShowToolbar(boolean isShow){
        mDataBinding.baseToolbar.toolbarSubtitle.setVisibility(View.VISIBLE);
    }

    private void showBack() {
        getToolbar().setNavigationIcon(R.drawable.ic_back);
        getToolbar().setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                onBackPress();
            }
        });
    }

    private void onBackPress() {
        onBackPressed();
    }


    private Toolbar getToolbar() {
        return mDataBinding.baseToolbar.toolbar;
    }

    protected ToolBarBean getToolBarBean() {
        return toolBarBean;
    }

    protected boolean isShowBacking() {
        return true;
    }

    protected void setToolBarTitle(String title) {
        toolBarBean.setTitle(title);
    }


    protected abstract @LayoutRes int getLayout();

    protected abstract void init(Bundle savedInstanceState);

    protected ToolBarBean getToolbarBean(){
        return toolBarBean;
    }

    /** * 重写setContentView方法,子类ActivityBinding会调用该方法 * * @param layoutResID 子类布局文件的id */
    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        super.setContentView(layoutResID);
        View decorView = getWindow().getDecorView();
        LinearLayout linearLayout = decorView.findViewById(R.id.ll_base);
        LayoutInflater.from(this).inflate(getLayout(), linearLayout, true);
    }

    public static class MyComponent implements android.databinding.DataBindingComponent {
        @BindingAdapter("android:alpha")
        public void setAlpha(View view, float alpha) {
            view.setAlpha(0.5f);
        }

        @Override
        public MyComponent getMyComponent() {
            return new MyComponent();
        }
    }

}

BaseActivity布局文件中的代码:

<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:bind="http://schemas.android.com/apk/res-auto">

    <data>

        <variable  name="toolbarBean" type="com.example.seven.mvvmdemo.ToolBarBean"></variable>
    </data>

    <LinearLayout  android:id="@+id/ll_base" android:layout_width="match_parent" android:orientation="vertical" android:layout_height="match_parent">

        <include  android:id="@+id/base_toolbar" layout="@layout/toolbar_layout" bind:toolbarBean="@{toolbarBean}" />



    </LinearLayout>
</layout>

此时我们让子Activity继承BaseActivity后,子类中便可省去Binding Layout的过程,也避免了很多重复代码!不过还是要声明一下,这个操作目前还没有用到实际项目中,因此不能保证不会出现问题。大胆的童鞋可以先拿来尝试一下,如有问题概不负责O(∩_∩)O。

源码传送门

上一篇:字符串连接用"+"和StringBuilder的append的区别 下一篇:retrofit(2017_09_22)