Android MVP架構(RxJava+SQLBrite+Retrofit+OkHttp+Glide)

新根發表於2017-05-17

每年都有新的框架出來,比起以往的開發方式更高效,更簡潔,唯一成本是學習與踩坑。框架能實現的東西,android原生知識點也是能實現的,但是前者更效率高,節省時間。使用框架,花費更少的時間,更少的力去實現一個需求,何樂而不為?

若是 , 不熟悉MVP專案架構,可以閱讀Android MVP

這裡使用MVP專案架構外,還是用以下的框架:

  • Rxjava和RxAndroid :非同步響應式框架,替代非同步執行緒

  • SQLBrite :配合RxJava,替代ContentProvider+SQLIte+CursorLoader

  • Retrofit和OkHttp : 新的網路傳輸,替代HttpUrlConnection

  • Glide :載入圖片,視訊,GIF,可以替代傳統的載入影象資源方式

因此,這裡介紹Android MVP架構(RxJava+SQLBrite+Retrofit+OkHttp+Glide)來實現專案需求。

前期準備

專案中,在Gradle中引入框架的配置如下:

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    testCompile 'junit:junit:4.12'
    //android原生庫
    compile 'com.android.support:appcompat-v7:25.3.1' 
    compile 'com.android.support:recyclerview-v7:25.3.1'
    compile 'com.android.support:design:25.3.1' 

    //以下引入都是是採用的第三方庫

    //非同步載入影象
    compile 'com.github.bumptech.glide:glide:3.8.0'
    //非同步執行緒操作
    compile 'io.reactivex:rxjava:1.3.0'
    //UI執行緒操作
    compile 'io.reactivex:rxandroid:1.2.1' 
    //資料庫操作,但不是ORM
    compile 'com.squareup.sqlbrite:sqlbrite:1.1.1'
   //網路請求操作
    compile 'com.squareup.retrofit2:retrofit:2.3.0'
    compile 'com.squareup.okhttp3:okhttp:3.8.0'
    //解析資料,Gson方式json
    compile 'com.squareup.retrofit2:converter-gson:2.3.0'
    //網路日誌輸出
    compile 'com.squareup.okhttp3:logging-interceptor:3.8.0'
    //結合RxJava使用
    compile 'com.squareup.retrofit2:adapter-rxjava:2.3.0'
}

接下來,解下專案需求

一個電影列表介面:

image

一個切換介面的抽屜選單:

image

一個收藏列表的介面:

image

根據上面的頁面,歸納出以下功能點:

  • 電影列表
  • 選擇多部電影進行收藏。
  • 檢視被收藏的電影列表。

按模組劃分,可以分為電影列表模組,電影收藏模組。

根據功能點,開始編寫專案程式碼

專案按(多個)業務模組,工具包模組,資料模組,UI模組分層如下:

這裡寫圖片描述

1. 專案通用的BasePrester和BaseView介面

專案中通用的BasePresenter介面,具備訂閱和取消訂閱的行為:

public interface  BasePresenter {
    /**
     * 訂閱
     */
    void  subscribe();

    /**
     * 取消訂閱
     */
    void unsubscribe();
}

專案中通用的BaseView介面,具備繫結Presenter的行為:

public interface BaseView<T> {
    void setPresenter(T presenter);
}

2. 開始編寫Model中本地資料來源和遠端資料來源

2.1 本地資料來源模組編寫

資料庫中常量欄位:

public class MovieConstract implements BaseColumns {
    /**
     * 資料庫的資訊
     */
    public static final String SQLITE_NAME="movie.db";
    public static final int SQLITE_VERSON=1;
    /**
     * 表和欄位資訊
     */
    public static final  String TABLE_NAME_MOVI="movieData";
    public static final  String COLUMN_ID ="id";
    public static final String COLUMN_YEAR="year";
    public static final String COLUMN_TITLE="title";
    public static final String COLUMN_IMAGES="image";
    public static final String SQL_QUERY_MOVIE="select * from "+TABLE_NAME_MOVI;
}

