MVP架构实践

一.MVP理论简介

1.为何要在android中引入MVP

  在Android项目中,Activity和Fragment占据了大部分的开发工作。而MVP设计模式可以优化Activity和Fragment的代码。

  相信很多人阅读代码的时候,都是从Activity开始的,对着一个1000+行代码的Activity,看了都觉得难受。

  使用MVP之后,Activity就能瘦身许多了,基本上只有FindView、SetListener以及Init的代码。其他的就是对Presenter的调用,还有对View接口的实现。这种情形下阅读代码就容易多了,而且你只要看Presenter的接口,就能明白这个模块都有哪些业务,很快就能定位到具体代码。Activity变得容易看懂,容易维护,以后要调整业务、删减功能也就变得简单许多。

MVP模式优点:

  • 分离了视图逻辑和业务逻辑,降低了耦合
  • Activity只处理生命周期的任务,代码变得更加简洁
  • 视图逻辑和业务逻辑分别抽象到了View和Presenter的接口中去,提高代码的可阅读性
  • Presenter被抽象成接口,可以有多种具体的实现,所以方便进行单元测试
  • 把业务逻辑抽到Presenter中去,避免后台线程引用着Activity导致Activity的资源无法被系统回收从而引起内存泄露和OOM

2.MVP是什么

MVP架构实践_第1张图片

View 对应于Activity,负责View的绘制以及与用户交互。
Model 依然是业务逻辑和实体模型。一般封装了数据库DAO或者网络获取数据的角色,或者两种数据获取方式的集合。
Presenter 负责完成View于Model间的交互。

MVP会解除View和Model的耦合,同时又带来了良好的可扩展性、可测试性,保证了系统的整洁性和灵活性。

MVP可以分离显示层和逻辑层,他们之间通过接口进行通信,减低耦合。理想化的MVP模式可以实现同一份逻辑代码搭配不同的显示界面,因为他们之间并不依赖具体,而是依赖于抽象。这使得Presenter可以运用于任何实现了View逻辑接口的UI,使之具有更广泛的适用性,保证了灵活性。

3.MVP与MVC的区别

MVP架构实践_第2张图片

  其实最明显的区别就是,MVC中是允许Model和View进行交互的,而MVP中很明显,Model与View之间的交互由Presenter完成。还有一点就是Presenter与View之间的交互是通过接口的(代码中会体现)。

4.android中的MVP实现方案

方案1:MVP把Activity中的UI逻辑抽象成View接口,把业务逻辑抽象成Presenter接口,Model类还是原来的Model。这就是MVP模式,现在这样的话,Activity的工作的简单了,只用来响应生命周期,其他工作都丢到Presenter中去完成。

方案2:Activity作为Presenter,也有这种方案,不常见,也不易理解,这里不讲解。以下代码也主要以方案1为主。

二.android MVP实战demo

1.入门demo

public class WeatherEntity {

