Guardar estados en aplicaciones de Android

Hoy quería compartir con ustedes otro enfoque para mantener el estado al desarrollar aplicaciones de Android. No es un secreto que nuestra aplicación en segundo plano se puede eliminar en cualquier momento y este problema se vuelve más urgente con la introducción de un ahorro energético agresivo: hola Oreo . Además, nadie canceló el cambio de configuración en el teléfono: orientación, cambio de idioma, etc. Y para abrir la aplicación desde el fondo y mostrar la interfaz en el último estado, debemos ocuparnos de guardarla. Oh, esto enSaveInstanceState .

onSaveInstanceState

Cuánto dolor nos trajo.

Daré ejemplos más adelante usando Clean Achitecture y Dagger2 , así que prepárate para esto :)

El problema de mantener el estado dependiendo de las tareas se puede resolver de varias maneras:

  1. Guarde los datos primarios en onSaveInstanceState del host (Actividad, Fragmento), como el identificador de página, el usuario y lo que sea. Lo que necesitamos para la adquisición inicial de datos y la visualización de la página.
  2. Guarde los datos recibidos en el programa interactivo en el repositorio (SharedPreference, Database.
  3. Utilice fragmentos de rethein para guardar y restaurar datos al recrear actividad.

Pero, ¿qué sucede si necesitamos restaurar el estado de la interfaz de usuario, así como la reacción actual de la interfaz a la acción del usuario? Para simplificar, consideremos la solución a este problema utilizando un ejemplo real. Tenemos una página de inicio de sesión: el usuario ingresa sus datos, hace clic en el botón y luego recibe una llamada entrante. Nuestra aplicación pasa a un segundo plano. El sistema lo mata. Suena aterrador, ¿no?)

El usuario vuelve a la aplicación y ¿qué debería ver? Como mínimo, continuar con la operación de inicio de sesión y mostrar progreso. Si la aplicación logró iniciar sesión antes de llamar al método onDestroy del host, el usuario verá la navegación a la pantalla de inicio de la aplicación. Este comportamiento se puede resolver fácilmente con la máquina de estado. Muy buen informe de Yandex . En el mismo artículo, intentaré compartir mis ideas masticadas sobre este informe.

Ahora un pequeño código:

Basestate

public interface BaseState<VIEW extends BaseView, OWNER extends BaseOwner> extends Parcelable{ /** * Get name * * @return name */ @NonNull String getName(); /** * Enter to state * * @param aView view */ void onEnter(@NonNull VIEW aView); /** * Exit from state */ void onExit(); /** * Return to next state */ void forward(); /** * Return to previous state */ void back(); /** * Invalidate view * * @param aView view */ void invalidateView(@NonNull VIEW aView); /** * Get owner * * @return owner */ @NonNull OWNER getOwner(); /** * Set owner * * @param aOwner owner */ void setOwner(@NonNull OWNER aOwner); } 

Baseoverner

 public interface BaseOwner<VIEW extends BaseView, STATE extends BaseState> extends BasePresenter<VIEW>{ /** * Set state * * @param aState state */ void setState(@NonNull STATE aState); } 

BasestateImpl

 public abstract class BaseStateImpl<VIEW extends BaseView, OWNER extends BaseOwner> implements BaseState<VIEW, OWNER>{ private OWNER mOwner; @NonNull @Override public String getName(){ return getClass().getName(); } @Override public void onEnter(@NonNull final VIEW aView){ Timber.d( getName()+" onEnter"); //depend from realization } @Override public void onExit(){ Timber.d(getName()+" onExit"); //depend from realization } @Override public void forward(){ Timber.d(getName()+" forward"); onExit(); //depend from realization } @Override public void back(){ Timber.d(getName()+" back"); onExit(); //depend from realization } @Override public void invalidateView(@NonNull final VIEW aView){ Timber.d(getName()+" invalidateView"); //depend from realization } @NonNull @Override public OWNER getOwner(){ return mOwner; } @Override public void setOwner(@NonNull final OWNER aOwner){ mOwner = aOwner; } 

En nuestro caso, el propietario del estado será un presentador.

Mirando la página de inicio de sesión, se pueden distinguir tres estados únicos:

LoginInitState , LoginProgressingState , LoginCompleteState .

Entonces, ahora considere lo que sucede en estos estados.

LoginInitState tenemos validación de campos y en caso de validación exitosa, el botón de inicio de sesión se activa.

Se realiza una solicitud de inicio de sesión en LoginProgressingState , se guarda un token, se realizan solicitudes adicionales para iniciar la actividad principal de la aplicación.

LoginCompleteState navega a la pantalla principal de la aplicación.

Condicionalmente, la transición entre estados se puede mostrar en el siguiente diagrama:

Diagrama de estado de inicio de sesión

La salida del estado LoginProgressingState se produce si la operación de inicio de sesión se realiza correctamente en el estado LoginCompleteState y si LoginInitState falla . Por lo tanto, cuando nuestra vista se separa, tenemos un estado completamente determinista del presentador. Debemos guardar este estado utilizando el mecanismo estándar de Android enSaveInstanceState . Para que podamos hacer esto, todos los estados de inicio de sesión deben implementar la interfaz Parcelable . Por lo tanto, estamos ampliando nuestra interfaz base BaseState .

A continuación, tenemos una pregunta, ¿cómo reenviar este estado desde el presentador a nuestro anfitrión? La forma más fácil es pedirle al presentador datos del host, pero desde el punto de vista de la arquitectura, esto no se ve muy bien. Y así retener fragmentos vienen en nuestra ayuda. Podemos crear una interfaz para el caché e implementarla en este fragmento:

 public interface Cache{ /** * Save cache data * * @param aData data */ void saveCacheData(@Nullable Parcelable aData); @Nullable Parcelable getCacheData(); /** * Check that cache exist * * @return true if cache exist */ boolean isCacheExist(); } 

Luego, inyectamos el fragmento de caché en el constructor del interactor, como Cache. Agregamos métodos en el integrador para obtener y guardar el estado en el caché. Ahora, con cada cambio en el estado del presentador, podemos guardar el estado en el interactor, y el interactor a su vez guarda en el caché. Todo se vuelve muy lógico. Cuando el host se carga por primera vez, el presentador recibe el estado del interactor, que a su vez recibe los datos del caché. Así es como se ve el método de cambio de estado en el presentador:

 @Override public void setState(@NonNull final LoginBaseState aState){ mState.onExit(); mState = aState; clearDisposables(); mState.setOwner(this); mState.onEnter(getView()); mInteractor.setState(mState); } 

Me gustaría señalar este punto: se pueden guardar datos a través de la memoria caché para cualquier dato, no solo para el estado. Es posible que deba crear su propia caché de fragmentos única para almacenar los datos actuales. Este artículo describe un enfoque general. También me gustaría señalar que la situación en cuestión es muy exagerada. En la vida tienes que resolver problemas mucho más complicados. Por ejemplo, en nuestra aplicación se combinaron tres páginas: inicio de sesión, registro, recuperación de contraseña. En este caso, el diagrama de estado era el siguiente:

Diagrama de estado en un proyecto real

Como resultado, utilizando el patrón de estado y el enfoque descritos en el artículo, pudimos hacer que el código sea más legible y fácil de mantener. Y lo importante es restaurar el estado actual de la aplicación.

El código completo se puede ver en el repositorio .

Source: https://habr.com/ru/post/es420023/


All Articles