español - Implementación correcta de MVVM en Android
mvvm android español (3)
He estado luchando por encontrar la forma correcta de implementar MVVM en Android.
Toda la Idea sigue siendo borrosa para mí, el patrón es tener una capa separada en la que se hace la lógica (ViewModel).
Este fragmento de código solo anima el alfa de un fondo en el que viven varios fragmentos.
public class StartActivity extends AppCompatActivity implements EntryFragment.EntryFragementListener {
private static final float MINIMUM_ALPHA = 0.4f;
private static final float MAXIMUM_ALPHA = 0.7f;
@State
float mCurrentAlpha = MINIMUM_ALPHA;
@State
String mCurrentTag = EntryFragment.TAG;
private ActivityStartBinding mBinding;
private StartViewModel mStartViewModel = new StartViewModel();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_start);
mBinding.setStartViewModel(mStartViewModel);
mBinding.bgBlackLayer.setAlpha(mCurrentAlpha);
if (getSupportFragmentManager().findFragmentByTag(mCurrentTag) == null) {
switch (mCurrentTag) {
case EntryFragment.TAG:
setEntryFragment();
break;
case FreeTrialFragment.TAG:
setFreeTrialFragment();
break;
}
}
}
private void setEntryFragment() {
mCurrentAlpha = MINIMUM_ALPHA;
mCurrentTag = EntryFragment.TAG;
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = new EntryFragment();
fm.beginTransaction().
add(R.id.fragment_content, fragment, EntryFragment.TAG).commit();
}
private void setFreeTrialFragment() {
mCurrentTag = FreeTrialFragment.TAG;
Fragment fragment = new FreeTrialFragment();
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.setCustomAnimations(R.anim.anim_enter_right, R.anim.anim_exit_left, R.anim.anim_enter_left, R.anim.anim_exit_right);
ft.replace(R.id.fragment_content, fragment, FreeTrialFragment.TAG);
ft.addToBackStack(FreeTrialFragment.TAG);
ft.commit();
StartViewModel.setAnimation(mBinding.bgBlackLayer,true, MAXIMUM_ALPHA);
}
private void setForgotPasswordFragmet() {
}
private void setLoginFragment() {
}
@Override
public void onBackPressed() {
super.onBackPressed();
StartViewModel.setAnimation(mBinding.bgBlackLayer,true, MINIMUM_ALPHA);
mCurrentAlpha = MINIMUM_ALPHA;
}
@Override
public void onEntryLoginButton() {
setLoginFragment();
}
@Override
public void onEntryFreeTrialButton() {
setFreeTrialFragment();
}
}
-El ViewModel solo hace la lógica al hacer la animación -Fragmentos tienen un oyente para pasar los eventos a la actividad -La vinculación ayuda a definir las vistas
public class StartViewModel {
public ObservableBoolean hasToAnimate = new ObservableBoolean(false);
public float alpha;
@BindingAdapter(value={"animation", "alpha"}, requireAll=false)
public static void setAnimation(View view, boolean hasToAnimate, float alpha) {
if (hasToAnimate) {
view.animate().alpha(alpha);
}
}
}
La pregunta es: ¿debería residir toda la lógica en el modelo de vista, incluidas las transacciones de fragmentos, la gestión de los cambios de orientación, etc.? ¿Hay una mejor manera de implementar MVVM?
Cuando se trata de patrones de diseño en general. Desea mantener la lógica empresarial lejos de Actividades y fragmentos.
MVVM y MVP son realmente buenas opciones si me preguntas. Pero ya que quiere implementar MVVM. Luego intentaré explicar un poco sobre cómo lo implemento.
La actividad
public class LoginActivity extends BaseActivity {
private LoginActivityViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityLoginBinding binding = DataBindingUtil.setContentView(this,R.layout.activity_login);
NavigationHelper navigationHelper = new NavigationHelper(this);
ToastHelper toastHelper = new ToastHelper(this);
ProgressDialogHelper progressDialogHelper = new ProgressDialogHelper(this);
viewModel = new LoginActivityViewModel(navigationHelper,toastHelper,progressDialogHelper);
binding.setViewModel(viewModel);
}
@Override
protected void onPause() {
if (viewModel != null) {
viewModel.onPause();
}
super.onPause();
}
@Override
protected void onDestroy() {
if (viewModel != null) {
viewModel.onDestroy();
}
super.onDestroy();
}
}
Esta es una actividad bastante simple. Nada especial. Simplemente empiezo por ejemplificar lo que mi viewModel necesita. Porque trato de mantener todo lo específico de Android lejos de él. Todo para facilitar la escritura de pruebas
Luego solo ato el viewmodel a la vista.
La vista
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.community.toucan.authentication.login.LoginActivityViewModel" />
</data>
<RelativeLayout
android:id="@+id/activity_login_main_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background"
tools:context="com.community.toucan.authentication.login.LoginActivity">
<ImageView
android:id="@+id/activity_login_logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="40dp"
android:src="@drawable/logo_small" />
<android.support.v7.widget.AppCompatEditText
android:id="@+id/activity_login_email_input"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_below="@+id/activity_login_logo"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="60dp"
android:drawableLeft="@drawable/ic_email_white"
android:drawablePadding="10dp"
android:hint="@string/email_address"
android:inputType="textEmailAddress"
android:maxLines="1"
android:text="@={viewModel.username}" />
<android.support.v7.widget.AppCompatEditText
android:id="@+id/activity_login_password_input"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_below="@+id/activity_login_email_input"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:drawableLeft="@drawable/ic_lock_white"
android:drawablePadding="10dp"
android:hint="@string/password"
android:inputType="textPassword"
android:maxLines="1"
android:text="@={viewModel.password}" />
<Button
android:id="@+id/activity_login_main_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/activity_login_password_input"
android:layout_centerHorizontal="true"
android:layout_marginTop="10dp"
android:background="@drawable/rounded_button"
android:onClick="@{() -> viewModel.tryToLogin()}"
android:paddingBottom="10dp"
android:paddingLeft="60dp"
android:paddingRight="60dp"
android:paddingTop="10dp"
android:text="@string/login"
android:textColor="@color/color_white" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/activity_login_main_button"
android:layout_centerHorizontal="true"
android:layout_marginTop="20dp"
android:onClick="@{() -> viewModel.navigateToRegister()}"
android:text="@string/signup_new_user"
android:textSize="16dp" />
<LinearLayout
android:id="@+id/activity_login_social_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerInParent="true"
android:layout_marginBottom="50dp"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/facebook" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/twitter" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/google" />
</LinearLayout>
<TextView
android:id="@+id/activity_login_social_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/activity_login_social_buttons"
android:layout_centerHorizontal="true"
android:layout_marginBottom="20dp"
android:text="@string/social_account"
android:textSize="16dp" />
</RelativeLayout>
</layout>
Bastante sencillo desde el lado de la vista. Ato los valores específicos que viewModel necesita para actuar según la lógica que tiene.
https://developer.android.com/topic/libraries/data-binding/index.html Consulte el siguiente enlace para obtener más información sobre cómo funciona la biblioteca de enlace de datos android
The ViewModel
public class LoginActivityViewModel extends BaseViewModel implements FirebaseAuth.AuthStateListener {
private final NavigationHelper navigationHelper;
private final ProgressDialogHelper progressDialogHelper;
private final ToastHelper toastHelper;
private final FirebaseAuth firebaseAuth;
private String username;
private String password;
public LoginActivityViewModel(NavigationHelper navigationHelper,
ToastHelper toastHelper,
ProgressDialogHelper progressDialogHelper) {
this.navigationHelper = navigationHelper;
this.toastHelper = toastHelper;
this.progressDialogHelper = progressDialogHelper;
firebaseAuth = FirebaseAuth.getInstance();
firebaseAuth.addAuthStateListener(this);
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onResume() {
super.onResume();
}
@Override
public void onDestroy() {
firebaseAuth.removeAuthStateListener(this);
super.onDestroy();
}
@Override
public void onStop() {
progressDialogHelper.onStop();
super.onStop();
}
public void navigateToRegister() {
navigationHelper.goToRegisterPage();
}
public void tryToLogin() {
progressDialogHelper.show();
if (validInput()) {
firebaseAuth.signInWithEmailAndPassword(username, password)
.addOnCompleteListener(new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
if (!task.isSuccessful()) {
String message = task.getException().getMessage();
toastHelper.showLongToast(message);
}
progressDialogHelper.hide();
}
});
}
}
private boolean validInput() {
return true;
}
@Override
public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) {
if (firebaseAuth.getCurrentUser() != null) {
navigationHelper.goToMainPage();
}
}
@Bindable
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
notifyPropertyChanged(BR.username);
}
@Bindable
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
notifyPropertyChanged(BR.password);
}
}
Aquí es donde sucede toda la diversión. Uso las clases de ayuda para mostrar y actuar con el sistema Android. De lo contrario, trato de mantener la lógica lo más limpia posible. Todo está hecho, así que es más fácil para mí crear y probar la lógica.
Tenga en cuenta
Ato el username
y la password
con la vista. Por lo tanto, cada cambio realizado en EditText se agregará automáticamente al campo. De ese modo. No necesito agregar ningún oyente específico
Espero que este pequeño escaparate pueda ayudarlo a comprender un poco cómo podría implementar MVVM en sus propios proyectos
En cuanto a mí, MVVM, MVP y otros patrones realmente geniales para chicos geniales no tienen un recibo / flujo directo. Por supuesto, tiene muchos tutoriales / recomendaciones / patrones y enfoques sobre cómo implementarlos. Pero de eso se trata en realidad toda la programación, solo necesita encontrar una solución que se adapte a sus necesidades. Dependiendo de la visión de los desarrolladores, puede aplicar muchos principios a su solución para facilitar / acelerar el desarrollo / prueba / soporte.
En su caso, creo que es mejor mover este tipo de lógica a las transiciones de Fragmentos (como lo ha hecho en setFreeTrialFragment()
), es más personalizable y cómodo de usar. Sin embargo, si su enfoque sigue siendo el mismo, el existente es normal. En realidad, @BindingAdapter
es más adecuado para atributos xml que para un uso directo.
En cuanto a mí, toda la lógica de la interfaz de usuario debe residir en la actividad, el objetivo principal es separar la lógica empresarial de la interfaz de usuario. Por eso, todas las animaciones, transacciones de fragmentos, etc. se manejan dentro de la actividad, ese es mi enfoque. ViewModel: es responsable de notificar a la vista que algo ha cambiado en el modelo correspondiente y que la vista debe organizarse para esos cambios. En el mundo perfecto, usted debería ser capaz de lograr un término tan popular como el enlace bidireccional, pero no siempre es necesario y no siempre se deben manejar los cambios de la interfaz de usuario dentro del ViewModel. Como de costumbre, demasiado MVVM es malo para su proyecto. Puede causar el código de Spaghetti , "¿De dónde es?", "¿Cómo se puede reciclar?" y otros temas populares. Por lo tanto, debe usarse solo para hacer la vida más atractiva, no para hacer que todo sea ideal, porque como cualquier otro patrón causará mucho dolor de cabeza y alguien que revisará su código dirá "¡EXCESO DE GRANJAS! 11".
Por solicitud, ejemplo de MVP:
Aquí tienes algunos artículos útiles:
- Ejemplo bastante simple.
- Aquí tiene una buena descripción con la guía de integración.
- La primera y la segunda parte de estos artículos pueden ser más útiles.
- Este es corto y muy descriptivo.
Ejemplo corto (generalizado), debe ajustarlo a su arquitectura:
Implementación:
Modelo:
public class GalleryItem {
private String mImagePath;
//other variables/getters/setters
}
Presentador:
//cool presenter with a lot of stuff
public class GalleryPresenter {
private GalleryView mGalleryView;
public void loadPicturesBySomeCreteria(Criteria criteria){
//perform loading here
//notify your activity
mGalleryView.setGalleryItems(yourGaleryItems);
}
//you can use any other suitable name
public void bind(GalleryView galleryView) {
mGalleryView = galleryView;
}
public void unbind() {
mGalleryView = null;
}
//Abstraction for basic communication with activity.
//We can say that this is our protocol
public interface GalleryView {
void setGalleryItems(List<GalleryItem> items);
}
}
Vista:
public class NiceGalleryView extends View {
public NiceGalleryView(Context context) {
super(context);
}
public NiceGalleryView(Context context, AttributeSet attrs) {
super(context, attrs);
}
// TODO: 29.12.16 do your stuff here
}
Y de cource el código de actividad:
public class GalleryActivity extends AppCompatActivity implements GalleryPresenter.GalleryView {
private GalleryPresenter mPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_gallery);
//init views and so on
mPresenter = new GalleryPresenter();
mPresenter.bind(this);
}
@Override
public void setGalleryItems(List<GalleryItem> items) {
//use RecyclerView or any other stuff to fill your UI
}
@Override
protected void onDestroy() {
super.onDestroy();
mPresenter.unbind();
}
}
También tenga en cuenta que incluso tiene muchos enfoques diferentes al usar MVP. Solo quiero enfatizar que prefiero inicializar vistas en actividad y no pasarlas fuera de actividad. Puede gestionar esto a través de la interfaz y eso es realmente cómodo no solo para el desarrollo, sino incluso para pruebas instrumentales.
Un buen ejemplo va aquí, así que compruébalo, vale la pena leerlo, ya que incluye más de 1 forma de incluir la arquitectura MVP. Muestras de MVP Google