SQLite資料庫配置:

public class MovieDataHelper extends SQLiteOpenHelper {
    public static final String CREATE_TABLE_MOVIE = "create table " +
            MovieConstract.TABLE_NAME_MOVI + "(" +
            MovieConstract._ID + " integer primary key autoincrement," +
            MovieConstract.COLUMN_ID + " text," +
            MovieConstract.COLUMN_TITLE + " text," +
            MovieConstract.COLUMN_YEAR + " text," +
            MovieConstract.COLUMN_IMAGES + " text"
            + ")";
    public MovieDataHelper(Context context) {
        super(context, MovieConstract.SQLITE_NAME, null, MovieConstract.SQLITE_VERSON);
    }
    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_TABLE_MOVIE);
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}

使用SQLBrite框架,運算元據庫:不熟悉這個框架,可以先閱讀SQLBrite框架

抽象出一個增,刪,查,改的行為的介面,在實現類中編寫SQLBrite操作:

public class MovieLocalSource implements LocalDataSource<MovieData> {
    private BriteDatabase briteDatabase;
    private static  MovieLocalSource instance;
    private Func1<Cursor,MovieData> cursorListFunc1;
    private MovieLocalSource(Context context , SchedulerProvider schedulerProvider){

        MovieDataHelper helper=new MovieDataHelper(context);
        //配置SQLBrite框架
        SqlBrite sqlBrite=new SqlBrite.Builder().build();
        this.briteDatabase= sqlBrite.wrapDatabaseHelper(helper,schedulerProvider.io());

        //查詢的資料返回Cusrsor將在這裡被回撥。
        this.cursorListFunc1=new Func1<Cursor, MovieData>() {
            @Override
            public MovieData call(Cursor cursor) {
                return TransformUtils.transformMovieData(cursor);
            }
        };
    }

    /**
     * 獲取例項
     * @param context
     * @param schedulerProvider
     * @return
     */
    public static MovieLocalSource getInstance(Context context , SchedulerProvider schedulerProvider){
        if(instance==null){
            instance=new MovieLocalSource(context,schedulerProvider);
        }
        return instance;
    }

    /**
     * 銷燬物件
     */
    public static void destroyInstance(){
        instance=null;
    }
    /**
     *
     *返回一個Observable物件,可以結合RxJava使用
     */
    @Override
    public Observable< List<MovieData>> queryAll() {
      return queryAction(MovieConstract.SQL_QUERY_MOVIE,null);
    }
    @Override
    public Observable< List<MovieData>> queryAction(String select, String[] selectArg) {
        QueryObservable observable= selectArg==null?this.briteDatabase.createQuery(MovieConstract.TABLE_NAME_MOVI,select):this.briteDatabase.createQuery(MovieConstract.TABLE_NAME_MOVI,select,selectArg);
        return observable.mapToList(this.cursorListFunc1);
    }
    @Override
    public long insert(MovieData movieData) {
        return this.briteDatabase.insert(MovieConstract.TABLE_NAME_MOVI,TransformUtils.transformMovieData(movieData));
    }
    /**
     *批量插入
     */
    @Override
    public int bulkInsert(List<MovieData> list) {
        int size=0;
        //開啟事物
        BriteDatabase.Transaction transaction= this.briteDatabase.newTransaction();
        try {
             for (MovieData movieData:list){
                 this.briteDatabase.insert(MovieConstract.TABLE_NAME_MOVI,TransformUtils.transformMovieData(movieData));
             }
            transaction.markSuccessful();
            size=list.size();
        }catch (Exception e){
            size=0;
            e.printStackTrace();
        }finally {
            transaction.end();
        }
        return size;
    }

    //專案中沒有刪,改操作,未編寫相關程式碼

    @Override
    public int update(MovieData movieData, String select, String[] selectArg) {
        return 0;
    }
    @Override
    public int delite(MovieData movieData, String select, String[] selectArg) {
        return 0;
    }
    @Override
    public void deliteAll() {
    }
}

2.2 遠端資料來源模組編寫

