MVPArms icon indicating copy to clipboard operation
MVPArms copied to clipboard

实际开发中对 MVP 使用上的优化

Open Yanqilong opened this issue 7 years ago • 14 comments
trafficstars

在使用框架过程中碰到 mvp 架构需创建文件数量庞问题,可能我的做法不正确,特请教下如何优化?

希望大佬能增加 DEMO 功能,提供多功能下如何设计和组织代码,为大家提供参考。以下是我的一些设计疑问,也贴出了源码希望和大家一起探讨如何设计。

比如用户模块下有以下功能

  • 登录
  • 注册

由于两个页面的 UI 、逻辑、网络请求都不一样,所以普通的做法是单独为每个功能创建 XXXActivity、XXXPresenter、创建 Contract 协议类、创建Model,比如一个登录功能就要创建四个文件吗?注册功能又要创建 4 个文件,意味着每个小功能都要创建 4 个文件,带来的问题是文件数量太过于庞大了,所以我想问有什么好的方案优化,你们是怎么处理的?

我的优化:

优化了协议类。不为每个功能创建 XXXContract ,由于我的是组件化工程,每个子工程下公用一个 Contract, 用内部类区分每个功能协议,这样减少了协议类的创建,比如用户模块

代码大致这样子:

public class UserContract {

    //--------- 微信授权 ----------
    public interface WxAuthView extends IView {
    }

    public interface WxAuthModel extends IModel {
        Observable<LoginBean> getLoginInfo(Map<String, String> reqMap);
    }

    //--------- 绑定手机 ----------
    public interface BindPhoneView extends IView {

        void enableVerificationCodeBtn(boolean enable);

        void setVerificationCodeText(String verificationCodeText);

    }

    public interface BindPhoneModel extends IModel {

        Observable<BaseBean> getVerificationCode(Map<String, String> reqMap);

        Observable<LoginBean> putBindPhoneNumber(Map<String, String> reqMap);
    }

    //--------- 选择区号 ----------
    public interface AreaCodeView extends IView {

        void showAreaCodeList(List<ShellAreaCodeBean.DataBean.AreaCodeBean> areaCodeBeanList);

        void onNetWorkError(String errorMsg);
    }

    public interface AreaCodeModel extends IModel {
        Observable<ShellAreaCodeBean> getAreaCodeList(Map<String, String> reqMap);
    }

    //--------- 登录 ----------
    public interface LoginView extends IView {
        void enableVerificationCodeBtn(boolean enable);

        void setVerificationCodeText(String verificationCodeText);

        void llCodeVisible(boolean visible, int loginType);
    }

    public interface LoginModel extends IModel {
        Observable<BaseBean> getVerificationCode(Map<String, String> reqMap);
        Observable<LoginBean> getLoginInfo(Map<String, String> reqMap);
    }

    //--------- 找回密码 ----------
    public interface FindPasswordView extends IView {

        void enableVerificationCodeBtn(boolean enable);

        void setVerificationCodeText(String verificationCodeText);

    }
    public interface FindPasswordModel extends IModel {
        Observable<BaseBean> getVerificationCode(Map<String, String> reqMap);
        Observable<BaseBean> putResetPassword(Map<String, String> reqMap);
    }

    //--------- 预填资料 ----------
    public interface PrepareInfoView extends IView {

    }
    public interface PrepareInfoModel extends IModel {
        Observable<BaseBean> postPrepareInfo(Map<String, String> reqMap);
    }

}

协议类复用好不好,提出来探讨,你们有什么好的优化方案?

优化了 Model 层,不为每个功能创建 Model ,由于我的组件化工程, 每个子工程复用 Model

代码大致这样子:

