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<ImagesBean> images;

    public TooltipsBean getTooltips()   {
        return tooltips;
    }

    public void setTooltips(TooltipsBean tooltips) {
        this.tooltips = tooltips;
    }

    public List<ImagesBean> getImages() {
        return images;
    }

    public void setImages(List<ImagesBean> 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文件中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值