2015-12-16 18 views
42

Przez kilka tygodni bawiłem się wzorcem MVP i doszedłem do punktu, w którym potrzebuję kontekstu, aby rozpocząć service i uzyskać dostęp do Shared Preferences.Czy prezenter znający działanie/kontekst jest złym pomysłem we wzorcu MVP?

Przeczytałem, że celem MVP jest oderwanie widoku od logiki i posiadanie context wewnątrz Presenter może pokonać ten cel (popraw mnie jeśli się mylę w tym).

Obecnie mam LoginActivity, który wygląda mniej więcej tak:

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

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

Jak widać, zdałem kontekst z Activity do mojego Presenter tylko tak mogę uzyskać dostęp do Shared Preferences. Martwię się, że przekazuję kontekst mojemu prezenterowi. Czy to jest w porządku? Czy powinienem to robić w inny sposób?

EDIT Zaimplementowane Jahnold za 3-te preferencji

Więc ignorować interfejsu i implementacji, ponieważ jest to dość dużo cała sprawa. Teraz jestem injecting interfejsem dla Sharedpreference w moim prezenterem. Oto mój kod na 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()); } 

    } 
} 

Sposób uzyskać kontekst jest z MyApplication.java

Kiedy zaczyna aplikacja, I upewnij się, aby stworzyć ten wykres obiektów z tej linii kodu:

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

Czy to w porządku? Chodzi mi o to, że teraz nie muszę przekazywać kontekstu z działania do mojego prezentera, ale wciąż mam kontekst aplikacji.

Odpowiedz

57

Minęło trochę czasu, odkąd zadałeś to pytanie, ale pomyślałem, że i tak byłoby użyteczne udzielenie odpowiedzi. Zdecydowanie sugerowałbym, aby prezenter nie miał pojęcia kontekstu Android (ani żadnych innych klas Android). Całkowicie oddzielając kod prezentera od kodu systemu Android, możesz przetestować go na maszynie wirtualnej JVM bez komplikacji związanych z draniem komponentów systemu.

Aby to osiągnąć, myślę, że masz trzy opcje.

dostępu SharedPreferences z widoku

To jest mój ulubiony najmniej z trzech, jak dostęp SharedPreferences jest nie akcja widzenia. Jednak zachowuje kod systemu Android w Activity poza Presenterem. W interfejsie widoku masz metodę:

boolean isLoggedIn(); 

, którą można wywołać z prezentera.

Inject SharedPreferences Korzystanie Dagger

Jak już używasz Dagger wstrzyknąć autobus zdarzeń można dodać SharedPreferences do ObjectGraph i jako takie byłoby uzyskać instancji SharedPreferences który został zbudowany przy użyciu ApplicationContext. Otrzymałeś je bez konieczności przekazywania kontekstu do swojego prezentera.

Wadą tego podejścia jest to, że nadal przechodzisz w klasie systemu Android (SharedPreferences) i będziesz musiał kpić z niej, gdy chcesz przetestować prezentera.

utworzyć interfejs SharePreferencesRepository

To moja preferowana metoda dostępu do danych SharedPreferences od wewnątrz prezentera. Zasadniczo traktujesz SharedPreferences jako model i posiadasz interfejs repozytorium dla niego.

interfejs byłby podobny do:

public interface SharedPreferencesRepository { 

    boolean isLoggedIn(); 
} 

Można wtedy mieć konkretną realizację tego:

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

} 

Jest to interfejs SharedPreferencesRepository które następnie wstrzyknąć z sztylet w swojej Presenter. W ten sposób podczas testów można zapewnić bardzo prosty przykład. Podczas normalnej pracy zapewniona jest konkretna realizacja.

+0

Podoba mi się również ten ostatni, ale patrząc na implementację, nadal potrzebowałbym odpowiedniego kontekstu? Więc Wewnątrz mojego modułu DI, muszę określić gdzieś właściwy kontekst? Pytam o to, ponieważ nie mam bladego pojęcia, jak ustawić ten zastrzyk. Czy to samo można zrobić z usługami? –

+0

Nieważne, rozgryzłem to. Działa dobrze, ale nie jestem pewien, jak to działa za kulisami. Zaktualizuję moje pytanie, aby pokazać, co zrobiłem, aby wykonać zastrzyk, i daj mi znać, jeśli to nie jest optymalne. –

+0

To, co zrobiłeś, wygląda dobrze. Kontekst aplikacji jest teraz ukryty przed prezenterem, ponieważ jest zamknięty wewnątrz SharedPreferencesRepository. Wszystkie prezenter wie o tym, że jest repozytorium. – Jahnold

3

To pytanie zostało udzielone jakiś czas temu i, zakładając, że definicja MVP jest tym, co OP używał w swoim kodzie, odpowiedź @Jahnolda jest naprawdę dobra.

Należy jednak podkreślić, że MVP jest koncepcją wysokiego poziomu i istnieje wiele wdrożeń zgodnych z zasadami MVP - istnieje więcej niż jeden sposób na skórze kota.

There is another implementation of MVP, która jest oparta na pomyśle, że Activities in Android are not UI Elements, który określa Activity i Fragment jako prezenterów MVP. W tej konfiguracji prezenterowie MVP mają bezpośredni dostęp do Context.

Nawiasem mówiąc, nawet we wspomnianym realizacji MVP, nie użyłby Context, aby uzyskać dostęp do SharedPreferences w prezentera - Chciałbym jeszcze zdefiniować klasy otoki dla SharedPreferences i wstrzyknąć go do prezentera.

1

Większość elementów domeny, takich jak baza danych lub sieć, wymaga kontekstu do zbudowania. Thay nie można utworzyć w widoku, ponieważ widok nie może mieć żadnej wiedzy o modelu. Należy je następnie utworzyć w Presenter. Mogą zostać wstrzyknięte przez Sztylet, ale używa też Kontekstu.Tak więc kontekst jest używany w Presenter xP

Hak polega na tym, że jeśli chcemy uniknąć kontekstu w prezencie, możemy po prostu stworzyć konstruktora, który tworzy wszystkie te obiekty modelu z kontekstu, a nie zapisywać go. Ale moim zdaniem to głupie. Nowy JUnit w systemie Android ma dostęp do kontekstu.

Innym hack jest uczynienie Context nullable, a w obiektach domeny powinien istnieć mechanizm do zapewnienia instancji testowania w przypadku zerowego kontekstu. Ja też nie lubię tego hackowania.