Android官方架構組件ViewModel+LiveData+DataBinding架構屬於本身的MVVM

Android官方架構組件ViewModel+LiveData+DataBinding架構屬於本身的MVVM

Demo運行效果

獲取Bing每日一圖並顯示

java

項目結構

實現過程

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
react


{
"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插件來自動生成。
android


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前綴信息。
git



因爲項目採用MVVM架構,View層與ViewModel層的通訊是經過LiveData這個架構組件實現的,不一樣於MVP架構中經過接口來通訊,因此還要對數據加載的狀態和錯誤信息進行維護。這裏建立一個包裝類來維護數據的狀態和錯誤信息,以便View層能夠對數據加載錯誤信息進行響應和處理。

public class Data {
private T mData;
private String mErrorMsg;



  
  
  
  

}
github

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的自定義屬性了。
app


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進行綁定。
mvvm

注意:xml文件中必定不能出現與業務相關的代碼!好比直接將ViewModel的訪問數據的方法在xml中與按鈕的點擊事件進行綁定,這種方作法是不可取的,由於XML文件的做用應該只是進行數據的顯示和用戶的交互,而訪問數據這種和業務相關的操做不該出如今XML文件中。
相關文章
相關標籤/搜索