注意點:遠端資料可以分為文字資料和影象資料。

文字資料用Retrofit+OkHttp實現請求的響應

採用OkHttp作為Retrofit的傳輸層 , 需 引入OkHttp庫和OkHttp:logging-interceptor庫:

public class OkHttpProvider {
    /**
     * 自定義配置OkHttpClient
     * @return
     */
    public static OkHttpClient createOkHttpClient(){
        OkHttpClient.Builder builder=new OkHttpClient.Builder();
        HttpLoggingInterceptor loggingInterceptor=new HttpLoggingInterceptor();
        //列印一次請求的全部資訊
        loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        builder.addInterceptor(loggingInterceptor);
        return builder.build();
    }
}

標註點:採用豆瓣API中搜尋功能,這裡搜尋張藝謀的電影,返回前20個。

https://api.douban.com/v2/movie/search?q=張藝謀

先編寫Retrofit中請求中傳送的Body和Header,Respose的介面 :需要新增retrofit:adapter-rxjava庫,實現介面卡功能。

public interface DouBanService{
    //這裡返回一個Observable,用於RxJava結合使用
    @GET("{path}")
    Observable<MovieList> movieList(@Path("path") String path , @QueryMap Map<String,String> options);

}

配置Retrofit: 新增OkHttp作為傳輸層,RxJava介面卡,Gson解析的轉換器。

public class RemoteDataSource {
    private final String BASE_URL = "https://api.douban.com/v2/movie/";
    private final Retrofit retrofit;
    private static RemoteDataSource instance;
    private DouBanService douBanService;
    private       SchedulerProvider provider;
    private RemoteDataSource() {
        this.provider= SchedulerProvider.getInstacne();
        OkHttpClient okHttpClient = OkHttpProvider.createOkHttpClient();
        this.retrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(okHttpClient)//傳輸層
                .addConverterFactory(GsonConverterFactory.create())  //Gson解析
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())   //Rxjava介面卡
                .build();
        this.douBanService = this.retrofit.create(DouBanService.class);
    }

    /**
     * 單例類物件
     *
     * @return
     */
    public static synchronized RemoteDataSource getInstance() {
        if (instance == null) {
            instance = new RemoteDataSource();
        }
        return instance;
    }
    /*
     * 在Presenter呼叫,在subscriber訂閱者中響應
     */
    public Subscription movieList(Subscriber<List<Movie>> subscriber) {

        String url = "search";
        Map<String,String> map=new HashMap<>();
        map.put("q","張藝謀");
        Observable<MovieList> observable = this.douBanService.movieList(url, map);
        //floatMap操作符轉換
       Observable<List<Movie>> observable1= observable.flatMap(new Func1<MovieList, Observable<List<Movie>>>() {
           @Override
           public Observable<List<Movie>> call(MovieList movieList) {

               return  Observable.just(movieList.getSubjects());
           }
       });
       return observable1.subscribeOn(provider.io()).unsubscribeOn(provider.io()).observeOn(provider.ui()).subscribe(subscriber);
    }

}

接下來,編寫影象的配置:

影象資料用Glide實現載入

建立ImageLoader類,封裝呼叫方法,URI的自定義拼接,實現按影象尺寸來獲取最原始資料:來源:參考於Google IO APP中處理方式。

public class ImageLoader {
    private final CenterCrop mCenterCrop;
    private final BitmapTypeRequest<String> glideModelRequest;
    private static final ModelCache<String ,GlideUrl> urlCache=new ModelCache<>(150);
    private int mPlaceHolderResId=-1;

    public ImageLoader(Context context){
        /**
         * 轉換網址的操作類
         */
        CustomVariableWithImageLoader variableWithImageLoader=new CustomVariableWithImageLoader(context);
        /*
         * 總是將資源載入成一個Bitmap
         */
       this.glideModelRequest= Glide.with(context).using(variableWithImageLoader).from(String.class).asBitmap();

        BitmapPool bitmapPool=Glide.get(context).getBitmapPool();

        this.mCenterCrop=new CenterCrop(bitmapPool);
    }

