60

几周以来,我一直在使用 MVP 模式,现在我已经到了需要上下文来启动service和访问的地步Shared Preferences

我读过 MVP 的目的是将视图与逻辑分离,并且context可能Presenter会破坏该目的(如果我错了,请纠正我)。

目前,我有一个如下所示的 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到我的Presenterjust 中,这样我就可以访问Shared Preferences. 我很担心将上下文传递给我的演示者。这是一件好事吗?或者我应该以其他方式做吗?

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

所以让我们忽略接口和实现,因为它几乎就是全部。所以现在我是injectingSharedpreference 到我的演示者的接口。这是我的代码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));

这个可以吗?我的意思是我现在不必将活动中的上下文传递给我的演示者,但我仍然拥有应用程序的上下文。

4

3 回答 3

76

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

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

从视图访问 SharedPreferences

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

boolean isLoggedIn();

可以从演示者那里调用。

使用 Dagger 注入 SharedPreferences

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

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

于 2016-01-07T20:22:55.580 回答
5

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

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

还有另一种 MVP 实现,它基于Android 中的活动不是 UI 元素的想法,它指定ActivityFragment作为 MVP 演示者。在此配置中,MVP 演示者可以直接访问Context.

顺便说一句,即使在前面提到的 MVP 实现中,我也不会使用Context它来访问SharedPreferencesPresenter——我仍然会为它定义一个包装类SharedPreferences并将其注入到 Presenter 中。

于 2016-11-11T13:03:28.173 回答
2

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

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

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

于 2017-03-03T15:52:44.100 回答