@ActivityScope
public class UserModel extends BaseModel
        implements UserContract.WxAuthModel, UserContract.BindPhoneModel,
        UserContract.AreaCodeModel, UserContract.LoginModel,
        UserContract.FindPasswordModel, UserContract.PrepareInfoModel {

    @Inject
    public UserModel(IRepositoryManager mRepositoryManager) {
        super(mRepositoryManager);
    }


    @Override
    public Observable<LoginBean> getLoginInfo(Map<String, String> reqMap) {
        return mRepositoryManager.obtainRetrofitService(UserService.class)
                .getLoginInfo(reqMap);
    }

    @Override
    public Observable<ShellAreaCodeBean> getAreaCodeList(Map<String, String> reqMap) {
        return mRepositoryManager.obtainRetrofitService(UserService.class)
                .getAreaCodeList(reqMap);
    }

    @Override
    public Observable<BaseBean> getVerificationCode(Map<String, String> reqMap) {
        return mRepositoryManager.obtainRetrofitService(UserService.class)
                .getVerificationCode(reqMap);
    }

    @Override
    public Observable<BaseBean> putResetPassword(Map<String, String> reqMap) {
        return mRepositoryManager.obtainRetrofitService(UserService.class)
                .putResetPassword(reqMap);
    }

    @Override
    public Observable<LoginBean> putBindPhoneNumber(Map<String, String> reqMap) {
        return mRepositoryManager.obtainRetrofitService(UserService.class)
                .putBindPhoneNumber(reqMap);
    }

    @Override
    public Observable<BaseBean> postPrepareInfo(Map<String, String> reqMap) {
        return mRepositoryManager.obtainRetrofitService(UserService.class)
                .postPrepareInfo(reqMap);
    }
}

Model复用好不好,提出来探讨,你们有什么好的优化方案?

经过上面优化可以大幅度 Contract 和 Model 的数量,只需要为每个功能创建 XXXActivity 和 XXXPresenter 类,应该每个功能都有自己的逻辑,所以我不建议复用 XXXPresenter。

这样优化的代价是让 DI 层的 XXXModule 层变得臃肿了 代码如下:

@Module
public class UserModule {

    private UserContract.WxAuthView wxAuthView;
    private UserContract.BindPhoneView bindPhoneView;
    private UserContract.AreaCodeView areaCodeView;
    private UserContract.LoginView loginView;
    private UserContract.FindPasswordView findPasswordView;
    private UserContract.PrepareInfoView prepareInfoView;

    /**
     * 构建 ActivityModule 时,将 View 的实现类传入,这样就可以提供 View 的实现类给 Presenter
     * @param view
     */
    public UserModule(UserContract.WxAuthView view) {
        this.wxAuthView = view;
    }

    public UserModule(UserContract.BindPhoneView view) {
        this.bindPhoneView = view;
    }

    public UserModule(UserContract.AreaCodeView view) {
        this.areaCodeView = view;
    }

    public UserModule(UserContract.LoginView view) {
        this.loginView = view;
    }

    public UserModule(UserContract.FindPasswordView view) {
        this.findPasswordView = view;
    }

    public UserModule(UserContract.PrepareInfoView view) {
        this.prepareInfoView = view;
    }

    //------ 微信授权 ---------
    @ActivityScope
    @Provides
    UserContract.WxAuthView provideWxAuthView() {
        return wxAuthView;
    }

    @ActivityScope
    @Provides
    UserContract.WxAuthModel provideWxAuthModel(UserModel model) {
        return model;
    }

    //------ 绑定手机号 ---------
    @ActivityScope
    @Provides
    UserContract.BindPhoneView provideBindPhoneView() {
        return bindPhoneView;
    }

    @ActivityScope
    @Provides
    UserContract.BindPhoneModel provideBindPhoneModel(UserModel model) {
        return model;
    }

    //------ 选择区号 ---------
    @ActivityScope
    @Provides
    UserContract.AreaCodeView provideAreaCodeView() {
        return areaCodeView;
    }

    @ActivityScope
    @Provides
    UserContract.AreaCodeModel provideAreaCodeModel(UserModel model) {
        return model;
    }

    //------ 登录 ---------
    @ActivityScope
    @Provides
    UserContract.LoginView provideLoginView() {
        return loginView;
    }

    @ActivityScope
    @Provides
    UserContract.LoginModel provideLoginModel(UserModel model) {
        return model;
    }

    //------ 发现密码 ---------
    @ActivityScope
    @Provides
    UserContract.FindPasswordView provideFindPasswordView() {
        return findPasswordView;
    }

    @ActivityScope
    @Provides
    UserContract.FindPasswordModel provideFindPasswordModel(UserModel model) {
        return model;
    }

    //------ 提交预填资料 ---------
    @ActivityScope
    @Provides
    UserContract.PrepareInfoView providePrepareInfoView() {
        return prepareInfoView;
    }