    /**
     * 設定一個預設的 placehodler drawable
     * @param context
     * @param placeholdrResId
     */
    public ImageLoader(Context context,int placeholdrResId){
       this(context);
        this.mPlaceHolderResId=placeholdrResId;
    }
    public void loadImage(String url, ImageView imageView){
        loadImage(url,imageView,null);
    }
    public void loadImage(String url, ImageView imageView, RequestListener<String,Bitmap> requestListener){
        loadImage(url,imageView,requestListener,null,false);
    }
    public void loadImage(String url, ImageView imageView, RequestListener<String,Bitmap> requestListener, Drawable placeholderOverride, boolean crop){
        BitmapRequestBuilder request=beginImageLoad(url,requestListener,crop);
        if(placeholderOverride!=null){
            request.placeholder(placeholderOverride);
        }else if(mPlaceHolderResId!=-1){
            request.placeholder(mPlaceHolderResId);
        }
        request.into(imageView);

    }
    public BitmapRequestBuilder beginImageLoad(String url, RequestListener<String,Bitmap> requestListener,boolean crop){
        return  crop==true?this.glideModelRequest.load(url).listener(requestListener).transform(this.mCenterCrop):this.glideModelRequest.load(url).listener(requestListener);
    }

    /**
     * 載入Resouces下圖片資源.
     * @param context
     * @param drawableResId
     * @param imageView
     */
    public  void loadImage(Context context, int drawableResId, ImageView imageView){
        Glide.with(context).load(drawableResId).into(imageView);
    }
    private static  class  CustomVariableWithImageLoader extends BaseGlideUrlLoader<String>{
        /**
         * 解析格式
         */
        private static final Pattern PATTERN = Pattern.compile("__w-((?:-?\\d+)+)__");

        public CustomVariableWithImageLoader(Context context) {
            super(context,urlCache);
        }

        @Override
        protected String getUrl(String model, int width, int height) {
            Matcher matcher=PATTERN.matcher(model);
            int bestBucket=0;
            if(matcher.find()){
                String[] found=matcher.group(1).split("-");
                for (String bucketStr : found) {
                    bestBucket = Integer.parseInt(bucketStr);
                    if (bestBucket >= width) {
                        // the best bucket is the first immediately bigger than the requested width
                        break;
                    }
                }
                if (bestBucket > 0) {
                    model =matcher.replaceFirst("w"+bestBucket);
                }
            }
            return model;
        }
    }

}

3. 根據模組業務編寫View和Presenter及它的實現類

這裡,列舉:電影列表介面的模組

  • View告訴Presenter要載入資料,Presenter要獲取遠端資料來源,然後回撥的響應資料更新到UI上.

  • View告訴Presenter要收藏的電影,Presenter將收藏資料傳遞給本地資料來源,進行儲存,最後Presenter將儲存結果更新到UI上。

根據以上的View與Presenter互動,在一個合同介面中抽象出具體行為的View和Presenter。

public interface MovieListConstract {

    interface  Presenter extends BasePresenter {
        /**
         *  收藏的資料
         */
       void collectionMovie(List<Movie> list);
    }
    interface  View extends BaseView<Presenter> {
        /**
         *  載入從資料來源中獲取的資料
         */
          void loadMovieList(List<Movie> list);
          //最新結果響應在UI上
          void showToast(String s);
    }
}

在Fragment中實現View介面,實現具體操作:

