RxJava2 Practical Knowledge Carding (15) - Implementing a Simple MVP + RxJava + Retrofit Application

Keywords: Retrofit Java network Android

RxJava2 Practical Series

RxJava2 Practical Knowledge Carding (1) - Time-consuming operation in the background, real-time notification of UI updates
RxJava2 Combat Knowledge Carding (2) - Calculating the Mean Value of Data over a Period of Time
RxJava2 Combat Knowledge Carding (3) - Optimizing Search Association Function
RxJava2 Combat Knowledge (4) - Request News Information in Combination with Retrofit
RxJava2 Practical Knowledge Carding (5) - Simple and Advanced Polling Operations
RxJava2 Practical Knowledge Carding (6) - Retry Request Based on Error Type
RxJava2 Practical Knowledge Carding (7) - Input Form Verification Based on combineLatest
RxJava2 Practical Knowledge Carding (8) - Use publish + merge to optimize the request process of loading caches before reading network data
RxJava2 Combat Knowledge (9) - Task Scheduling Using timer/interval/delay
RxJava2 Practical Knowledge Combing (10) - Screen Rotation Causes Activity Recovery Tasks in Reconstruction
RxJava2 Combat Knowledge Carding (11) - Detecting Network Status and Automatically Retrying Requests
RxJava2 Practical Knowledge Carding (12) - Practical Explanation Push & Reply & Share & refCount & autoConnect
RxJava2 Practical Knowledge Carding (13) - How to Make Subscription Relations Not Stop automatically when Errors Occur
RxJava2 Practical Knowledge Carding (14) - When token expires, refresh expired token and reissue requests
RxJava2 Practical Knowledge Carding (15) - Implementing a Simple MVP + RxJava + Retrofit Application

I. Preface

Unconsciously, from August 27, the first tutorial RxJava2 Practical Knowledge Carding (1) - Time-consuming operation in the background, real-time notification of UI updates Today is just two weeks, the purpose of this series of tutorials is mainly to let you have a more intuitive understanding of some operators in RxJava through some practical cases.

Today's article, which took several hours last night, simplifies the whole architecture of MVP + RxJava + Retrofit used in the project and extracts the reading that was written in the core part of it. Gank China and Latin America take examples of news and information.

The source code for this example can be passed through RxSample In Chapter 15, we first introduce the framework of an entire example.


Two, Model

Model corresponds to the NewsRepository in the figure above, which is responsible for providing data for the Presenter layer, because the data may come from caching or network, so there are two data sources in NewsRepository, namely LocalNewsSource and RemoteNewsSource:

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 implements information caching through a database, which contains two fields: information classification and specific data.

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

RemoteNews Source is responsible for pulling information from the network. Retrofit is used here to achieve this. Students who need to know can refer to the previous article. RxJava2 Combat Knowledge (4) - Request News Information in Combination with 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 and Presenter

3.1 Interface definitions for View and Presenter

First, we need to define interfaces for interaction between View and Presenter, which we write in NewsMvpControl:

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();
    }

}

The View layer defines two interfaces, which mean:

  • onRefreshFinished: Refresh the information list.
  • showTips: Give the user a hint when an error occurs.

Presenter layer is responsible for calling Model layer to get data after requesting from View layer, then calling back the interface of View layer to update the interface, so it provides two interfaces:

  • Refresh: Used to initiate refresh operations at the View layer.
  • destroy: When the View layer is destroyed, unsubscribe.

3.2 View Layer Implementation

Here, we implement Activity as the View layer implementation, which notifies RecyclerView to refresh when a new information list comes back, and prompts through SnackBar when prompts are needed. It also holds an instance of NewsPresenter and calls its destroy method to cancel subscriptions when destroyed.

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 Layer Implementation

Presenter layer is a complex place. When the View layer calls refresh interface, it needs to request different interfaces of Model layer to process according to the refresh type. When the data returns, it merges them with the current data and notifies the View layer to refresh.

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) { //The automatic refresh interval is 10 minutes.
                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()) { //To de-duplicate information, we need to rewrite the corresponding method of NewsBean.
            NewsBean newsBean = entityToBean(resultEntity);
            if (!mNewsBeans.contains(newsBean)) {
                filter.add(newsBean);
            }
        }
        if (refreshType == NewsMvpContract.REFRESH_CACHE && mNewsBeans.size() == 0) { //Caching is used only when there is no data at present.
            mNewsBeans = filter;
        } else if (refreshType == NewsMvpContract.REFRESH_AUTO) { //The automatically refreshed data is placed in the head.
            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("refresh error");
        }

        @Override
        public void onComplete() {}
    }
}

The reason why we define a refresh type here is that in a real project, we have the following refresh methods:

  • Read Cache: Add only when the list is empty.
  • Auto refresh: It needs to be judged according to the time, and it will be automatically acquired every 10 minutes after entering the interface.
  • Drop-down refresh: Add data to the head.
  • Pull-up refresh: Add data to the tail.

Here, in order to demonstrate the convenience, we have only realized the first two ways. You can also learn from the corresponding ways when dealing with them in the future.

Four, example

Next, let's demonstrate how to retrieve information in the networked state and re-enter it when the network is disconnected to read the cache:



The principle of this article is not much, because I believe that you are already familiar with MVP, mainly through a simple case to demonstrate how to use RxJava in MVP architecture, some of the code you have not posted can be seen. RxSample Chapter 15.

For more articles, please visit my Android Knowledge Carding Series:

Posted by PC Nerd on Tue, 01 Jan 2019 08:48:09 -0800