    @ActivityScope
    @Provides
    UserContract.PrepareInfoModel providePrepareInfoModel(UserModel model) {
        return model;
    }

    //------ Other ---------
    @ActivityScope
    @Provides
    Interrupter provideInterrupter() {
        Interrupter nsxInterrupter = new Interrupter();
        return nsxInterrupter;
    }

}

Yanqilong avatar Apr 18 '18 01:04 Yanqilong

谢谢你写这么多,我在常用 Issues 中已经说了,重用 Presenter 和 重用 Model 都没问题,包括你重用 Contract 也没问题,但是不可能每个业务逻辑都是一样的,有的复杂有的简单,所以没有一个可以统一并且完美的解决方案,在逻辑简单的页面重用上面说到的任何类都没问题,但如果在逻辑复杂或者变更特别频繁的地方就应该使用独立的几个类来处理,我只提供工具和脚手架,实际开发中只有你们自己按照自己的需求和实际情况灵活使用才能真正的发挥 Arms 的价值做好一个完美的软件

因为没有完美的解决方案,所以对任何情况都统一使用同一种解决方案都是愚蠢的,只有在不同情况下使用不同解决方案,发挥每个解决方案的长处才能尽可能的让软件完美,随机应变才是一个程序员解决问题的正确方向

JessYanCoding avatar Apr 18 '18 04:04 JessYanCoding

所以我建议大家在社群里多讨论,说出自己的想法,哪怕有瑕疵,但任何对 MVP 的优化意见我都是欢迎的,所有人看到后都可以积极的去尝试,这才叫开源,但是我并不会将某个方案作为官方的指定的统一解决方案,原因上面也说了

JessYanCoding avatar Apr 18 '18 04:04 JessYanCoding

我不完善 Demo 以展示更多情况无非有四点:

  1. 我不希望 Demo 太过复杂,让任何一个刚刚接触 Arms 的人就望而生畏,上面说的这些问题都是熟练 Arms 并且开始大量使用到实际开发中才会遇到的
  2. 我鼓励新手直接将 Demo 更改包名后直接作为开发的 Module,这样可以直接使用 Demo 的包结构,加快开发效率,所以不能让 Demo 太复杂,类太多,这样增加别人工作量
  3. 向我上面说的一样没有一个可以统一并且完美的解决方案,即使是我也不可能把所有情况考虑完整,所以 Demo 有最初级的使用方式就可以了,我不会官方的指定任何方案来约束复杂的情况,这样大家才能发挥自己想法随机应变,这才是成长
  4. 我很忙优化框架已经占用我很多时间,多维护一个代码,都是需要从其他时间里挤出来的,所以我希望大家自己讨论

最后再重申 Demo 的所有内容只是为了展示并不是让大家必须按照 Demo 这样做,Demo 只是作为参照,我时间都花在框架上,并没有花太多时间在 Demo

JessYanCoding avatar Apr 18 '18 04:04 JessYanCoding

大佬说的很有道理, 那这个 issue 先关闭了,如果以后大家在设计上碰到什么问题,在这里写出来讨论

Yanqilong avatar Apr 18 '18 04:04 Yanqilong

早上说来占领沙发,可怜不会回复代码。自己现在就公用了dagger 注入

cocowobo avatar Apr 18 '18 05:04 cocowobo

这个是一个优化方案,但是每个项目可能有自己的业务逻辑在里面,博主不可能把Demo都写出来,博主写的是框架、架构,不是写Demo,这两个要区分开,框架就是解决一系列问题而设计可扩展的支撑结构。在此结构上可以根据具体问题具体分析扩展、安插你自己更多的组成部分,这只是一个思想怎么灵活运用看自己

RockyQu avatar Apr 18 '18 05:04 RockyQu

类文件创建过多的问题也可以看看 MVPArt

RockyQu avatar Apr 18 '18 05:04 RockyQu

@Yanqilong 我建议 reopen 这个 issues,在你的方案和我的回答之上大家一起讨论更多可行的方案

JessYanCoding avatar Apr 18 '18 05:04 JessYanCoding

粒度自由控制

xiaobailong24 avatar Apr 18 '18 07:04 xiaobailong24

