Android官方架构组件ViewModel+LiveData+DataBinding架构属于自己的MVVM

Android官方架构组件ViewModel+LiveData+DataBinding架构属于自己的MVVM

Demo运行效果

获取Bing每日一图并显示

项目结构

实现过程

1. 添加Glide、Retrofit、RxJava的依赖

implementation 'com.squareup.retrofit2:retrofit:2.4.0'
compile 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
compile 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation 'io.reactivex.rxjava2:rxjava:2.1.12'
implementation 'com.github.bumptech.glide:glide:4.6.1'
annotationProcessor 'com.github.bumptech.glide:compiler:4.6.1'

2. 启用DataBinding

dataBinding{
	enabled=true
}

3. 添加网络访问权限

<uses-permission android:name="android.permission.INTERNET"/>

4. 解析网络接口返回信息

接口地址:https://cn.bing.com/HPImageArchive.aspx?format=js&idx=1&n=1

	{
		"images": [
		{
			"startdate": "20180408",
			"fullstartdate": "201804081600",
			"enddate": "20180409",
			"url": "/az/hprichbg/rb/LenaDelta_ZH-CN9073097502_1920x1080.jpg",
			"urlbase": "/az/hprichbg/rb/LenaDelta_ZH-CN9073097502",
			"copyright": "位于西伯利亚的勒拿河三角洲野生动物保护区,俄罗斯 (© USGS EROS Data Center/NASA)",
			"copyrightlink": "http://www.bing.com/search?q=%E5%8B%92%E6%8B%BF%E6%B2%B3%E4%B8%89%E8%A7%92%E6%B4%B2%E9%87%8E%E7%94%9F%E5%8A%A8%E7%89%A9%E4%BF%9D%E6%8A%A4%E5%8C%BA&form=hpcapt&mkt=zh-cn",
			"quiz": "/search?q=Bing+homepage+quiz&filters=WQOskey:%22HPQuiz_20180408_LenaDelta%22&FORM=HPQUIZ",
			"wp": true,
			"hsh": "b3ed2f27f31a4e68da602e232fe223f0",
			"drk": 1,
			"top": 1,
			"bot": 1,
			"hs": []
			}
		],
		"tooltips": {
			"loading": "正在加载...",
			"previous": "上一个图像",
			"next": "下一个图像",
			"walle": "此图片不能下载用作壁纸。",
			"walls": "下载今日美图。仅限用作桌面壁纸。"
		}
	}

接口返回一个Json对象,其中images为一个图片信息列表。图片信息中,我们关心的是"url",和"copyright"这两个属性。其中url返回的字符串在首部拼接上"https://www.bing.com/"就是图片的url,copyright是图片的描述信息。

5. 创建实体类ImageBean

创建实体类的过程相对简单,直接通过AndroidStudio的GsonFormat插件来自动生成。

	public class ImageBean {

	    private TooltipsBean tooltips;
	    private List images;
	
	    public TooltipsBean getTooltips() 	{
	        return tooltips;
	    }
	
	    public void setTooltips(TooltipsBean tooltips) {
	        this.tooltips = tooltips;
	    }
	
	    public List getImages() {
	        return images;
	    }
	
	    public void setImages(List images) {
	        this.images = images;
	    }
	
	    public static class TooltipsBean {
	
	        private String loading;
	        private String previous;
	        private String next;
	        private String walle;
	        private String walls;
	
	        public String getLoading() {
	            return loading;
	        }
	
	        public void setLoading(String loading) {
	            this.loading = loading;
	        }
	
	        public String getPrevious() {
	            return previous;
	        }
	
	        public void setPrevious(String previous) {
	            this.previous = previous;
	        }
	
	        public String getNext() {
	            return next;
	        }
	
	        public void setNext(String next) {
	            this.next = next;
	        }
	
	        public String getWalle() {
	            return walle;
	        }
	
	        public void setWalle(String walle) {
	            this.walle = walle;
	        }
	
	        public String getWalls() {
	            return walls;
	        }
	
	        public void setWalls(String walls) {
	            this.walls = walls;
	        }
	    }
	
	    public static class ImagesBean {
	
	        public static final String BASE_URL = "https://www.bing.com/";
	
	        private String startdate;
	        private String fullstartdate;
	        private String enddate;
	        private String url;
	        private String urlbase;
	        private String copyright;
	        private String copyrightlink;
	        private String quiz;
	        private boolean wp;
	        private String hsh;
	        private int drk;
	        private int top;
	        private int bot;
	        private List hs;
	
	        public String getStartdate() {
	            return startdate;
	        }
	
	        public void setStartdate(String startdate) {
	            this.startdate = startdate;
	        }
	
	        public String getFullstartdate() {
	            return fullstartdate;
	        }
	
	        public void setFullstartdate(String fullstartdate) {
	            this.fullstartdate = fullstartdate;
	        }
	
	        public String getEnddate() {
	            return enddate;
	        }
	
	        public void setEnddate(String enddate) {
	            this.enddate = enddate;
	        }
	
	        public String getUrl() {
	            return url;
	        }
	
	        public void setUrl(String url) {
	            this.url = url;
	        }
	
	        public String getUrlbase() {
	            return urlbase;
	        }
	
	        public void setUrlbase(String urlbase) {
	            this.urlbase = urlbase;
	        }
	
	        public String getCopyright() {
	            return copyright;
	        }
	
	        public void setCopyright(String copyright) {
	            this.copyright = copyright;
	        }
	
	        public String getCopyrightlink() {
	            return copyrightlink;
	        }
	
	        public void setCopyrightlink(String copyrightlink) {
	            this.copyrightlink = copyrightlink;
	        }
	
	        public String getQuiz() {
	            return quiz;
	        }
	
	        public void setQuiz(String quiz) {
	            this.quiz = quiz;
	        }
	
	        public boolean isWp() {
	            return wp;
	        }
	
	        public void setWp(boolean wp) {
	            this.wp = wp;
	        }
	
	        public String getHsh() {
	            return hsh;
	        }
	
	        public void setHsh(String hsh) {
	            this.hsh = hsh;
	        }
	
	        public int getDrk() {
	            return drk;
	        }
	
	        public void setDrk(int drk) {
	            this.drk = drk;
	        }
	
	        public int getTop() {
	            return top;
	        }
	
	        public void setTop(int top) {
	            this.top = top;
	        }
	
	        public int getBot() {
	            return bot;
	        }
	
	        public void setBot(int bot) {
	            this.bot = bot;
	        }
	
	        public List getHs() {
	            return hs;
	        }
	
	        public void setHs(List hs) {
	            this.hs = hs;
	        }
	    }
	}

值得注意的是由于接口并没有返回图片url前缀信息,所以我在ImagesBean的内部手动添加了一个变量BASE_URL来存储图片url前缀信息。


由于项目采用MVVM架构,View层与ViewModel层的通信是通过LiveData这个架构组件实现的,不同于MVP架构中通过接口来通信,所以还要对数据加载的状态和错误信息进行维护。这里创建一个包装类来维护数据的状态和错误信息,以便View层可以对数据加载错误信息进行响应和处理。
public class Data {
    private T mData;
    private String mErrorMsg;
public Data(T data, String errorMsg) {
    mData = data;
    mErrorMsg = errorMsg;
}

public T getData() {
    return mData;
}

public void setData(T data) {
    mData = data;
}

public String getErrorMsg() {
    return mErrorMsg;
}

public void setErrorMsg(String errorMsg) {
    mErrorMsg = errorMsg;
}

}


Data类的内部非常简单,只有一个泛型T的成员mData来存储数据,和一个String类型的mErrorMsg来存储错误信息。这样View层就可以通过判断mErrorMsg是否为空来判断出数据加载成功与否。

