在 MVP 模式中,了解活动/上下文的演示者是不是是个坏主意?

Posted

技术标签:

【中文标题】在 MVP 模式中,了解活动/上下文的演示者是不是是个坏主意?【英文标题】:Does the presenter having knowledge of the Activity / Context a bad idea in the MVP pattern?在 MVP 模式中,了解活动/上下文的演示者是否是个坏主意? 【发布时间】:2016-03-22 02:05:09 【问题描述】:

我已经用 MVP 模式玩了几个星期了,我已经到了需要上下文来启动 service 和访问 Shared Preferences 的地步。

我读到 MVP 的目的是将视图与逻辑分离,在 Presenter 中包含 context 可能会破坏该目的(如果我在这方面错了,请纠正我)。

目前,我有一个如下所示的 LoginActivity:

LoginActivity.java

public class LoginActivity extends Activity implements ILoginView 

    private final String LOG_TAG = "LOGIN_ACTIVITY";

    @Inject
    ILoginPresenter mPresenter;
    @Bind(R.id.edit_login_password)
    EditText editLoginPassword;
    @Bind(R.id.edit_login_username)
    EditText editLoginUsername;
    @Bind(R.id.progress)
    ProgressBar mProgressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        MyApplication.getObjectGraphpresenters().inject(this);
        mPresenter.setLoginView(this, getApplicationContext());
    

    @Override
    public void onStart() 
        mPresenter.onStart();
        ButterKnife.bind(this);
        super.onStart();
    

    @Override
    public void onResume() 
        mPresenter.onResume();
        super.onResume();
    

    @Override
    public void onPause() 
        mPresenter.onPause();
        super.onPause();
    

    @Override
    public void onStop() 
        mPresenter.onStop();
        super.onStop();
    

    @Override
    public void onDestroy() 
        ButterKnife.unbind(this);
        super.onDestroy();
    

    @OnClick(R.id.button_login)
    public void onClickLogin(View view) 
        mPresenter.validateCredentials(editLoginUsername.getText().toString(),
                editLoginPassword.getText().toString());
    

    @Override public void showProgress()  mProgressBar.setVisibility(View.VISIBLE); 

    @Override public void hideProgress() 
        mProgressBar.setVisibility(View.GONE);
    

    @Override public void setUsernameError()  editLoginUsername.setError("Username Error"); 

    @Override public void setPasswordError()  editLoginPassword.setError("Password Error"); 

    @Override public void navigateToHome() 
        startActivity(new Intent(this, HomeActivity.class));
        finish();
    

演示者接口 ILoginPresenter.java

public interface ILoginPresenter 
    public void validateCredentials(String username, String password);


    public void onUsernameError();

    public void onPasswordError();

    public void onSuccess(LoginEvent event);

    public void setLoginView(ILoginView loginView, Context context);

    public void onResume();

    public void onPause();

    public void onStart();

    public void onStop();

最后,我的演示者:

LoginPresenterImpl.java

public class LoginPresenterImpl implements ILoginPresenter 

    @Inject
    Bus bus;

    private final String LOG_TAG = "LOGIN_PRESENTER";
    private ILoginView loginView;
    private Context context;
    private LoginInteractorImpl loginInteractor;

    public LoginPresenterImpl() 
        MyApplication.getObjectGraph().inject(this);
        this.loginInteractor = new LoginInteractorImpl();
    

    /**
     * This method is set by the activity so that way we have context of the interface
     * for the activity while being able to inject this presenter into the activity.
     *
     * @param loginView
     */
    @Override
    public void setLoginView(ILoginView loginView, Context context) 
        this.loginView = loginView;
        this.context = context;

        if(SessionUtil.isLoggedIn(this.context)) 
            Log.i(LOG_TAG, "User logged in already");
            this.loginView.navigateToHome();
        
    

    @Override
    public void validateCredentials(String username, String password) 
        loginView.showProgress();
        loginInteractor.login(username, password, this);
    

    @Override
    public void onUsernameError() 
        loginView.setUsernameError();
        loginView.hideProgress();
    

    @Override
    public void onPasswordError() 
        loginView.setPasswordError();
        loginView.hideProgress();
    

    @Subscribe
    @Override
    public void onSuccess(LoginEvent event) 
        if (event.getIsSuccess()) 
            SharedPreferences.Editor editor =
                    context.getSharedPreferences(SharedPrefs.LOGIN_PREFERENCES
                            .isLoggedIn, 0).edit();
            editor.putString("logged_in", "true");
            editor.commit();

            loginView.navigateToHome();
            loginView.hideProgress();
        
    

    @Override
    public void onStart() 
        bus.register(this);
    

    @Override
    public void onStop() 
        bus.unregister(this);

    

    @Override
    public void onPause() 

    

    @Override
    public void onResume() 
    

如您所见,我将上下文从Activity 传递到我的Presenter 中,这样我就可以访问Shared Preferences。我很担心将上下文传递给我的演示者。这是一件好事吗?还是我应该以其他方式来做?

EDIT 实现了 Jahnold 的第三个偏好

所以让我们忽略接口和实现,因为它几乎就是全部。所以现在我是 injecting 将 Sharedpreference 的接口连接到我的演示者中。这是我的AppModule代码

AppModule.java

@Module(library = true,
    injects = 
            LoginInteractorImpl.class,
            LoginPresenterImpl.class,
            HomeInteractorImpl.class,
            HomePresenterImpl.class,

    
)
public class AppModule 

    private MyApplication application;

    public AppModule(MyApplication application) 
        this.application = application;
    

    @Provides
    @Singleton
    public RestClient getRestClient() 
        return new RestClient();
    

    @Provides
    @Singleton
    public Bus getBus() 
        return new Bus(ThreadEnforcer.ANY);
    

    @Provides
    @Singleton
    public ISharedPreferencesRepository getSharedPreferenceRepository()  return new SharedPreferencesRepositoryImpl(application.getBaseContext()); 

    