@RockyQu 谢谢你的提议,但可能有些误解我意思了,这 issue 主要是探讨下设计的问题和解决下我的疑惑,优化 Demo 使用场景是小小的建议,不是要求啊,当然大佬也说了为啥不增加复杂场景的原因了。

Yanqilong avatar Apr 19 '18 02:04 Yanqilong

可以再新建一个项目,专门介绍一个完成的功能或者流程,让新手对MVPArms框架理解更透彻

AndMy avatar Aug 27 '18 02:08 AndMy

感觉挺好的,确实可以减少文件创建并复用Contract和Module,你这大概就算是根据功能模块划分mvp,Demo中则是依照页面,相对来说demo方式文件更轻量,不好复用,但是与页面功能一一对应,容易查找,这个话相对于还是冗余一点,但是用在登陆注册类似的可重兴比较高的地方我感觉也是很不错的,总结来说就是如果当前的功能页面逻辑复杂,与其他可重用性小就独立创建多个文件,否则就可以考虑你这种复用

walkthehorizon avatar Aug 28 '18 07:08 walkthehorizon

我这里也说一下,我用Kotlin的最新版2.5.0的优化方案:

也是分模块,例如用户模块,假如有两个界面: 用户个人主页、登录,这两个界面都是 用户相关的,可以共用一套Contract和Module和Model,只需要创建对应的Component和Activity和Presenter即可。

用户个人主页 :

UserInfoActivity:

 class UserInfoActivity : BaseActivity<UserInfoPresenter>(), UserContract.View {
    override fun setupActivityComponent(appComponent: AppComponent) {
        DaggerUserInfoComponent.builder().appComponent(appComponent).view(this).build().inject(this)
    }
    override fun initView(savedInstanceState: Bundle?) = R.layout.activity_user_info
    override fun initData(savedInstanceState: Bundle?) {
        mPresenter?.getUserInfo("")
    }
    //获取用户信息完毕
    override fun showUserInfo(data: User) {}
    
    override fun showLoading(text: String?) {}

    override fun hideLoading() { }

    override fun killMyself() {
        finish()
    }
}

UserInfoComponent:

@ActivityScope
@Component(modules = arrayOf(UserModule::class), dependencies = arrayOf(AppComponent::class))
interface UserInfoComponent {
    fun inject(activity: UserInfoActivity)
    @Component.Builder
    interface Builder {
        @BindsInstance
        fun view(view: UserContract.View): Builder
        fun appComponent(appComponent: AppComponent): Builder
        fun build(): UserInfoComponent
    }
}

UserInfoPresenter:

@ActivityScope
class UserInfoPresenter
@Inject
constructor(model: UserContract.Model, rootView: UserContract.View) :
        BasePresenter<UserContract.Model, UserContract.View>(model, rootView) {
    @Inject
    lateinit var mErrorHandler: RxErrorHandler
    @Inject
    lateinit var mApplication: Application
    @Inject
    lateinit var mImageLoader: ImageLoader
    @Inject
    lateinit var mAppManager: AppManager
 
    override fun onDestroy() {
        super.onDestroy()
    }
    fun getUserInfo(userID: String) {
        RxUtils.applySchedulers<BaseJson<User>>(mRootView).apply(mModel.getUserInfo(userID))
                .subscribe(object : ErrorHandleSubscriber<BaseJson<User>>(mErrorHandler){
                    override fun onNext(bean: BaseJson<User>) {
                        if(bean.isAllSuc()){
                            mRootView.showUserInfo(bean.getData())
                        } 
                    } 
                })
    }
}

登录页面:

LoginActivity:

class LoginActivity : BaseActivity<LoginPresenter>(), UserContract.View {

    override fun setupActivityComponent(appComponent: AppComponent) {
        DaggerLoginComponent.builder().appComponent(appComponent).view(this).build().inject(this)
    }

    override fun initView(savedInstanceState: Bundle?) = R.layout.activity_login

    override fun initData(savedInstanceState: Bundle?) {
        initListener()
    }

    private fun initListener(){
        mBtnLogin.setOnClickListener {
            var phone = mETPhone.text.toString()
            var pass = mETPass.text.toString()
            mPresenter?.phoneLogin(phone,pass)
        }
    }