public class MovieListFragment extends Fragment implements MovieListConstract.View, View.OnClickListener, SwipeRefreshLayout.OnRefreshListener {
    private View rootView;
    private RecyclerView recyclerView;
    private MovieListAdapter adapter;
    private ScrollChildSwipeRefreshLayout swipeRefreshLayout;
    public static final String TAG = MovieListFragment.class.getSimpleName();
    public static MovieListFragment newInstance() {
        return new MovieListFragment();
    }
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        this.rootView = inflater.inflate(R.layout.fragment_movielist, container, false);
        return this.rootView;
    }
    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

       // .......省略部分程式碼

        this.presenter.subscribe();
    }

    // .......省略部分程式碼

    @Override
    public void onPause() {
        super.onPause();
        //解除訂閱
        this.presenter.unsubscribe();
    }

    private MovieListConstract.Presenter presenter;

    @Override
    public void setPresenter(MovieListConstract.Presenter presenter) {
        this.presenter = presenter;
    }

    @Override
    public void showToast(String s) {
        Toast.makeText(BaseApplication.getAppContext(), s, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void loadMovieList(List<Movie> list) {
        this.adapter.upData(list);
        this.setLoadingIndicator(false);
    }

    @Override
    public void onClick(View v) {
        if (this.adapter.getMoviesCollecion().size() == 0) {
            showToast("請勾選中電影");
        } else {
            this.presenter.collectionMovie(this.adapter.getMoviesCollecion());
        }
    }

}

在一個Pesenter介面的實現類中:使用RxJava和RxAndroid實現非同步運算元據,和UI更新。

public class MovieListPresenter implements MovieListConstract.Presenter {
    private CompositeSubscription compositeSubscription;
    private MovieListConstract.View view;
    private LocalDataSource<MovieData> dataLocalDataSource;
    private RemoteDataSource remoteDataSource;
    private SchedulerProvider schedulerProvider;

    public MovieListPresenter(MovieListConstract.View view, LocalDataSource<MovieData> dataLocalDataSource, RemoteDataSource remoteDataSource) {
        this.compositeSubscription = new CompositeSubscription();
        this.dataLocalDataSource = dataLocalDataSource;
        this.remoteDataSource = remoteDataSource;
        this.schedulerProvider = SchedulerProvider.getInstacne();
        this.view = view;
        this.view.setPresenter(this);
    }

    @Override
    public void collectionMovie(final List<Movie> list) {
        Subscription subscription = Observable.create(new Observable.OnSubscribe<Boolean>() {
            @Override
            public void call(Subscriber<? super Boolean> subscriber) {
                List<MovieData> movieDataList = new ArrayList<>();
                for (Movie movie : list) {
                    movieDataList.add(TransformUtils.transformMovies(movie));
                }
                int size = dataLocalDataSource.bulkInsert(movieDataList);
                subscriber.onNext(size > 0 ? true : false);
                subscriber.onCompleted();
            }
        }).subscribeOn(schedulerProvider.io()).
                observeOn(schedulerProvider.ui()).
                subscribe(new Observer<Boolean>() {
                    @Override
                    public void onCompleted() {
                    }
                    @Override
                    public void onError(Throwable e) {
                        if (isViewBind()) {
                            String msg="收藏失敗";
                            view.showToast(msg);
                        }
                    }
                    @Override
                    public void onNext(Boolean aBoolean) {
                        if (isViewBind()) {
                            String msg=aBoolean==false?"收藏失敗":"收藏成功,可在收藏頁面檢視";
                            view.showToast(msg);
                        }
                    }
                });
        this.compositeSubscription.add(subscription);
    }

    @Override
    public void subscribe() {
        loadRemoteTask();
    }

    /**
     * 開始載入遠端的資料
     */
    private void loadRemoteTask() {
        Subscription subscription = remoteDataSource.movieList(new Subscriber<List<Movie>>() {
            @Override
            public void onCompleted() {
                if (isViewBind()) {
                    view.showToast("獲取列表成功");
                }
            }

            @Override
            public void onError(Throwable e) {
                if (isViewBind()) {
                    view.showToast("載入失敗");
                }
            }

            @Override
            public void onNext(List<Movie> list) {
                view.loadMovieList(list);
            }
        });
        this.compositeSubscription.add(subscription);
    }

    @Override
    public void unsubscribe() {
        this.compositeSubscription.clear();
    }

    /**
     * 檢查View是否被繫結
     *
     * @return
     */
    private boolean isViewBind() {
        return this.view == null ? false : true;
    }
}

最後Activity中,建立Presenter和View:

    private  MovieListConstract.Presenter presenter;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_movielist);
        .....

        MovieListFragment fragment=null;
        if(savedInstanceState!=null){
            fragment=(MovieListFragment) getSupportFragmentManager().findFragmentByTag(MovieListFragment.TAG);
        }else{
            fragment=MovieListFragment.newInstance();
            getSupportFragmentManager().beginTransaction().add(R.id.movielist_content_layout,fragment,MovieListFragment.TAG).commit();
        }
        this.presenter=new MovieListPresenter(fragment,MovieLocalSource.getInstance(BaseApplication.getAppContext(), SchedulerProvider.getInstacne()), RemoteDataSource.getInstance());

    }