    /**
     * error : 0
     * status : success
     * date : 2016-07-18
     * results : [{"currentCity":"花都","pm25":"","index":[{"title":"穿衣","zs":"炎热","tipt":"穿衣指数","des":"天气炎热,建议着短衫、短裙、短裤、薄型T恤衫等清凉夏季服装。"},{"title":"洗车","zs":"较适宜","tipt":"洗车指数","des":"较适宜洗车,未来一天无雨,风力较小,擦洗一新的汽车至少能保持一天。"},{"title":"旅游","zs":"一般","tipt":"旅游指数","des":"天气较好,同时有微风相伴,但温度较高,天气热,请尽量避免高温时段外出,若外出请注意防暑降温和防晒。"},{"title":"感冒","zs":"少发","tipt":"感冒指数","des":"各项气象条件适宜,发生感冒机率较低。但请避免长期处于空调房间中,以防感冒。"},{"title":"运动","zs":"较适宜","tipt":"运动指数","des":"天气较好,户外运动请注意防晒。推荐您进行室内运动。"},{"title":"紫外线强度","zs":"中等","tipt":"紫外线强度指数","des":"属中等强度紫外线辐射天气,外出时建议涂擦SPF高于15、PA+的防晒护肤品,戴帽子、太阳镜。"}],"weather_data":[{"date":"周一 07月18日 (实时:33℃)","dayPictureUrl":"http://api.map.baidu.com/images/weather/day/qing.png","nightPictureUrl":"http://api.map.baidu.com/images/weather/night/duoyun.png","weather":"晴转多云","wind":"微风","temperature":"35 ~ 27℃"},{"date":"周二","dayPictureUrl":"http://api.map.baidu.com/images/weather/day/duoyun.png","nightPictureUrl":"http://api.map.baidu.com/images/weather/night/duoyun.png","weather":"多云","wind":"微风","temperature":"35 ~ 27℃"},{"date":"周三","dayPictureUrl":"http://api.map.baidu.com/images/weather/day/duoyun.png","nightPictureUrl":"http://api.map.baidu.com/images/weather/night/duoyun.png","weather":"多云","wind":"微风","temperature":"35 ~ 27℃"},{"date":"周四","dayPictureUrl":"http://api.map.baidu.com/images/weather/day/duoyun.png","nightPictureUrl":"http://api.map.baidu.com/images/weather/night/qing.png","weather":"多云转晴","wind":"微风","temperature":"35 ~ 27℃"}]}]
     */

    private int error;
    private String status;
    private String date;
    /**
     * currentCity : 花都
     * pm25 :
     * index : [{"title":"穿衣","zs":"炎热","tipt":"穿衣指数","des":"天气炎热,建议着短衫、短裙、短裤、薄型T恤衫等清凉夏季服装。"},{"title":"洗车","zs":"较适宜","tipt":"洗车指数","des":"较适宜洗车,未来一天无雨,风力较小,擦洗一新的汽车至少能保持一天。"},{"title":"旅游","zs":"一般","tipt":"旅游指数","des":"天气较好,同时有微风相伴,但温度较高,天气热,请尽量避免高温时段外出,若外出请注意防暑降温和防晒。"},{"title":"感冒","zs":"少发","tipt":"感冒指数","des":"各项气象条件适宜,发生感冒机率较低。但请避免长期处于空调房间中,以防感冒。"},{"title":"运动","zs":"较适宜","tipt":"运动指数","des":"天气较好,户外运动请注意防晒。推荐您进行室内运动。"},{"title":"紫外线强度","zs":"中等","tipt":"紫外线强度指数","des":"属中等强度紫外线辐射天气,外出时建议涂擦SPF高于15、PA+的防晒护肤品,戴帽子、太阳镜。"}]
     * weather_data : [{"date":"周一 07月18日 (实时:33℃)","dayPictureUrl":"http://api.map.baidu.com/images/weather/day/qing.png","nightPictureUrl":"http://api.map.baidu.com/images/weather/night/duoyun.png","weather":"晴转多云","wind":"微风","temperature":"35 ~ 27℃"},{"date":"周二","dayPictureUrl":"http://api.map.baidu.com/images/weather/day/duoyun.png","nightPictureUrl":"http://api.map.baidu.com/images/weather/night/duoyun.png","weather":"多云","wind":"微风","temperature":"35 ~ 27℃"},{"date":"周三","dayPictureUrl":"http://api.map.baidu.com/images/weather/day/duoyun.png","nightPictureUrl":"http://api.map.baidu.com/images/weather/night/duoyun.png","weather":"多云","wind":"微风","temperature":"35 ~ 27℃"},{"date":"周四","dayPictureUrl":"http://api.map.baidu.com/images/weather/day/duoyun.png","nightPictureUrl":"http://api.map.baidu.com/images/weather/night/qing.png","weather":"多云转晴","wind":"微风","temperature":"35 ~ 27℃"}]
     */

