美文网首页androidRxjavaRxJava
RxJava2 实战知识梳理(15) - 实现一个简单的 MVP

RxJava2 实战知识梳理(15) - 实现一个简单的 MVP

作者: 泽毛 | 来源:发表于2017-09-09 14:13 被阅读1387次

    RxJava2 实战系列文章

    RxJava2 实战知识梳理(1) - 后台执行耗时操作,实时通知 UI 更新
    RxJava2 实战知识梳理(2) - 计算一段时间内数据的平均值
    RxJava2 实战知识梳理(3) - 优化搜索联想功能
    RxJava2 实战知识梳理(4) - 结合 Retrofit 请求新闻资讯
    RxJava2 实战知识梳理(5) - 简单及进阶的轮询操作
    RxJava2 实战知识梳理(6) - 基于错误类型的重试请求
    RxJava2 实战知识梳理(7) - 基于 combineLatest 实现的输入表单验证
    RxJava2 实战知识梳理(8) - 使用 publish + merge 优化先加载缓存,再读取网络数据的请求过程
    RxJava2 实战知识梳理(9) - 使用 timer/interval/delay 实现任务调度
    RxJava2 实战知识梳理(10) - 屏幕旋转导致 Activity 重建时恢复任务
    RxJava2 实战知识梳理(11) - 检测网络状态并自动重试请求
    RxJava2 实战知识梳理(12) - 实战讲解 publish & replay & share & refCount & autoConnect
    RxJava2 实战知识梳理(13) - 如何使得错误发生时不自动停止订阅关系
    RxJava2 实战知识梳理(14) - 在 token 过期时,刷新过期 token 并重新发起请求
    RxJava2 实战知识梳理(15) - 实现一个简单的 MVP + RxJava + Retrofit 应用


    一、前言

    不知不觉,从八月二十七号的第一篇教程 RxJava2 实战知识梳理(1) - 后台执行耗时操作,实时通知 UI 更新 到今天刚好两个星期,这一系列教程的目的主要是希望通过一些实际的案例,让大家对于RxJava中的一些操作符能有比较直观的认识。

    今天这篇文章,是昨天晚上花了几个小时,对项目中用到的MVP + RxJava + Retrofit的整个架构做了一个简化,抽离出其中最核心的部分编写的读取 Gank 中拉取新闻资讯的例子。

    该例子的源码可以通过 RxSample 的第十五章获取,下面我们先介绍一个整个例子的框架:

    二、Model

    Model对应上图中的NewsRepository,它负责为Presenter层提供数据,因为数据有可能来自缓存或者网络,因此在NewsRepository中包含了两个数据源,也就是LocalNewsSourceRemoteNewsSource

    public class NewsRepository {
    
        private LocalNewsSource mLocalNewsSource;
        private RemoteNewsSource mRemoteNewsSource;
    
        private NewsRepository() {
            mLocalNewsSource = new LocalNewsSource();
            mRemoteNewsSource = new RemoteNewsSource();
        }
    
        public static NewsRepository getInstance() {
            return Holder.INSTANCE;
        }
    
        private static class Holder {
            private static NewsRepository INSTANCE = new NewsRepository();
        }
    
        public Observable<NewsEntity> getNetNews(String category) {
            return mRemoteNewsSource.getNews(category).doOnNext(new Consumer<NewsEntity>() {
                @Override
                public void accept(NewsEntity newsEntity) throws Exception {
                    mLocalNewsSource.saveNews(newsEntity);
                }
            });
        }
    
        public Observable<NewsEntity> getCacheNews(String category) {
            return mLocalNewsSource.getNews(category);
        }
    
    }
    

    2.1 LocalNewsSource

    LocalNewsSource通过数据库的方式实现了资讯的缓存,该数据库的表包含两个字段,即资讯的分类,和资讯的具体数据。

    public class LocalNewsSource {
    
        private static final String[] QUERY_PROJECTION = new String[] { NewsContract.NewsTable.COLUMN_NAME_DATA };
        private static final String QUERY_SELECTION = NewsContract.NewsTable.COLUMN_NAME_CATEGORY + "= ?";
    
        private NewsDBHelper mNewsDBHelper;
        private SQLiteDatabase mSQLiteDatabase;
    
        public LocalNewsSource() {
            mNewsDBHelper = new NewsDBHelper(Utils.getAppContext());
            mSQLiteDatabase = mNewsDBHelper.getWritableDatabase();
        }
    
        public Observable<NewsEntity> getNews(String category) {
            return Observable.just(category).flatMap(new Function<String, ObservableSource<NewsEntity>>() {
                @Override
                public ObservableSource<NewsEntity> apply(String category) throws Exception {
                    NewsEntity newsEntity = new NewsEntity();
                    Cursor cursor = mSQLiteDatabase.query(NewsContract.NewsTable.TABLE_NAME, QUERY_PROJECTION, QUERY_SELECTION, new String[] { category }, null, null, null);
                    if (cursor != null && cursor.moveToNext()) {
                        String data = cursor.getString(cursor.getColumnIndex(NewsContract.NewsTable.COLUMN_NAME_DATA));
                        newsEntity = JSON.parseObject(data, NewsEntity.class);
                    }
                    if (cursor != null) {
                        cursor.close();
                    }
                    return Observable.just(newsEntity);
                }
            });
        }
    
    
        public void saveNews(NewsEntity newsEntity) {
            Observable.just(newsEntity).observeOn(Schedulers.io()).subscribe(new Consumer<NewsEntity>() {
    
                @Override
                public void accept(NewsEntity newsEntity) throws Exception {
                    if (newsEntity.getResults() != null && newsEntity.getResults().size() > 0) {
                        String cache = JSON.toJSONString(newsEntity);
                        ContentValues values = new ContentValues();
                        values.put(NewsContract.NewsTable.COLUMN_NAME_CATEGORY, "Android");
                        values.put(NewsContract.NewsTable.COLUMN_NAME_DATA, cache);
                        mSQLiteDatabase.insert(NewsContract.NewsTable.TABLE_NAME, null, values);
                    }
                }
            });
        }
    }
    

    2.2 RemoteNewsSource

    RemoteNewsSource则负责从网络上拉取资讯信息,这里用到了Retrofit来实现,有需要了解的同学可以参考前面的这篇文章 RxJava2 实战知识梳理(4) - 结合 Retrofit 请求新闻资讯

    public class RemoteNewsSource {
    
        private NewsApi mNewsApi;
    
        public RemoteNewsSource() {
            mNewsApi = new Retrofit.Builder()
                    .baseUrl("http://gank.io")
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .build().create(NewsApi.class);
        }
    
        public Observable<NewsEntity> getNews(String category) {
            return mNewsApi.getNews(category, 10, 1);
        }
    
    }
    

    三、View 和 Presenter

    3.1 View 和 Presenter 的接口定义

    首先,我们需要对于ViewPresenter之间的交互定义接口,这些接口我们写在NewsMvpContract中:

    public class NewsMvpContract {
    
        public static final int REFRESH_AUTO = 0;
        public static final int REFRESH_CACHE = 1;
    
        @IntDef ({REFRESH_AUTO, REFRESH_CACHE})
        public @interface RefreshType {}
    
        public interface View {
            void onRefreshFinished(@RefreshType int refreshType, List<NewsBean> newsEntity);
            void showTips(String message);
        }
    
        public interface Presenter {
            void refresh(@RefreshType int refreshType);
            void destroy();
        }
    
    }
    

    View层定义了两个接口,它们的含义分别为:

    • onRefreshFinished:刷新资讯列表。
    • showTips:在发生错误时给予用户提示。

    Presenter层则负责在View层发起请求之后,调用Model层获取数据,然后回调View层的接口进行界面的更新,因此它提供了两个接口:

    • refresh:用于View层发起刷新操作。
    • destroy:在View层销毁时,取消订阅。

    3.2 View 层实现

    这里,我们实现Activity作为View层的实现,它在有新的资讯列表回来时,通知RecyclerView进行刷新,而需要提示时则通过SnackBar进行提示,同时它还持有一个NewsPresenter的实例,并在销毁时调用它的destroy方法取消订阅。

    public class NewsMvpActivity extends AppCompatActivity implements NewsMvpContract.View {
    
        private CoordinatorLayout mRootLayout;
        private RecyclerView mRecyclerView;
        private NewsMvpAdapter mRecyclerAdapter;
        private List<NewsBean> mNewsBeans = new ArrayList<>();
        private NewsMvpContract.Presenter mPresenter;
        private LinearLayoutManager mLayoutMgr;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_news_mvp);
            initView();
            dispatchRefresh(NewsMvpContract.REFRESH_CACHE);
        }
    
        private void initView() {
            mRootLayout = (CoordinatorLayout) findViewById(R.id.cl_root);
            mRecyclerView = (RecyclerView) findViewById(R.id.rv_news);
            mRecyclerAdapter = new NewsMvpAdapter();
            mRecyclerAdapter.setNewsResult(mNewsBeans);
            mLayoutMgr = new LinearLayoutManager(this);
            mRecyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
            mRecyclerView.setLayoutManager(mLayoutMgr);
            mRecyclerView.setAdapter(mRecyclerAdapter);
            mPresenter = new NewsPresenter(this);
        }
    
        @Override
        protected void onResume() {
            super.onResume();
            dispatchRefresh(NewsMvpContract.REFRESH_AUTO);
        }
    
        private void dispatchRefresh(@NewsMvpContract.RefreshType int refreshType) {
            mPresenter.refresh(refreshType);
        }
    
        @Override
        public void onRefreshFinished(@NewsMvpContract.RefreshType int refreshType, List<NewsBean> newsBeans) {
            mNewsBeans.clear();
            mNewsBeans.addAll(newsBeans);
            mRecyclerAdapter.notifyDataSetChanged();
        }
    
        @Override
        public void showTips(String message) {
            Snackbar.make(mRootLayout, message, Snackbar.LENGTH_SHORT).show();
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            mPresenter.destroy();
        }
    }
    

    3.3 Presenter 层实现

    Presenter层是比较复杂的地方,当View层调用refresh接口时,它需要根据刷新的类型,请求Model层不同的接口进行处理,并且在数据返回时,将其和当前的数据进行合并,再通知View层进行刷新。

    public class NewsPresenter implements NewsMvpContract.Presenter {
    
        private static final long AUTO_REFRESH_TIME = 1000 * 60 * 10;
        private CompositeDisposable mCompositeDisposable;
        private NewsMvpContract.View mView;
        private List<NewsBean> mNewsBeans;
        private long mLastNetUpdateTime;
    
        public NewsPresenter(NewsMvpContract.View view) {
            mView = view;
            mCompositeDisposable = new CompositeDisposable();
            mNewsBeans = new ArrayList<>();
        }
    
        @Override
        public void refresh(@RefreshType int refreshType) {
            if (refreshType == NewsMvpContract.REFRESH_CACHE) {
                NewsRepository.getInstance()
                        .getCacheNews("Android")
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(new RefreshObserver(refreshType));
            } else {
                if (System.currentTimeMillis() - mLastNetUpdateTime > AUTO_REFRESH_TIME) { //自动刷新的间隔时间为十分钟。
                    NewsRepository.getInstance()
                            .getNetNews("Android")
                            .subscribeOn(Schedulers.io())
                            .observeOn(AndroidSchedulers.mainThread())
                            .subscribe(new RefreshObserver(refreshType));
                }
            }
        }
    
        @Override
        public void destroy() {
            mCompositeDisposable.clear();
            mView = null;
        }
    
        private void updateNewsBeans(@NewsMvpContract.RefreshType int refreshType, NewsEntity newsEntity) {
            List<NewsBean> filter = new ArrayList<>();
            for (NewsResultEntity resultEntity : newsEntity.getResults()) { //对资讯进行去重,需要重写NewsBean的对应方法。
                NewsBean newsBean = entityToBean(resultEntity);
                if (!mNewsBeans.contains(newsBean)) {
                    filter.add(newsBean);
                }
            }
            if (refreshType == NewsMvpContract.REFRESH_CACHE && mNewsBeans.size() == 0) { //只有当前没有数据时,才使用缓存。
                mNewsBeans = filter;
            } else if (refreshType == NewsMvpContract.REFRESH_AUTO) { //自动刷新的数据放在头部。
                mNewsBeans.addAll(0, filter);
                mLastNetUpdateTime = System.currentTimeMillis();
            }
        }
    
        private NewsBean entityToBean(NewsResultEntity resultEntity) {
            String title = resultEntity.getDesc();
            NewsBean bean = new NewsBean();
            bean.setTitle(title);
            return bean;
        }
    
        private class RefreshObserver extends DisposableObserver<NewsEntity> {
    
            private @NewsMvpContract.RefreshType int mRefreshType;
    
            RefreshObserver(@NewsMvpContract.RefreshType int refreshType) {
                mRefreshType = refreshType;
            }
    
            @Override
            public void onNext(NewsEntity newsEntity) {
                updateNewsBeans(mRefreshType, newsEntity);
                mView.onRefreshFinished(mRefreshType, mNewsBeans);
            }
    
            @Override
            public void onError(Throwable throwable) {
                mView.showTips("刷新错误");
            }
    
            @Override
            public void onComplete() {}
        }
    }
    

    这里我们之所以要定义一个刷新类型的目的在于,在实际项目中,我们有以下几种刷新方式:

    • 读取缓存:只在列表为空时才添加。
    • 自动刷新:需要根据时间判断,每隔10分钟进入一次界面后自动获取。
    • 下拉刷新:将数据添加在头部。
    • 上拉刷新:将数据添加在尾部。

    这里为了演示方便我们只实现了前两种方式,大家以后在处理时,也可以借鉴相应的方式。

    四、示例

    下面,我们演示一下,先在联网状态下拉取资讯,再在断网情况下重新进入,读取缓存的情况:


    这篇文章原理的东西不多,因为相信大家对于MVP也已经很熟悉,主要是通过一个简单的案例演示一个如何在MVP架构当中使用RxJava,有部分没有贴出来的代码大家可以查看 RxSample 中的第十五章。

    更多文章,欢迎访问我的 Android 知识梳理系列:

    相关文章

      网友评论

      • seraphzxz::+1: ,看完了你这个系列的文章很有收获。
        泽毛:@seraphzxz 😁谢谢支持
      • 夜幕流星雨:用flatmap来实现注册后的登录,如果网络请求失败(比如请求超时),如何判断是注册失败,还是登录失败
        seraphzxz:在注册订阅前 doNext 处理一下,不过还没尝试。
      • ec2612df5cb8:楼主用的meizu手机。感谢分享:+1:
        泽毛:@ec2612df5cb8 哈哈,被你发现了:smiley:
      • 5ae450c18910:看完了你的RxJava全部文章,很受用,赞👍
        泽毛: @Leu_Z 😝谢谢你的支持啊,如果在实际中遇到什么问题可以随时私信我,一起交流哈

      本文标题:RxJava2 实战知识梳理(15) - 实现一个简单的 MVP

      本文链接:https://www.haomeiwen.com/subject/suwojxtx.html