web-dev-qa-db-fra.com

Le présentateur ayant une connaissance de l'activité / du contexte est-il une mauvaise idée dans le modèle MVP?

Je joue avec le modèle MVP depuis quelques semaines maintenant et je suis arrivé au point où j'ai besoin de contexte pour démarrer un service et accéder à Shared Preferences.

J'ai lu que le but de MVP est de découpler la vue de la logique et d'avoir context dans un Presenter peut aller à l'encontre de cet objectif (corrigez-moi si je me trompe).

Actuellement, j'ai une LoginActivity qui ressemble à ceci:

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

Interface du présentateur 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();
}

Enfin, mon présentateur:

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() {
    }
}

Comme vous pouvez le voir, j'ai passé le contexte du Activity dans mon Presenter juste pour pouvoir accéder au Shared Preferences. Je suis très inquiet de passer le contexte à mon présentateur. Est-ce une chose correcte à faire? Ou devrais-je le faire d'une autre manière?

EDIT implémenté la 3ème préférence de Jahnold

Ignorons donc l'interface et l'implémentation car c'est à peu près tout. Alors maintenant, je suis injecting l'interface pour la préférence partagée dans mon présentateur. Voici mon code pour le 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()); }

    }
}

La façon dont j'obtiens le contexte est de MyApplication.Java

Lorsque l'application démarre, je m'assure de créer ce graphe d'objet avec cette ligne de code:

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

Est-ce correct? Je veux dire que je n'ai plus à transmettre le contexte de l'activité à mon présentateur, mais j'ai toujours le contexte de l'application.

55
remedy.

Cela fait un certain temps que vous n'avez pas posé cette question, mais j'ai pensé qu'il serait utile de fournir une réponse de toute façon. Je suggérerais fortement que le présentateur ne devrait avoir aucun concept du contexte Android (ou tout autre Android). En séparant complètement votre code Presenter du = Android code système vous pouvez le tester sur la JVM sans la complication des composants système moqueurs.

Pour y parvenir, je pense que vous avez trois options.

Accéder aux préférences partagées depuis la vue

C'est mon moins préféré des trois, car l'accès à SharedPreferences n'est pas une action de vue. Cependant, il conserve le code système Android dans l'activité à l'écart du présentateur. Dans votre interface d'affichage, disposez d'une méthode:

boolean isLoggedIn();

qui peut être appelé par le présentateur.

Injecter SharedPreferences à l'aide de la dague

Comme vous utilisez déjà Dagger pour injecter le bus d'événements, vous pouvez ajouter SharedPreferences à votre ObjectGraph et en tant que tel obtenir une instance SharedPreferences qui a été construite à l'aide d'ApplicationContext. C'était vous les obtenez sans avoir à passer un contexte dans votre présentateur.

L'inconvénient de cette approche est que vous passez toujours dans une classe système Android (SharedPreferences) et que vous deviez vous moquer de lui lorsque vous vouliez tester le présentateur.

Créer une interface SharePreferencesRepository

C'est ma méthode préférée pour accéder aux données SharedPreferences à partir d'un présentateur. Fondamentalement, vous traitez SharedPreferences comme un modèle et disposez d'une interface de référentiel pour celui-ci.

Votre interface serait similaire à:

public interface SharedPreferencesRepository {

    boolean isLoggedIn();
}

Vous pouvez alors avoir une implémentation concrète de ceci:

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);
    }

}

C'est l'interface SharedPreferencesRepository que vous injectez ensuite avec Dagger dans votre Presenter. De cette façon, une maquette très simple peut être fournie lors de l'exécution pendant les tests. Pendant le fonctionnement normal, la mise en œuvre concrète est fournie.

71
Jahnold

Cette question a été répondue il y a quelque temps, et, en supposant que la définition de MVP est celle utilisée par OP dans son code, la réponse de @Jahnold est vraiment bonne.

Cependant, il convient de souligner que le MVP est un concept de haut niveau, et il peut y avoir de nombreuses implémentations suivant les principes du MVP - il y a plus d'une façon d'habiller le chat.

Il existe une autre implémentation de MVP , qui est basé sur l'idée que Les activités dans Android ne sont pas des éléments d'interface utilisateur , qui désigne Activity et Fragment en tant que présentateurs MVP. Dans cette configuration, les présentateurs MVP ont un accès direct à Context.

Par ailleurs, même dans l'implémentation susmentionnée de MVP, je n'utiliserais pas Context pour avoir accès à SharedPreferences dans le présentateur - je définirais toujours une classe wrapper pour SharedPreferences et l'injecter dans le présentateur.

4
Vasiliy

La plupart des éléments de domaine, comme la base de données ou le réseau, nécessitent la création de contexte. Thay ne peut pas être créé dans View car View ne peut pas avoir de connaissances sur Model. Ils doivent ensuite être créés dans Presenter. Ils peuvent être injectés par Dagger, mais utilise-t-il également le contexte. Le contexte est donc utilisé dans Presenter xP

Le hack est que si nous voulons éviter le contexte dans Presenter, nous pouvons simplement créer le constructeur qui crée tous ces objets de modèle à partir du contexte et ne pas l'enregistrer. Mais à mon avis, c'est stupide. Nouveau JUnit dans Android a accès au contexte.

Un autre hack consiste à rendre le contexte nullable, et dans les objets de domaine, il devrait y avoir un mécanisme pour fournir une instance de test en cas de null dans le contexte. Je n'aime pas non plus ce hack.

2
MarcinM