您的位置:首页 > 理论基础 > 计算机网络

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

2017-05-17 18:54 525 查看
每年都有新的框架出来,比起以往的开发方式更高效,更简洁,唯一成本是学习与踩坑。框架能实现的东西,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'
}


接下来,解下项目需求

一个电影列表界面:



一个切换界面的抽屉菜单:



一个收藏列表的界面:



根据上面的页面,归纳出以下功能点:

电影列表

选择多部电影进行收藏。

查看被收藏的电影列表。

按模块划分,可以分为电影列表模块,电影收藏模块。

根据功能点,开始编写项目代码

项目按(多个)业务模块,工具包模块,数据模块,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. 项目运行效果如下



本项目的代码https://github.com/13767004362/MVPPractice

资源参考

Android MVP架构http://blog.csdn.net/hexingen/article/details/72058057

Android MVP架构(Volley+CursorLoader+ContentProvider): http://blog.csdn.net/hexingen/article/details/72082419
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: