“This is the 13th day of my participation in the Gwen Challenge in November. Check out the details: The Last Gwen Challenge in 2021”

preface

In WanAndroid, a combination of MVP + Retrofit + RxJava was used for development, and this article is a reworking of the MVP part of the project. Because I read a lot of MVP blogs, and there are different ways to do it. I combined the Google MVP example and some other blogs, in the project to achieve their own MVP framework, currently used in the project is relatively stable, so the framework of the implementation of the record, convenient future development.

MVC vs. MVP

One of the things that has to be said about MVP is how it compares to the MVC architecture pattern, as well as its strengths and weaknesses. MVC mode refers to the traditional architecture such as Model-View-Controller. Model represents the data layer and is used for data acquisition and processing. View interface layer, with the corresponding control to display data; Controller represents the control layer and is the bridge between the data layer and the interface layer. Controller controls the corresponding relationship between the data and the interface and processes the related event logic. In Android development, an Activity or Fragment usually plays the role of Controller, but it also plays the role of View. We initialize the Controller in the Activity (or Fragment) and load data for the control. Handling events such as clicks on controls and writing logic code in activities (or fragments) can result in an Activity (or Fragment) that is bloated and heavily coupled. Model-view-presenter is a variant of MVC. It separates all the business logic from the Activity and lets the Activity only handle the UI logic. All business logic unrelated to the Android API is handled by the Presenter layer.

In MVP mode, the Activity or Fragment acts as the View layer and handles only the UI, with no actual logical code. The Model layer is still used for data acquisition and processing. Presenter layer is the business processing layer that connects the Model layer and View layer. It can call UI logic and request data. The Presenter layer is pure Java class and does not involve any Android API. The call sequence between the three layers is View → Presenter → Model.

The downside of the MVP model is that it adds a lot of code and requires a lot of extra classes and interfaces.

Framework implementations

The base class to implement

The first step is to implement the Model, View, and Presenter base classes that provide the lowest level of abstraction.

BaseModel

public interface BaseModel {}Copy the code

BaseView

public interface BaseView{
    /** * displays the loading progress */
    void showLoading(a);


    /** * hide the loading progress */
    void hideLoading(a);
}
Copy the code

Two methods are provided to show and hide the progress bar when the page loads.

public abstract class BasePresenter<V extends BaseView> {

    protected Reference<V> mViewRef;//View weak reference to interface type
    
    public void attachView(V view){
        mViewRef = new WeakReference<>(view);
    }

    protected V getView(a){
        return mViewRef.get();
    }

    public boolean isViewAttached(a){
        returnmViewRef ! =null&& mViewRef.get() ! =null;
    }

    public void detachView(a){
        if(mViewRef ! =null){
            mViewRef.clear();
            mViewRef = null; }}public abstract  void start(a);
}
Copy the code

BasePresenter has an object of a BaseView subclass as a member variable. AttachView () is used to associate a specific View implementation class, namely an Activity or Fragment, with a weak reference. Avoid memory leaks.

BaseActivity

The fragment-related base class design is similar to that of an Activity, for example. BaseActivity is a required base class that encapsulates some of the most basic functionality for each subclass of Activity. Here’s a quick post on the BaseActivity implementation written in the project:

/** * Base class Activity, which handles some common logic */
public abstract class BaseActivity extends AppCompatActivity {

    private Unbinder unbinder;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(attachLayout());
        unbinder = ButterKnife.bind(this);
        initData();
        initView();
    }

    /** * Unbind ButterKnife */
    @Override
    protected void onDestroy(a) {
        super.onDestroy();
        if(unbinder ! =null&& unbinder ! = Unbinder.EMPTY) { unbinder.unbind(); unbinder =null; }}/** * Initializes data */
    protected abstract void initData(a);

    /** * Initialize the View control */
    protected abstract void initView(a);

    /** * Load the layout file **@return* /
    protected abstract int attachLayout(a);

    /** * handles the return button event **@param item
     * @return* /
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                finish();
                break;
        }
        return true;
    }

    /** * Check whether the user is logged in **@return* /
    protected boolean isLogined(a) {
        return newDataManager().getLoginState(); }}Copy the code

Here’s a look at some of the methods in the class. Using the ButterKnife framework in BaseActivity, binding ButterKnife in onCreate(), unbinding onDestroy(), This eliminates the need for repeated binding and unbinding in a specific Activity and allows you to use annotations such as @bindView directly.

There are also three abstract methods defined in BaseActivity:

  • InitData () : initializes data
  • InitView () : Initializes the control
  • AttachLayout () : Binds the layout file

These three abstract methods are used to standardize the Activity initialization and make the code more common.

As for the processing of the back button and determining whether the user is logged in or not, it depends on the specific project and will not be detailed here.

MVPBaseActivity

To apply the MVP mode, we add a MVPBaseActivity class that acts like A BaseActivity and adds View and Presenter associations to it. My thinking is that in projects where the interface logic is so simple that MVP mode is not necessary (to reduce the amount of code) and MVC mode can be used, the Activity can inherit BaseActivity. For interfaces with complex business logic, you can inherit MVPBaseActivity and use the MVP pattern for business decoupling.

public abstract class MVPBaseActivity<P extends BasePresenter>
        extends BaseActivity implements BaseView{

    protected P mPresenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        mPresenter = createPresenter();
        mPresenter.attachView(this);
        super.onCreate(savedInstanceState);
    }

    @Override
    protected void onDestroy(a) {
        super.onDestroy();
        mPresenter.detachView();
    }

    protected abstract P createPresenter(a);
}
Copy the code

MVPBaseActivity inherits from BaseActivity and adds support for MVP on it. The main implementation is to add a BasePresenter generic object as a member variable. Then bind Presenter and View in the onCreate() method and unbind in the onDestroy() method.

Specific business realization

After all the base classes are implemented, the MVP mode can be applied to specific businesses. Imagine a simple home page logic, where all articles can be displayed in a list on the home page of the App, and you can click the button in the list to favorites or unfavorites articles, and click the list item to open the details page of articles. We need to implement the following classes:

HomeContract

public interface HomeContract {
    interface Model extends BaseModel{
        Observable<Optional<ArticlePage>> getArticleList(int page);
        Observable<Optional<String>> collectArticle(int articleId);
        Observable<Optional<String>> unCollectArticle(int articleId);
    }

    interface View extends BaseView{

        /** * Displays a list of articles *@param articles
         */
        void showArticleList(List<ArticlePage.Article> articles);

        /** ** *@param position
         * @param success
         */
        void showCollectArticle(boolean success, int position);

        /** * display unfavorites *@param position
         * @param success
         */
        void showCancelCollectArticle(boolean success, int position);

        /** * Display open article details *@param article
         */
        void showOpenArticleDetail(ArticlePage.Article article);
	}

    abstract class Presenter extends BasePresenter<View>{

        /**
         * 获取文章列表
         */
        public abstract void getArticleList(a);

        /** * Open the article details page *@param article
         */
        public abstract void openArticleDetail(ArticlePage.Article article);


        /** **@param position
         * @param article
         */
        public abstract void collectArticle(int position, ArticlePage.Article article);

        /** * Unbookmark articles *@param position
         * @param article
         */
        public abstract void cancelCollectArticle(int position, ArticlePage.Article article); }}Copy the code

HomeContract is a contract interface written in the MVP sample from Google that binds the abstract definitions of all three parts of the MVP to a single interface. The main benefits I see are clearer code structure and fewer unnecessary class files (three class files in one). In a contract interface, there are three subinterfaces (Presenter is an abstract class) in which method definitions are defined and concrete implementation classes are created to implement these interfaces. An MVP framework is written. Let’s see how the framework is actually used to implement business functions:

HomeModel

HomeModel is an implementation class of Homecontract. Model that sends requests for data.

public class HomeModel implements HomeContract.Model{

    private WanAndroidService mService;

    public HomeModel(a){
        mService = RetrofitHelper.getInstance().getWanAndroidService();
    }

    @Override
    public Observable<Optional<ArticlePage>> getArticleList(int page) {
        return BaseModelFactory.compose(mService.getArticleList(page, null));
    }

    @Override
    public Observable<Optional<String>> collectArticle(int articleId) {
        return BaseModelFactory.compose(mService.collectArticle(articleId));
    }

    @Override
    public Observable<Optional<String>> unCollectArticle(int articleId) {
        returnBaseModelFactory.compose(mService.unCollectArticleFromArticleList(articleId)); }}Copy the code

Retrofit was used in the project to encapsulate network requests and implement asynchracy using RxJava, which will be described in more detail in future articles.

HomePresenter

HomePresenter inherits from Homecontract. Presenter, which is the concrete implementation class responsible for the logic.

public class HomePresenter extends HomeContract.Presenter {

    private HomeModel homeModel;

    private int mCurrentPage = 0;

    public HomePresenter(a){
        this.homeModel = new HomeModel();
    }


    @Override
    public void getArticleList(a) {
        mCurrentPage = 0;
        getTopArticles();
        homeModel.getArticleList(mCurrentPage).subscribe(new Consumer<Optional<ArticlePage>>() {
            @Override
            public void accept(Optional<ArticlePage> articlePage) throws Exception { getView().showArticleList(articlePage.getIncludeNull().getArticleList()); }}); }@Override
    public void loadMoreArticle(a) {
        mCurrentPage++;
        homeModel.getArticleList(mCurrentPage).subscribe(new Consumer<Optional<ArticlePage>>() {
            @Override
            public void accept(Optional<ArticlePage> articlePage) throws Exception {
                getView().showLoadMore(articlePage.getIncludeNull().getArticleList(), true); }},new Consumer<Throwable>() {
            @Override
            public void accept(Throwable throwable) throws Exception {
                ToastUtil.showToast("Failed to get article list data"); }}); }@Override
    public void openArticleDetail(ArticlePage.Article article) {
        getView().showOpenArticleDetail(article);
    }

