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

60

我最近几周一直在尝试 MVP 模式,并且已经到了需要上下文来启动服务并访问共享首选项的地步。

我读到说 MVP 的目的是将视图与逻辑解耦,而在 Presenter 中包含上下文可能会破坏这个目的(如果我对此有误,请纠正我)。

目前,我有一个类似下面这样的 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();
    }
}

Presenter接口 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();
}

最后,我的Presenter:

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。我非常担心将上下文传递给我的Presenter。这样做可以吗?还是应该用其他方式?

编辑 实施Jahnold的第3个偏好设置

因此,让我们忽略接口和实现,因为它几乎是整个事情。现在我正在将共享首选项的接口注入到我的Presenter中。这是我的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));

这样可以吗?我的意思是现在我不必再将上下文从活动传递到我的 presenter 中,但我仍然有应用程序的上下文。


你可能还想在这里查看一个答案:https://dev59.com/O1wZ5IYBdhLWcg3wBcQJ#49936324 - Ali Nem
3个回答

76

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

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

从View中访问SharedPreferences

这是我最不喜欢的三种方法之一,因为访问SharedPreferences并不是一个视图操作。但它确实将Activity中的Android系统代码与Presenter隔开了。在您的视图界面中,加入一个方法:

boolean isLoggedIn();

可以从Presenter中调用。

使用Dagger注入SharedPreferences

由于您已经使用Dagger来注入事件总线,因此您可以将SharedPreferences添加到您的ObjectGraph中,从而获得一个使用ApplicationContext构建的SharedPreferences实例。这样一来,您就无需将Context传递到Presenter中即可获得它们。

这种方法的缺点是您仍然会传递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);
    }

}

接下来您需要使用Dagger将SharedPreferencesRepository接口注入到Presenter中。这样,在测试期间可以提供非常简单的模拟。在正常操作期间,提供具体实现。


我也喜欢最后一个,但是看实现的话,我还需要上下文对吧?所以在我的 DI 模块中,我必须指定某个地方的上下文,对吧?我问这个问题是因为我不知道如何设置注入。另外,服务也可以这样做吗? - remedy.
没关系,我弄明白了。工作得很好,但是我不确定背后的原理是如何运作的。我会更新我的问题,展示给你我进行注入的操作,并告诉我这是否是最佳选择。 - remedy.
你所做的看起来很好。应用程序上下文现在已从Presenter中隐藏,因为它被封装在SharedPreferencesRepository中。Presenter只知道存储库的存在。 - Jahnold
1
为什么存储库会做那样的事情?它是一个集合,必须保持相同类型的对象集合。我错了吗? - Alexandr
1
在这种情况下,“repository”只是位于数据(共享首选项、数据库、http等)和Presenter之间的层的名称。它是一种抽象,允许Presenter不关心数据来自哪里。它也可以被称为“DataManager”或您喜欢的任何名称。 - Jahnold
显示剩余3条评论

5

这个问题早就有了答案,假设 MVP 的定义是 OP 在他的代码中使用的,@Jahnold 的回答非常好。

然而,值得指出的是,MVP 是一个高层概念,遵循 MVP 原则可以有许多实现方法 - 有更多的方法来解决问题。

还有另一种 MVP 实现方式,它基于这样的想法:Android 中的活动不是 UI 元素,将 ActivityFragment 指定为 MVP 展示器。在这种配置中,MVP 展示器可以直接访问 Context

顺便提一下,即使在上述的 MVP 实现中,我也不会在展示器中使用 Context 来获取对 SharedPreferences 的访问 - 我仍然会为 SharedPreferences 定义一个包装类并将其注入到展示器中。


2
大多数域元素,例如DB或网络,需要构建上下文。它们不能在View中创建,因为View不能有任何关于Model的知识。它们必须在Presenter中创建。它们可以通过Dagger注入,但是它也使用了Context。所以,在Presenter xP中使用了Context。
窍门是,如果我们想要避免在Presenter中使用Context,那么我们可以只从Context创建所有这些Model对象的构造函数,并且不保存它。但是在我看来,这很愚蠢。Android中的新JUnit可以访问Context。
另一个窍门是使Context可为空,在域对象中应该有机制在上下文为空的情况下提供测试实例。我也不喜欢这个方法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接