演示者了解 Activity / Context 在 MVP 模式中是个坏主意吗?

Does the presenter having knowledge of the Activity / Context a bad idea in the MVP pattern?

我已经研究 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。我很担心将上下文传递给我的演示者。这是一件好事吗?或者我应该用其他方式来做吗?

编辑实现了 Jahnold 的第三个偏好

所以让我们忽略接口和实现,因为它几乎就是全部。所以现在我 injecting 共享首选项的界面进入我的演示者。这是我的 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 传递到我的演示者,但我仍然有应用程序的上下文。

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

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

从视图访问 SharedPreferences

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

boolean isLoggedIn();

可以从演示者那里调用。

使用 Dagger 注入 SharedPreferences

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

这种方法的缺点是您仍在传递 Android 系统 class (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 中。这样可以在测试期间在运行时提供一个非常简单的模拟。正常运行时给出具体实现。

这个问题前段时间有人回答过,假设MVP的定义是OP在他的代码中使用的,@Jahnold的回答非常好。

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

There is another implementation of MVP, which is based on the idea that Activities in Android are not UI Elements,指定 ActivityFragment 为 MVP 演示者。在此配置中,MVP 演示者可以直接访问 Context.

顺便说一下,即使在前面提到的 MVP 实现中,我也不会使用 Context 来访问 Presenter 中的 SharedPreferences - 我仍然会定义一个包装器 class 用于 SharedPreferences 并将其注入演示者。

大多数领域元素,如数据库或网络,都需要构建上下文。无法在 View 中创建 Thay,因为 View 无法了解 Model。然后必须在 Presenter 中创建它们。它们可以通过 Dagger 注入,但它是否也使用 Context.所以在Presenter中使用了Context xP

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

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