我获取上下文的方式来自MyApplication.java

当应用程序开始时,我确保使用这行代码创建这个对象图:

objectGraph = ObjectGraph.create(new AppModule(this));

这样好吗?我的意思是我现在不必将 Activity 的上下文传递给我的演示者,但我仍然拥有应用程序的上下文。

【问题讨论】:

您可能还想在这里查看答案:***.com/a/49936324/4514796 【参考方案1】:

大多数域元素,如数据库或网络,都需要构建上下文。 Thay 不能在 View 中创建,因为 View 对 Model 没有任何了解。然后必须在 Presenter 中创建它们。它们可以由 Dagger 注入,但它也可以使用 Context。所以在 Presenter xP 中使用了 Context

诀窍在于,如果我们想在 Presenter 中避免使用 Context,那么我们可以只创建从 Context 创建所有这些 Model 对象而不保存它的构造函数。但在我看来,这是愚蠢的。 android 中的新 JUnit 可以访问 Context。

另一个技巧是使 Context 可以为空,并且在域对象中应该有一种机制来提供测试实例以防上下文中的 null。我也不喜欢这种 hack。

【讨论】:

【参考方案2】:

这个问题前段时间已经回答过了,假设MVP的定义就是OP在他的代码中使用的,@Jahnold的回答真的很好。

但是,需要指出的是,MVP 是一个高级概念,遵循 MVP 原则可以有很多实现——给猫剥皮的方法不止一种。

There is another implementation of MVP,它基于Activities in Android are not UI Elements 的想法,将ActivityFragment 指定为MVP 演示者。在此配置中,MVP 演示者可以直接访问 Context

顺便说一句,即使在上述 MVP 的实现中,我也不会使用 Context 来访问演示者中的 SharedPreferences - 我仍然会为 SharedPreferences 定义一个包装类并将其注入主持人。

【讨论】:

【参考方案3】:

您问这个问题已经有一段时间了,但我认为无论如何提供答案都会很有用。我强烈建议演示者不应该有 Android 上下文(或任何其他 Android 类)的概念。通过将 Presenter 代码与 Android 系统代码完全分离,您可以在 JVM 上对其进行测试,而无需复杂的模拟系统组件。

要实现这一点,我认为您有三个选择。

从视图访问 SharedPreferences

这是三者中我最不喜欢的,因为访问 SharedPreferences 不是视图操作。但是,它确实使 Activity 中的 Android 系统代码远离 Presenter。在你的视图界面中有一个方法:

boolean isLoggedIn();

可以从演示者那里调用。

使用 Dagger 注入 SharedPreferences

由于您已经在使用 Dagger 注入事件总线,您可以将 SharedPreferences 添加到您的 ObjectGraph 中,因此将获得一个使用 ApplicationContext 构造的 SharedPreferences 实例。这是您无需将上下文传递给您的演示者即可获得它们。

这种方法的缺点是您仍在传递一个 Android 系统类 (SharedPreferences),并且在您想要测试 Presenter 时必须模拟它。

创建 SharePreferencesRepository 接口

这是我从 Presenter 中访问 SharedPreferences 数据的首选方法。基本上,您将 SharedPreferences 视为模型并为其提供存储库接口。

您的界面类似于:

public interface SharedPreferencesRepository 

    boolean isLoggedIn();

然后你可以有一个具体的实现:

public class SharedPreferencesRepositoryImpl implements SharedPreferencesRepository 

    private SharedPreferences prefs;

    public SharedPreferencesRepositoryImpl(Context context) 

        prefs = PreferenceManager.getDefaultSharedPreferences(context);
    

    @Override
    public boolean isLoggedIn() 

        return prefs.getBoolean(Constants.IS_LOGGED_IN, false);
    


它是 SharedPreferencesRepository 接口,然后你用 Dagger 将它注入你的 Presenter。这样,可以在测试期间在运行时提供一个非常简单的模拟。正常运行时提供具体实现。

【讨论】:

我也喜欢最后一个,但看看实现,我仍然需要上下文,对吧?所以在我的 DI 模块中,我必须在某个地方指定正确的上下文?我问这个是因为我对如何设置注射一无所知。另外,服务也可以做同样的事情吗? 没关系,我想通了。工作得很好,但我不确定这是如何在幕后工作的。我将更新我的问题,向您展示我为执行注射所做的工作,并让我知道这是否不是最佳的。 你所做的看起来不错。 Application Context 现在对 Presenter 隐藏,因为它封装在 SharedPreferencesRepository 中。 Presenter 只知道存储库。 为什么存储库会这样做?它是集合,它必须保持一组相同类型的对象。我错了吗? 在这种情况下,repository 只是位于数据(共享首选项、数据库、http 等)和 Presenter 之间的层的名称。这是一种抽象,允许 Presenter 不关心数据来自哪里。它同样可以称为 DataManager 或任何您喜欢的名称。

以上是关于在 MVP 模式中,了解活动/上下文的演示者是不是是个坏主意?的主要内容,如果未能解决你的问题,请参考以下文章

在 MVP 模式中,Presenter 应该是 Android 应用程序的活动方式还是功能方式

多个 MVP 演示者:如何沟通和共享信息?

android MVP - 我可以有多个演示者用于自定义视图和片段

在 MVP 模式中,适配器应该持有模型还是演示者应该持有模型并让适配器引用它?

使用 MVP 模式和 OO 原则

哪个是在MVP模式中初始化演示者的最佳方式(swift)