medium - mvp android studio example
¿El presentador que tiene conocimiento de la Actividad/Contexto es una mala idea en el patrón MVP? (3)
He estado jugando con el patrón MVP durante algunas semanas y he llegado al punto en que necesito contexto para iniciar un
service
y acceder a
Shared Preferences
.
He leído que el propósito de MVP es desacoplar la vista de la lógica y tener
context
dentro de un
Presenter
puede vencer ese propósito (corrígeme si me equivoco en esto).
Actualmente, tengo una LoginActivity que se parece a esto:
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();
}
}
Interfaz del presentador 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();
}
Por último, mi presentador:
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() {
}
}
Como puede ver, pasé el contexto de la
Activity
a mi
Presenter
solo para poder acceder a las
Shared Preferences
.
Estoy bastante preocupado por pasar el contexto a mi presentador.
¿Es esto algo correcto?
¿O debería hacerlo de otra manera?
EDITAR Implementa la tercera preferencia de Jahnold
Ignoremos la interfaz y la implementación porque es prácticamente todo.
Así que ahora estoy
injecting
la interfaz para la preferencia compartida en mi presentador.
Aquí está mi código para el
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 forma en que obtengo el contexto es de
MyApplication.java
Cuando comienza la aplicación, me aseguro de crear este gráfico de objetos con esta línea de código:
objectGraph = ObjectGraph.create(new AppModule(this));
¿Esta bien? Quiero decir que ahora no tengo que pasar el contexto de la actividad a mi presentador, pero todavía tengo el contexto de la aplicación.
Esta pregunta fue respondida hace algún tiempo, y, suponiendo que la definición de MVP es lo que OP usó en su código, la respuesta de @Jahnold es realmente buena.
Sin embargo, debe señalarse que MVP es un concepto de alto nivel, y puede haber muchas implementaciones siguiendo los principios de MVP: hay más de una forma de desollar al gato.
Hay otra implementación de MVP
, que se basa en la idea de que las
actividades en Android no son elementos de la interfaz de usuario
, que designa a
Activity
and
Fragment
como presentadores de MVP.
En esta configuración, los presentadores de MVP tienen acceso directo al
Context
.
Por cierto, incluso en la implementación antes mencionada de MVP, no usaría
Context
para obtener acceso a
SharedPreferences
en el presentador; todavía definiría una clase de contenedor para
SharedPreferences
y la inyectaría en el presentador.
Ha pasado algún tiempo desde que hizo esta pregunta, pero pensé que sería útil proporcionar una respuesta de todos modos. Sugeriría encarecidamente que el presentador no tenga ningún concepto del contexto de Android (o cualquier otra clase de Android). Al separar completamente su código de Presenter del código del sistema Android, puede probarlo en la JVM sin la complicación de burlarse de los componentes del sistema.
Para lograr esto, creo que tienes tres opciones.
Acceda a las preferencias compartidas desde la vista
Este es mi menos favorito de los tres, ya que acceder a SharedPreferences no es una acción de vista. Sin embargo, mantiene el código del sistema Android en la Actividad lejos del Presentador. En su interfaz de vista tiene un método:
boolean isLoggedIn();
que se puede llamar desde el presentador.
Inyectar SharedPreferences usando Dagger
Como ya está utilizando Dagger para inyectar el bus de eventos, puede agregar SharedPreferences a su ObjectGraph y, como tal, obtendrá una instancia SharedPreferences que se ha construido utilizando ApplicationContext. Esto es lo que obtienes sin tener que pasar un contexto a tu presentador.
La desventaja de este enfoque es que todavía estás pasando una clase de sistema Android (SharedPreferences) y tendrías que burlarte de él cuando quisieras probar el presentador.
Crear una interfaz SharePreferencesRepository
Este es mi método preferido para acceder a datos de Preferencias Compartidas desde un Presentador. Básicamente, usted trata SharedPreferences como un modelo y tiene una interfaz de repositorio para ello.
Su interfaz sería similar a:
public interface SharedPreferencesRepository {
boolean isLoggedIn();
}
Entonces puede tener una implementación concreta de esto:
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);
}
}
Es la interfaz SharedPreferencesRepository que luego se inyecta con Dagger en su presentador. De esta forma, se puede proporcionar un simulacro muy simple en tiempo de ejecución durante las pruebas. Durante el funcionamiento normal se proporciona la implementación concreta.
La mayoría de los elementos del dominio, como la base de datos o la red, necesitan que se cree el contexto. Thay no se puede crear en View porque View no puede tener ningún conocimiento sobre Model. Luego deben crearse en Presentador. Pueden ser inyectados por Dagger, pero también está usando Context. Entonces, el contexto se usa en Presenter xP
El truco es que si queremos evitar el Contexto en Presenter, entonces podemos hacer el constructor que está creando todos estos objetos Modelo a partir del Contexto y no guardarlo. Pero en mi opinión, es estúpido. El nuevo JUnit en Android tiene acceso a Context.
Otro truco es hacer que el contexto sea anulable, y en los objetos de dominio debe haber un mecanismo para proporcionar una instancia de prueba en caso de nulo en contexto. Tampoco me gusta este truco.