6. 创建数据访问接口ImageRepertory

	public class ImageRepertory {
	
	    private Retrofit mRetrofit;
	
	    public ImageRepertory() {
	        mRetrofit = new Retrofit.Builder()
	                .baseUrl("https://cn.bing.com/")
	                .addConverterFactory(GsonConverterFactory.create())
	                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
	                .build();
	    }
	
	    private interface Service {
	
	        @GET("HPImageArchive.aspx")
	        Observable getImage(
	                @Query("format") String format,
	                @Query("idx") int idx,
	                @Query("n") int n
	        );
	
	    }
	
	    public Observable getImage(String format, int idx, int n) {
	        return mRetrofit.create(Service.class).getImage(format, idx, n);
	    }
	
	}

项目采用了Retrofit+Rxjava作为网络访问框架。首先ImageRepertory内部有一个Retrofit实例,并且在构造函数中进行Retrofit的配置和创建。接着创建一个Service接口,其中的getImage方法用来获取图片信息,方法返回一个ImageBean的Observable对象。

7. 编写ImageViewModel

public class ImageViewModel extends ViewModel {

    private MutableLiveData<Data<ImageBean.ImagesBean>> mImage;
    private ImageRepertory mRepertory;
    private int idx;

    public ImageViewModel() {
        mImage = new MutableLiveData<>();
        mRepertory = new ImageRepertory();
        idx = 0;
    }

    public MutableLiveData<Data<ImageBean.ImagesBean>> getImage() {
        return mImage;
    }

    public void LoadImage() {
        mRepertory.getImage("js", idx, 1)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<ImageBean>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                    }

                    @Override
                    public void onNext(ImageBean imageBean) {
                        mImage.setValue(new Data<ImageBean.ImagesBean>(
                                imageBean.getImages().get(0), null
                        ));
                    }

                    @Override
                    public void onError(Throwable e) {
                        mImage.setValue(new Data<ImageBean.ImagesBean>(
                                null, e.getMessage()
                        ));
                    }

                    @Override
                    public void onComplete() {
                    }
                });
    }

    public void nextImage() {
        mRepertory.getImage("js", ++idx, 1)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<ImageBean>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                    }

                    @Override
                    public void onNext(ImageBean imageBean) {
                        mImage.setValue(new Data<ImageBean.ImagesBean>(
                                imageBean.getImages().get(0), null
                        ));
                    }

                    @Override
                    public void onError(Throwable e) {
                        mImage.setValue(new Data<ImageBean.ImagesBean>(
                                null, e.getMessage()
                        ));
                        idx--;
                    }

                    @Override
                    public void onComplete() {
                    }
                });
    }

    public void previousImage() {
        if (idx <= 0) {
            mImage.setValue(new Data<ImageBean.ImagesBean>(
                    null, "已经是第一个了"
            ));
            return;
        }
        mRepertory.getImage("js", --idx, 1)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<ImageBean>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                    }

                    @Override
                    public void onNext(ImageBean imageBean) {
                        mImage.setValue(new Data<ImageBean.ImagesBean>(
                                imageBean.getImages().get(0), null
                        ));
                    }

                    @Override
                    public void onError(Throwable e) {
                        mImage.setValue(new Data<ImageBean.ImagesBean>(
                                null, e.getMessage()
                        ));
                        idx++;
                    }

                    @Override
                    public void onComplete() {
                    }
                });
    }

}

首先这个类要继承自android.arch.lifecycle.ViewModel这个类,以便在创建时与View层的生命周期相关联。然后是三个成员变量:mImage这个变量的类型是MutableLiveData用来存放图片信息,以便当信息发生变化时及时通知View层来更新界面;mRepertory这个变量来负责数据访问;idx这个变量来记录当前的图片页码。这三个变量在构造函数中创建并初始化,接着为mImage添加了getter方法以便View层可以对其进行观察与响应。loadImage,nextImage和previousImage这三个方法分别对应图片的加载,下一张和上一张,并且内部通过访问mRepertory的方法来完成数据的访问,又对返回的数据进行判断处理并触发mImage的setValue方法来对数据进行更新。

8. 编写页面