電影收藏的業務也是類似,只要要抽出View與Presenter的互動行為,剩下的便是呼叫資料來源。
最好,可以結合Android MVP架構來加深理解。

4. 一些工具類,和UI類配置

轉換工具類:轉換常用物件

public class TransformUtils {
    /**
     *  將Cursor 生成MovieData物件
     * @param cursor
     * @return
     */
    public static MovieData transformMovieData(Cursor cursor) {
        MovieData movieData = new MovieData();
        movieData.setId(cursor.getString(cursor.getColumnIndex(MovieConstract.COLUMN_ID)));
        movieData.setTitle(cursor.getString(cursor.getColumnIndex(MovieConstract.COLUMN_TITLE)));
        movieData.setYear(cursor.getString(cursor.getColumnIndex(MovieConstract.COLUMN_YEAR)));
        movieData.setImages(cursor.getString(cursor.getColumnIndex(MovieConstract.COLUMN_IMAGES)));
        return movieData;
    }
    public static MovieData transformMovies(Movie movie){
        MovieData movieData=new MovieData();
        movieData.setId(movie.getId());
        movieData.setYear(movie.getYear());
        movieData.setTitle(movie.getTitle());
        movieData.setImages(movie.getImages().getLarge());
        return  movieData;
    }
    /**
     * 將Movie生成Cursor.
     * @param movie
     * @return
     */
    public static ContentValues transformMovieData(MovieData movie){
        ContentValues contentValues=new ContentValues();
        contentValues.put(MovieConstract.COLUMN_ID,movie.getId());
        contentValues.put(MovieConstract.COLUMN_TITLE,movie.getTitle());
        contentValues.put(MovieConstract.COLUMN_YEAR,movie.getYear());
        contentValues.put(MovieConstract.COLUMN_IMAGES,movie.getImages());
        return contentValues;
    }
    /**
     * 將Movie生成Cursor.
     * @param movie
     * @return
     */
    public static ContentValues transformMovie(Movie movie){
        ContentValues contentValues=new ContentValues();
        contentValues.put(MovieConstract.COLUMN_ID,movie.getId());
        contentValues.put(MovieConstract.COLUMN_TITLE,movie.getTitle());
        contentValues.put(MovieConstract.COLUMN_YEAR,movie.getYear());
        contentValues.put(MovieConstract.COLUMN_IMAGES,movie.getImages().getLarge());
        return contentValues;
    }
}

UI自定義類:SwipeRefreshLayout 支援非直接子類滾動檢視

ublic class ScrollChildSwipeRefreshLayout extends SwipeRefreshLayout {
    private View scrollUpChild;
    public ScrollChildSwipeRefreshLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 設定在哪個view中觸發重新整理。
     * @param view
     */
    public void setScrollUpChild(View view){
        this.scrollUpChild=view;
    }

    /**
     *ViewCompat..canScrollVertically():用於檢查view是否可以在某個方向上垂直滑動
     * @return
     */
    @Override
    public boolean canChildScrollUp() {
        if(scrollUpChild!=null){
            return ViewCompat.canScrollVertically(scrollUpChild,-1);
        }
        return super.canChildScrollUp();
    }

}

5. 專案執行效果如下

image

本專案的程式碼https://github.com/13767004362/MVPPractice

資源參考

相關文章