    private List<ResultsEntity> results;

    public void setError(int error) {
        this.error = error;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public void setDate(String date) {
        this.date = date;
    }

    public void setResults(List<ResultsEntity> results) {
        this.results = results;
    }

    public int getError() {
        return error;
    }

    public String getStatus() {
        return status;
    }

    public String getDate() {
        return date;
    }

    public List<ResultsEntity> getResults() {
        return results;
    }

    public static class ResultsEntity {
        private String currentCity;
        private String pm25;
        /**
         * title : 穿衣
         * zs : 炎热
         * tipt : 穿衣指数
         * des : 天气炎热,建议着短衫、短裙、短裤、薄型T恤衫等清凉夏季服装。
         */

        private List<IndexEntity> index;
        /**
         * date : 周一 0718日 (实时:33℃)
         * dayPictureUrl : http://api.map.baidu.com/images/weather/day/qing.png
         * nightPictureUrl : http://api.map.baidu.com/images/weather/night/duoyun.png
         * weather : 晴转多云
         * wind : 微风
         * temperature : 35 ~ 27℃
         */

        private List<WeatherDataEntity> weather_data;

        public void setCurrentCity(String currentCity) {
            this.currentCity = currentCity;
        }

        public void setPm25(String pm25) {
            this.pm25 = pm25;
        }

        public void setIndex(List<IndexEntity> index) {
            this.index = index;
        }

        public void setWeather_data(List<WeatherDataEntity> weather_data) {
            this.weather_data = weather_data;
        }

        public String getCurrentCity() {
            return currentCity;
        }

        public String getPm25() {
            return pm25;
        }

        public List<IndexEntity> getIndex() {
            return index;
        }

        public List<WeatherDataEntity> getWeather_data() {
            return weather_data;
        }

        public static class IndexEntity {
            private String title;
            private String zs;
            private String tipt;
            private String des;

            public void setTitle(String title) {
                this.title = title;
            }

            public void setZs(String zs) {
                this.zs = zs;
            }

            public void setTipt(String tipt) {
                this.tipt = tipt;
            }

            public void setDes(String des) {
                this.des = des;
            }

            public String getTitle() {
                return title;
            }

            public String getZs() {
                return zs;
            }

            public String getTipt() {
                return tipt;
            }

            public String getDes() {
                return des;
            }
        }

        public static class WeatherDataEntity {
            private String date;
            private String dayPictureUrl;
            private String nightPictureUrl;
            private String weather;
            private String wind;
            private String temperature;

            public void setDate(String date) {
                this.date = date;
            }

            public void setDayPictureUrl(String dayPictureUrl) {
                this.dayPictureUrl = dayPictureUrl;
            }

            public void setNightPictureUrl(String nightPictureUrl) {
                this.nightPictureUrl = nightPictureUrl;
            }

            public void setWeather(String weather) {
                this.weather = weather;
            }

            public void setWind(String wind) {
                this.wind = wind;
            }

            public void setTemperature(String temperature) {
                this.temperature = temperature;
            }

            public String getDate() {
                return date;
            }

            public String getDayPictureUrl() {
                return dayPictureUrl;
            }

            public String getNightPictureUrl() {
                return nightPictureUrl;
            }

            public String getWeather() {
                return weather;
            }

            public String getWind() {
                return wind;
            }

            public String getTemperature() {
                return temperature;
            }
        }
    }
}

public abstract class BasePresenter<T> {

    protected Reference<T> mViewRef;

    public void attachView(T view) {
        mViewRef = new WeakReference<T>(view);
    }

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

    public void detachView() {
        if (mViewRef != null) {
            mViewRef.clear();
            mViewRef = null;
        }
    }

}

public class WeatherPresenter extends BasePresenter<WeatherView> {

    public void fetchList() {
        mViewRef.get().onShowLoading();
        VolleyNetHelper.getInstance().doPost(new BaseRequest() {
            @Override
            public String getMobileApi() {
                return "http://api.map.baidu.com/telematics/v3/weather?location=guangzhou&output=json&ak=B95329fb7fdda1e32ba3e3a245193146";
            }

            @Override
            public Map<String, String> getParams() {
                return null;
            }
        }, new BaseResponse<WeatherEntity>() {
            @Override
            public void onSuccess(WeatherEntity o) {
                mViewRef.get().onHideLoading();
                mViewRef.get().onFetchDataSuccess(o);
            }

            @Override
            public void onError(String msg) {
                mViewRef.get().onHideLoading();
                mViewRef.get().onFetchDataError(msg);
            }

            @Override
            public Class<WeatherEntity> getResponseClass() {
                return WeatherEntity.class;
            }
        });


    }


}
public interface WeatherView {

    void onFetchDataSuccess(WeatherEntity entity);

    void onShowLoading();

    void onHideLoading();

    void onFetchDataError(String msg);
}
public class WeahterActivity extends BaseActivity implements WeatherView {

    @InjectView(R.id.weather_tv)
    TextView mWeatherTv;
    private WeatherPresenter mWeatherPresenter;

    @Override
    protected int getLayoutId() {
        return R.layout.activity_weather;
    }

    @Override
    protected void init(Bundle savedInstanceState) {
        mWeatherPresenter = new WeatherPresenter();
        mWeatherPresenter.attachView(this);
        mWeatherPresenter.fetchList();
    }

    @Override
    public void onFetchDataSuccess(WeatherEntity entity) {
        mWeatherTv.setText(entity.getDate());
    }

    @Override
    public void onShowLoading() {
        super.mLoadingDialog.showLoading(LoadingDialog.NETWORK_LOADING);
    }

    @Override
    public void onHideLoading() {
        super.mLoadingDialog.hideLoading();
    }

    @Override
    public void onFetchDataError(String msg) {
        ToastUtils.longShow(msg);
    }


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

其他入门demo
登陆案例:https://segmentfault.com/a/1190000003927200
天气预报案例:http://rocko.xyz/2015/02/06/Android%E4%B8%AD%E7%9A%84MVP/

2.项目级demo

https://github.com/maoruibin/GankDaily
http://p.codekk.com/detail/Android/gzsll/TLint

这里个人感觉TLint的MVP实践要更好一些。有需要的可以直接去看源码。

三.一些问题和难点

这里针对方案1:

  1. 例如当应用进入后台且内存不足的时候,系统是会回收这个Activity的。通常我们都知道要用OnSaveInstanceState()去保存状态,用OnRestoreInstanceState()去恢复状态。 但是在我们的MVP中,View层是不应该去直接操作Model的,这样做不合理,同时也增大了M与V的耦合。
  2. 界面复用问题。通常我们在APP最初版本中是无法预料到以后会有什么变动的,例如我们最初使用一个Fragment去作为界面的显示,后来在版本变动中发现这个Fragment越来越庞大,而Fragment的生命周期又太过复杂造成很多难以理解的BUG,我们需要把这个界面放到一个Activity中实现。这时候就麻烦了,要把Fragment转成Activity,这可不仅仅是改改类名的问题,更多的是一大堆生命周期需要去修改。例如参考文章2中的译者就遇到过这样的问题。
  3. Activity本身就是Android中的一个Context。不论怎么去封装,都难以避免将业务逻辑代码写入到其中。
  4. 如何防止activity等等内存泄露。这个问题非常严重,好好想一下有无好的方案。
  5. V,P对应的比例关系。

四.参考资料

https://segmentfault.com/a/1190000003927200
http://blog.csdn.net/lmj623565791/article/details/46596109
http://www.kymjs.com/code/2015/11/09/01
http://www.jianshu.com/p/9a6845b26856
https://github.com/122627018/BaseMVP
http://m.h5.com.cn/news/anzhuo/19287.html

你可能感兴趣的:(MVP架构实践)