<?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="imageBean"
            type="com.njp.mvvm.ImageBean.ImagesBean" />

        <variable
            name="presenter"
            type="com.njp.mvvm.ImageActivity.Presenter" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <ImageView
            url="@{imageBean.BASE_URL+imageBean.url}"
            android:layout_width="match_parent"
            android:layout_height="300dp" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">

            <Button
                android:id="@+id/btn_previous"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:onClick="@{presenter.onClick}"
                android:layout_weight="1"
                android:text="上一张" />

            <Button
                android:id="@+id/btn_load"
                android:layout_width="0dp"
                android:onClick="@{presenter.onClick}"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="加载" />

            <Button
                android:id="@+id/btn_next"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:onClick="@{presenter.onClick}"
                android:layout_weight="1"
                android:text="下一张" />

        </LinearLayout>
    </LinearLayout>

</layout>

与正常的XML布局文件不同的是,根标签改成了layout标签,内部有data标签和具体的布局。dat标签内存放Databinding的数据类。除了需要用到的ImagesBean类之外,这里还声明了一个Presenter类用来对界面的用户行为做统一的管理。


注意到ImageView的标签内声明了一个url属性,并且和data内的image的数据进行了绑定。然而ImageView并没有这个属性,这时就需要用到Databinding的自定义属性了。

public class BindingAdapter {

    @android.databinding.BindingAdapter("url")
    public static void setImageUrl(ImageView imageView, String url) {
        Glide.with(imageView.getContext())
                .load(url)
                .into(imageView);
    }

}

编写一个BindingAdapter类用来声明自定义属性,并使用@android.databinding.BindingAdapter注解来让编译器知道你的属性名。在方法中来对属性值进行处理,这里使用了Glide来进行网络图片的加载。

9. 编写ImageActivity

public class ImageActivity extends AppCompatActivity {

    private ActivityImageBinding mBinding;
    private ImageViewModel mViewModel;
    private ProgressDialog mProgressDialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_image);
        mViewModel = new ViewModelProvider(
                this, new ViewModelProvider.AndroidViewModelFactory(getApplication())
        ).get(ImageViewModel.class);
        mProgressDialog = new ProgressDialog(this);
        mProgressDialog.setMessage("加载中");

        mViewModel.getImage().observe(this, new Observer<Data<ImageBean.ImagesBean>>() {
            @Override
            public void onChanged(@Nullable Data<ImageBean.ImagesBean> imagesBeanData) {
                if (imagesBeanData.getErrorMsg() != null) {
                    Toast.makeText(ImageActivity.this, imagesBeanData.getErrorMsg(), Toast.LENGTH_SHORT).show();
                    mProgressDialog.dismiss();
                    return;
                }
                mBinding.setImageBean(imagesBeanData.getData());
                setTitle(imagesBeanData.getData().getCopyright());
                mProgressDialog.dismiss();
            }
        });

        mBinding.setPresenter(new Presenter());

        mProgressDialog.show();
        mViewModel.loadImage();
    }

    public class Presenter {

        public void onClick(View view) {
            mProgressDialog.show();
            switch (view.getId()) {
                case R.id.btn_load:
                    mViewModel.loadImage();
                    break;
                case R.id.btn_previous:
                    mViewModel.previousImage();
                    break;
                case R.id.btn_next:
                    mViewModel.nextImage();
                    break;
                default:
                    break;
            }
        }

    }

}

三个成员变量:mBinding数据绑定对象,用来实现数据绑定;mViewModel用来获取数据,实现与数据层的解耦;mProgressDialog用来弹出加载提示框。这三个变量在oncreate方法中初始化,mBinding用DataBindingUtil的setContentView方法实现视图层的绑定;mViewModel要使用ViewModelProvider的get方法完成创建。接着对ViewModel中的LiveData进行观察,在observe方法中处理错误和数据的绑定。内部类Presenter用来对点击事件进行响应,并且也要在oncreate方法里与mBinding进行绑定。


注意:xml文件中一定不能出现与业务相关的代码!比如直接将ViewModel的访问数据的方法在xml中与按钮的点击事件进行绑定,这种方做法是不可取的,因为XML文件的作用应该只是进行数据的显示和用户的交互,而访问数据这种和业务相关的操作不应出现在XML文件中。
posted @ 2018-04-10 21:46  DEV_NJP  阅读(32937)  评论(1)    收藏  举报