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. 添加网络访问权限
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> mImage;
private ImageRepertory mRepertory;
private int idx;
public ImageViewModel() {
mImage = new MutableLiveData<>();
mRepertory = new ImageRepertory();
idx = 0;
}
public MutableLiveData> getImage() {
return mImage;
}
public void LoadImage() {
mRepertory.getImage("js", idx, 1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(ImageBean imageBean) {
mImage.setValue(new Data(
imageBean.getImages().get(0), null
));
}
@Override
public void onError(Throwable e) {
mImage.setValue(new Data(
null, e.getMessage()
));
}
@Override
public void onComplete() {
}
});
}
public void nextImage() {
mRepertory.getImage("js", ++idx, 1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(ImageBean imageBean) {
mImage.setValue(new Data(
imageBean.getImages().get(0), null
));
}
@Override
public void onError(Throwable e) {
mImage.setValue(new Data(
null, e.getMessage()
));
idx--;
}
@Override
public void onComplete() {
}
});
}
public void previousImage() {
if (idx <= 0) {
mImage.setValue(new Data(
null, "已经是第一个了"
));
return;
}
mRepertory.getImage("js", --idx, 1)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(ImageBean imageBean) {
mImage.setValue(new Data(
imageBean.getImages().get(0), null
));
}
@Override
public void onError(Throwable e) {
mImage.setValue(new Data(
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布局文件不同的是,根标签改成了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>() {
@Override
public void onChanged(@Nullable Data 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文件中。