    //登录成功
    override fun showLoginSuc(data: User) {
        
    }

    //登录失败
    override fun showLoginFail(message: String?) {
        
    }

    override fun showLoading(text: String?) { }
    override fun hideLoading() { }
    override fun killMyself() {
        finish()
    }
}

LoginPresenter:

@ActivityScope
class LoginPresenter
@Inject
constructor(model: UserContract.Model, rootView: UserContract.View) :
        BasePresenter<UserContract.Model, UserContract.View>(model, rootView) {
    @Inject
    lateinit var mErrorHandler: RxErrorHandler
    @Inject
    lateinit var mApplication: Application
    @Inject
    lateinit var mImageLoader: ImageLoader
    @Inject
    lateinit var mAppManager: AppManager


    override fun onDestroy() {
        super.onDestroy()
    }

    fun verifyLogin(phone: String, pass: String): Boolean {
        return when{
            TextUtils.isEmpty(phone) ->{
                mRootView.showMessage(mApplication.getString(R.string.phone_not_empty_tip))
                false
            }
            TextUtils.isEmpty(pass) ->{
                mRootView.showMessage(mApplication.getString(R.string.pass_not_empty_tip))
                false
            }
            else ->{
                true
            }
        }
    }


    fun phoneLogin(phone: String, pass: String) {
        if(verifyLogin(phone,pass)){
            RxUtils.applySchedulers<BaseJson<User>>(mRootView,mApplication.getString(R.string.loading)).apply(mModel.phoneLogin(phone,pass))
                    .subscribe(object : ErrorHandleSubscriber<BaseJson<User>>(mErrorHandler){
                         override fun onNext(bean: BaseJson<User>) {
                            if(bean.isAllSuc()){
                                mRootView.showLoginSuc(bean.getData())
                            }else{
                                mRootView.showLoginFail(bean.msg)
                            }
                        }

                        override fun onError(t: Throwable) {
                            //super.onError(t)
                            mRootView.showLoginFail(t.message)
                        }
                    })
        }
    }
}

LoginComponent:

@ActivityScope
@Component(modules = arrayOf(UserModule::class), dependencies = arrayOf(AppComponent::class))
interface LoginComponent {
    fun inject(activity: LoginActivity)

    @Component.Builder
    interface Builder {
        @BindsInstance
        fun view(view: UserContract.View): Builder
        fun appComponent(appComponent: AppComponent): Builder
        fun build(): LoginComponent
    }
}

然后就是他们共用的Contact、module、modle:

UserContact:

interface UserContract {
    interface View : ITPView{
        fun showUserInfo(data: User){}
        fun showLoginFail(message: String?){}
        fun showLoginSuc(data: User){}

    }

    interface Model : IModel{
        fun phoneLogin(phone: String,pass: String): Observable<BaseJson<User>>
        fun getUserInfo(userID: String): Observable<BaseJson<User>>
    }
}

UserModule:

@Module
abstract class UserModule {

    @Binds
    internal abstract fun bindUserInfoModel(model: UserModel): UserContract.Model
}

UserModel:

@ActivityScope
class UserModel
@Inject
constructor(repositoryManager: IRepositoryManager) : BaseModel(repositoryManager), UserContract.Model {
    @Inject
    lateinit var mGson: Gson
    @Inject
    lateinit var mApplication: Application

    override fun onDestroy() {
        super.onDestroy()
    }

    override fun phoneLogin(phone: String,pass: String)  = mRepositoryManager.obtainRetrofitService(UserService::class.java).phoneLogin(phone,pass)

    override fun getUserInfo(userID: String) = mRepositoryManager.obtainRetrofitService(UserService::class.java).getUserInfo(userID)

}

tpnet avatar Jan 18 '19 04:01 tpnet

我也同意 JessYan 的说法,我觉得就应该比较清楚的使用不同的presenter,比较符合单一原则。就算目前的情况是presenter长的一样,以后呢?产品的需求一定是多变的。尽量避免使用helper,manager类来整合项目里相同的功能,说不定哪一天就变了,到时候一动而发全身,修改花费的时间代价是很高的。而且也不利于重构,业务层面脚手架多一点没有关系。

XWC95 avatar Mar 24 '19 03:03 XWC95