    @Override
    public void collectArticle(final int position, ArticlePage.Article article) {
        homeModel.collectArticle(article.getId()).subscribe(new Consumer<Optional<String>>() {
            @Override
            public void accept(Optional<String> s) throws Exception {
                getView().showCollectArticle(true, position); }},new Consumer<Throwable>() {
            @Override
            public void accept(Throwable throwable) throws Exception {
                getView().showCollectArticle(false, position); }}); }@Override
    public void cancelCollectArticle(final int position, ArticlePage.Article article) {
        homeModel.unCollectArticle(article.getId()).subscribe(new Consumer<Optional<String>>() {
            @Override
            public void accept(Optional<String> s) throws Exception {
                getView().showCancelCollectArticle(true, position); }},new Consumer<Throwable>() {
            @Override
            public void accept(Throwable throwable) throws Exception {
                getView().showCancelCollectArticle(false, position); }}); }@Override
    public void start(a) {
        mCurrentPage = 0; getArticleList(); }}Copy the code

HomePresenter has a HomeModel member variable that calls methods in the Model layer.

HomeFragment

public class HomeFragment extends MVPBaseFragment<HomeContract.Presenter> implements HomeContract.View{

    @BindView(R.id.fhome_smart_refresh)
    SmartRefreshLayout mSmartRefreshLayout;
    com.youth.banner.Banner mBanner;
    @BindView(R.id.fhome_recyclerview)
    RecyclerView mRecyclerView;

    private ArticleAdapter mAdapter;
    public static HomeFragment newInstance(a){
        return new HomeFragment();
    }

    @Override
    public void onResume(a) {
        super.onResume();
        mPresenter.start();
    }


    @Override
    protected void initData(a) {}@Override
    protected void initView(View view) {

        // Initialize the Adapter
        mAdapter = new ArticleAdapter(R.layout.item_recyclerview_article,
                new ArrayList<ArticlePage.Article>(0));
        mAdapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() {
            @Override
            public void onItemClick(BaseQuickAdapter adapter, View view, int position) { ArticlePage.Article article = mAdapter.getData().get(position); mPresenter.openArticleDetail(article); }}); mAdapter.setOnItemChildClickListener(new BaseQuickAdapter.OnItemChildClickListener() {
            @Override
            public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
                    // If the article is already bookmarked, unbookmark it, if not, bookmark it
                if(mAdapter.getItem(position).isCollect()){
                    mPresenter.cancelCollectArticle(position, mAdapter.getItem(position));
                }else{ mPresenter.collectArticle(position, mAdapter.getItem(position)); }}});// Set the Adapter and layout manager of RecyclerView
        mRecyclerView.setAdapter(mAdapter);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));

        // Set the Header of SmartRefreshLayout
        mSmartRefreshLayout.setRefreshHeader(new MaterialHeader(getActivity()));
        // Set the listener for the pull-down refresh
        mSmartRefreshLayout.setOnRefreshListener(new OnRefreshListener() {
            @Override
            public void onRefresh(RefreshLayout refreshLayout) { mPresenter.getArticleList(); }});@Override
    protected int attachLayout(a) {
        return R.layout.fragment_home;
    }

    @Override
    public void showArticleList(List<ArticlePage.Article> articles) {
        mAdapter.addData(articles);
        mSmartRefreshLayout.finishRefresh();
    }


    @Override
    public void showCollectArticle(boolean success, int position) {
        if(success){
            ToastUtil.showToast("Collecting articles successfully");
            mAdapter.getData().get(position).setCollect(true);
            mAdapter.notifyDataSetChanged();
        }else{
            ToastUtil.showToast("Failed to collect articles"); }}@Override
    public void showCancelCollectArticle(boolean success, int position) {
        if(success){
            ToastUtil.showToast("Unbookmarking success");
            mAdapter.getData().get(position).setCollect(false);
            mAdapter.notifyDataSetChanged();
        }else{
            ToastUtil.showToast("Unbookmark article failed"); }}@Override
    public void showOpenArticleDetail(ArticlePage.Article article) {
        // Jump to ArticleDetailActivity with Title and URL
        ArticleDetailActivity.actionStart(getActivity(),
                article.getTitle(), article.getLink());
    }

    @Override
    public void showLoading(a) {}@Override
    public void hideLoading(a) {}@Override
    protected HomeContract.Presenter createPresenter(a) {
        return newHomePresenter(); }}Copy the code

In HomeFragment, you need to return a corresponding HomePresenter object in the createPresenter() method to bind to the HomeFragment.