2015-12-16 24 views
42

Ho giocato per qualche settimana con lo schema MVP e sono giunto al punto in cui ho bisogno di un contesto per avviare uno service e accedere allo Shared Preferences.Il presentatore che ha conoscenza dell'attività/contesto è una cattiva idea nel pattern MVP?

Ho letto che lo scopo di MVP è quello di disaccoppiare la vista dalla logica e avendo context all'interno di un Presenter può sconfiggere questo scopo (correggetemi se sbaglio su questo).

Attualmente, ho un LoginActivity che sembra qualcosa di simile:

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

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

Infine, il mio presentatore :

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

Come potete vedere, ho passato il contesto dal Activity nel mio Presenter solo così posso accedere al Shared Preferences. Sono abbastanza preoccupato di passare il contesto al mio presentatore. È una cosa giusta da fare? O dovrei farlo in un altro modo?

EDIT Implementato di Jahnold terza preferenza

Quindi cerchiamo di ignorare l'interfaccia e l'implementazione in quanto è praticamente l'intera cosa. Quindi ora sono injecting l'interfaccia per Sharedpreference nel mio presentatore. Ecco il mio codice per il 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()); } 

    } 
} 

Il modo in cui ricevo il contesto è da MyApplication.java

Quando inizia l'applicazione, faccio in modo di creare questo grafico Oggetto con questa riga di codice:

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

Va bene? Voglio dire che ora non devo passare il contesto dall'attività al mio presentatore, ma ho ancora il contesto dell'applicazione.

risposta

57

È passato un po 'di tempo da quando hai fatto questa domanda, ma ho pensato che sarebbe stato comunque utile fornire una risposta. Suggerisco caldamente che il presentatore non debba avere alcun concetto del contesto Android (o di altre classi Android). Separando completamente il codice Presenter dal codice di sistema Android, è possibile testarlo sulla JVM senza la complicazione dei componenti del sistema di simulazione.

Per raggiungere questo penso che tu abbia tre opzioni.

SharedPreferences accesso dalla vista

Questo è il mio preferito almeno dei tre, come l'accesso ai SharedPreferences è non un'azione vista. Tuttavia, mantiene il codice di sistema Android nell'Attività lontano dal Presenter. Nella tua vista l'interfaccia ha un metodo:

boolean isLoggedIn(); 

che può essere chiamato dal presentatore.

SharedPreferences Iniettare Uso Dagger

Come si sta già utilizzando Dagger per iniettare il bus caso in cui si potrebbe aggiungere al vostro SharedPreferences ObjectGraph e come tale sarebbe ottenere un'istanza SharedPreferences che è stato costruito utilizzando l'ApplicationContext. Questo li hai ottenuti senza dover passare un contesto al tuo presentatore.

Lo svantaggio di questo approccio è che si sta ancora passando una classe di sistema Android (SharedPreferences) e si dovrebbe prendere in giro quando si desidera testare il Presenter.

creare un'interfaccia SharePreferencesRepository

Questo è il mio metodo preferito per accedere ai dati SharedPreferences dall'interno di un presentatore. Fondamentalmente trattate SharedPreferences come un modello e disponete di un'interfaccia di repository per questo.

L'interfaccia potrebbe essere simile a:

public interface SharedPreferencesRepository { 

    boolean isLoggedIn(); 
} 

È quindi possibile avere una concreta attuazione di questo:

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

} 

E 'l'interfaccia SharedPreferencesRepository che poi inietta con il pugnale nella vostra Presenter. In questo modo, durante i test, è possibile fornire una simulazione molto semplice durante l'esecuzione. Durante il normale funzionamento viene fornita l'implementazione concreta.

+0

Mi piace anche l'ultimo, ma guardando l'implementazione, avrei ancora bisogno di un contesto giusto? Quindi, all'interno del mio modulo DI, devo specificare da qualche parte il contesto corretto? Lo chiedo perché non ho idea di come impostare questa iniezione. Inoltre, si può fare lo stesso con i servizi? –

+0

Non importa, l'ho capito. Funzionando bene, ma non sono sicuro di come funzioni dietro le quinte. Aggiornerò la mia domanda per mostrarti cosa ho fatto per eseguire l'iniezione e fammi sapere se questo non è ottimale. –

+0

Quello che hai fatto sembra a posto. Il contesto dell'applicazione è ora nascosto da Presenter perché è incapsulato all'interno del SharedPreferencesRepository. Tutto ciò che il presentatore conosce è il repository. – Jahnold

3

Questa domanda ha avuto risposta qualche tempo fa e, supponendo che la definizione di MVP sia l'OP utilizzata nel suo codice, la risposta di @Jahnold è davvero buona.

Tuttavia, è necessario sottolineare che MVP è un concetto di alto livello e che possono esserci molte implementazioni in base ai principi MVP: esiste più di un modo per applicare la skin al gatto.

There is another implementation of MVP, che si basa sull'idea che Activities in Android are not UI Elements, che designa Activity e Fragment come presentatori MVP. In questa configurazione, i relatori MVP hanno accesso diretto a Context.

proposito, anche nel summenzionato attuazione di MVP, non userei Context al fine di ottenere l'accesso a SharedPreferences in presenter - avrei ancora definire una classe wrapper per SharedPreferences e iniettarla presentatore.

1

La maggior parte degli elementi del dominio, come DB o rete, richiede che sia creato il contesto. Thay non può essere creato in Visualizza perché View non può avere alcuna conoscenza del Modello. Devono quindi essere creati in Presenter. Possono essere iniettati da Dagger, ma usa anche Context.Quindi Context è usato in Presenter xP

L'hack è che se vogliamo evitare Context in Presenter, possiamo semplicemente creare il costruttore che sta creando tutti questi oggetti del modello dal contesto e non salvandolo. Ma a mio parere, è stupido. Nuovo JUnit in Android ha accesso al contesto.

Un altro hack è rendere il contesto nullable, e negli oggetti di dominio dovrebbe esserci un meccanismo per fornire l'istanza di test in caso di null nel contesto. Anche a me non piace questo trucco.