
En AppsConf 2018 , que tuvo lugar del 8 al 9 de octubre, hice una presentación sobre la creación de aplicaciones de Android completamente en una Actividad. Aunque el tema es bien conocido, existen muchos prejuicios con respecto a esa elección: la sala abarrotada y la cantidad de preguntas después del discurso lo confirman. Para no esperar la grabación del video, decidí hacer un artículo con una transcripción del discurso.

Lo que voy a decir
- Por qué y por qué debería cambiar a Actividad única
- Un enfoque universal para resolver tareas que está acostumbrado a resolver en varias actividades
- Ejemplos de tareas comerciales estándar
- Cuellos de botella donde el código generalmente está apuntalado en lugar de hacer todo honestamente
¿Por qué es correcta la actividad individual?
Ciclo de vida

Todos los desarrolladores de Android conocen el esquema de arranque en frío de la aplicación. Primero, se llama a onCreate en la clase Aplicación, luego el ciclo de vida de la primera Actividad entra en acción.
Si hay varias actividades en nuestra aplicación (y hay una mayoría de tales aplicaciones), sucede lo siguiente:
App.onCreate() ActivityA.onCreate() ActivityA.onStart() ActivityA.onResume() ActivityA.onPause() ActivityB.onCreate() ActivityB.onStart() ActivityB.onResume() ActivityA.onStop()
Este es el registro de inicio abstracto de la actividad B de la Actividad A. Una línea vacía es el momento en que se llamó al lanzamiento de una nueva pantalla. A primera vista, todo está bien. Pero si volvemos a la documentación, quedará claro: para garantizar que la pantalla sea visible para el usuario y que pueda interactuar con ella, es posible solo después de llamar al onResume
en cada pantalla:
App.onCreate() ActivityA.onCreate() ActivityA.onStart() ActivityA.onResume() <-------- ActivityA.onPause() ActivityB.onCreate() ActivityB.onStart() ActivityB.onResume() <-------- ActivityA.onStop()
El problema es que dicho registro no ayuda a comprender el ciclo de vida de la aplicación. Cuando el usuario todavía está adentro, y cuando ya ha cambiado a otra aplicación o ha minimizado la nuestra, etc. Y esto es necesario cuando queremos vincular la lógica empresarial a la LC de la aplicación, por ejemplo, mantener una conexión de socket mientras el usuario está en la aplicación y cerrarla al salir
En una aplicación Single-Activity, todo es simple: LC Activity se convierte en una aplicación LC. Todo lo que necesita para cualquier lógica es fácil de vincular al estado de la aplicación.
Pantallas de inicio
Como usuario, a menudo me encontré con el hecho de que no se produce una llamada de la guía telefónica (y esto es claramente el lanzamiento de una Actividad separada) después de hacer clic en un contacto. No está claro con qué está relacionado esto, pero aquellos a quienes intenté comunicar sin éxito dijeron que recibieron la llamada y escucharon el sonido de los pasos. Al mismo tiempo, mi teléfono inteligente ha estado en mi bolsillo durante mucho tiempo.

¡El problema es que comenzar una Actividad es un proceso completamente asíncrono! No hay garantía de arranque instantáneo y, lo que es peor, no podemos controlar el proceso. Absolutamente
En la aplicación Single-Activity, trabajando con el administrador de fragmentos, podemos controlar el proceso.
transaction.commit()
: cambiará las pantallas de forma asincrónica, lo que le permite abrir o cerrar varias pantallas a la vez.
transaction.commitNow()
: cambia la pantalla sincrónicamente si no es necesario agregarla a la pila.
fragmentManager.executePendingTransactions () `le permite ejecutar todas las transacciones iniciadas anteriormente en este momento.
Análisis de pila de pantalla
Imagine que la lógica empresarial de su aplicación depende de la profundidad actual de la pila de pantallas (por ejemplo, restricciones de anidamiento). O, al final de algún proceso, debe volver a una pantalla determinada, y si hay varias idénticas, a la más cercana a la raíz (el comienzo de la cadena).
¿Cómo obtener una pila de actividades? ¿Qué parámetros deben especificarse al iniciar la pantalla?

Por cierto, sobre la magia de las opciones de lanzamiento de Activity:
- puede especificar indicadores de inicio en Intención (y también mezclarlos y cambiarlos desde diferentes lugares);
- puede agregar parámetros de inicio en el manifiesto, porque toda la Actividad debe describirse allí;
- agregue filtros de intención aquí para manejar la activación externa;
- y finalmente piense en MultiTasks, cuando las Actividades pueden ejecutarse en diferentes "tareas".
Juntos, esto crea confusión y problemas con la depuración de soporte. Nunca se puede decir con certeza exactamente cómo se lanzó la pantalla y cómo afectó a la pila.
En una aplicación de actividad única, todas las pantallas cambian solo a través de transacciones fragmentarias. Puede analizar la pila actual de pantallas y transacciones guardadas.
En la demostración de la biblioteca Cicerone , puede ver cómo se muestra el estado actual de la pila en la barra de herramientas.

Nota: en la última versión, las bibliotecas de soporte bloquearon el acceso a la matriz de fragmentos dentro del administrador de fragmentos, pero si realmente lo desea, este problema siempre se puede resolver.
Solo una actividad en pantalla
En aplicaciones reales, definitivamente necesitaremos combinar pantallas "lógicas" en una Actividad, entonces no puede escribir una aplicación real SOLAMENTE en Actividad. La dualidad del enfoque siempre es mala, ya que el mismo problema se puede resolver de diferentes maneras (en algún lugar, el diseño está directamente en la Actividad, y en algún lugar, la Actividad es solo un contenedor).
No guardes actividades
Este indicador de prueba realmente le permite encontrar algunos errores en la aplicación, ¡pero el comportamiento que reproduce NUNCA ocurre en realidad! ¡No sucede que el proceso de solicitud permanezca, y en ese momento la Actividad, aunque no está activa, muere! Las actividades solo pueden morir con el proceso de solicitud. Si la aplicación se muestra al usuario y el sistema no tiene suficientes recursos, todo lo que esté a su alrededor morirá (otras aplicaciones inactivas, servicios e incluso el iniciador), y su aplicación vivirá hasta el final, y si tiene que morir, entonces será completamente.
Puedes comprobarlo.
Legado
Históricamente, hay una gran cantidad de lógica innecesaria en Activity que probablemente no sea útil para usted. Por ejemplo, todo lo que necesita para trabajar con loaders
, actionBar
, action menu
etc. Esto hace que la clase en sí sea bastante masiva y pesada.
Animaciones
Quizás, cualquiera puede hacer una animación de cambio simple al cambiar entre Actividad. Aquí vale la pena aclarar que necesita hacer un descuento en la asincronía del lanzamiento de la Actividad, del que hablamos anteriormente.
Si necesita algo más interesante, puede recordar ejemplos de animaciones de transición que se realizan en la Actividad:

Pero hay un gran problema: personalizar esta animación es casi imposible. Es poco probable que esto complazca a los diseñadores y al cliente.
Con fragmentos, todo es diferente. ¡Podemos ir directamente al nivel de la jerarquía de la vista y hacer cualquier animación que pueda imaginar! Evidencia directa aquí :

Si observa el código fuente, encontrará que esto se hace en un diseño regular. Sí, el código es decente allí, pero la animación siempre es lo suficientemente difícil, y tener esa oportunidad siempre es una ventaja. Si tiene dos Actividades conmutadas, la aplicación no tiene un contenedor común donde pueda realizar tales transiciones.
Configurar configuración sobre la marcha
Este punto no estaba en mi discurso, pero también es muy importante. Si tiene una función para cambiar el idioma dentro de la aplicación, con varias actividades será bastante problemático implementarla, si, entre otras cosas, no necesita reiniciar la aplicación, sino permanecer en el mismo lugar donde estaba el usuario al momento de llamar a la funcionalidad.
En una aplicación de Actividad única, es suficiente cambiar la configuración regional instalada en el contexto de la aplicación y llamar a recreate()
en la Actividad, el resto del sistema hará todo por sí mismo.
Al final
Google tiene una solución de navegación, cuya documentación establece explícitamente que es aconsejable escribir aplicaciones de actividad única.
En este punto, espero que no tenga dudas de que el enfoque clásico con varias actividades contiene una serie de deficiencias, que es habitual hacer la vista gorda, escondiéndose detrás de la tendencia general del descontento de Android.
Si es así, ¿por qué Single-Activity todavía no es un estándar de desarrollo?
Aquí citaré a mi buen amigo:

Al comenzar un nuevo proyecto serio, cualquier líder tiene miedo de equivocarse y evita decisiones arriesgadas. Esto es correcto Pero trataré de proporcionar un plan integral para la transición a una sola actividad.
Cambiar a una sola actividad

Si estudia esta aplicación, puede determinar a partir de las animaciones y el comportamiento característicos que está escrita en varias Actividades. Podría estar equivocado, y todo se hizo incluso en vistas personalizadas, pero esto no afectará nuestro razonamiento.
Ahora atencion! Lo hacemos así:

Solo realizamos dos cambios: agregamos la clase AppActivity y reemplazamos toda la Actividad con FlowFragment. Considere cada cambio con más detalle.
De qué AppActivity es responsable:
- contiene solo un contenedor para fragmentos
- es el punto de inicialización de los objetos de UI de Scope (solía hacerse en la aplicación, lo cual es incorrecto, porque, por ejemplo, los objetos de servicio en nuestra aplicación definitivamente no necesitan tales objetos)
- es un proveedor de aplicaciones
- trae todos los beneficios de Single-Activity.
¿Qué es FlowFragment ?
- hace exactamente lo mismo que la Actividad, en lugar de la que se creó.
Nueva navegación
La principal diferencia con el viejo enfoque es la navegación.

Anteriormente, el desarrollador tenía una opción: lanzar una nueva Actividad o una transacción fragmentaria en la actual. La elección no ha desaparecido, pero los métodos han cambiado: ahora debemos decidir si iniciar la fragmentación del fragmento en AppActivity o dentro del FlowFragment actual.

Del mismo modo con el procesamiento del botón Atrás. Anteriormente, la Actividad pasaba el evento al fragmento actual y, si no lo procesaba, tomaba la decisión en sí. Ahora AppActivity pasa el evento al FlowFragment actual y, a su vez, lo pasa al fragmento actual.
Transferencia de resultados entre pantallas
Para los desarrolladores sin experiencia, el problema de la transferencia de datos entre pantallas es el principal problema del nuevo enfoque, porque antes era posible usar la funcionalidad startActivityForResult ().
No es el primer año, se han discutido varios enfoques arquitectónicos para aplicaciones de escritura. La tarea principal al mismo tiempo sigue siendo la separación de la interfaz de usuario y la capa de datos y la lógica empresarial. Desde este punto de vista, startActivityForResult () rompe el canon, ya que los datos entre las pantallas de una aplicación se transfieren en el lado de la entidad de la capa UI. Destaco que es solo una aplicación, ya que tenemos una capa de datos común, modelos comunes en un ámbito global, etc. No usamos estas oportunidades y nos metemos en el marco de un paquete (serialización, tamaño y más).
Mi consejo : ¡no uses startActivityForResult () dentro de la aplicación! Úselo solo para el propósito previsto: ejecutar aplicaciones externas y obtener resultados de ellas.
¿Cómo, entonces, abrir una pantalla con una opción para otra pantalla? Hay tres opciones:
- Fragmento objetivo
- Eventbus
- modelo jet
TargetFragment: una opción "lista para usar", pero la misma transferencia de datos en el lado de la capa de interfaz de usuario. Mala opción
EventBus: si puede ponerse de acuerdo sobre un equipo y, lo que es más importante, controlar los arreglos, puede implementar la transferencia de datos entre pantallas en el bus de datos global. Pero dado que este es un movimiento peligroso, la conclusión es una mala opción.
Modelo reactivo: este enfoque implica la presencia de devoluciones de llamada y más. El equipo de cada proyecto decide cómo implementarlos. Pero este enfoque es óptimo, ya que proporciona control sobre lo que está sucediendo y no permite que el código se use para otros fines. Nuestra eleccion!
Resumen
Me encantan los nuevos enfoques cuando son simples y tienen beneficios obvios. Espero que este sea el caso en este caso. Los beneficios se describen en la primera parte, y debe juzgar la dificultad. Es suficiente reemplazar toda la Actividad con FlowFragment, manteniendo toda la lógica sin cambios. Cambie un poco el código de navegación y piense en trabajar con la transferencia de datos entre pantallas, si aún no lo ha hecho.
Para mostrar la simplicidad del enfoque, yo mismo cambié la aplicación abierta a Single-Activity, y me tomó solo unas pocas horas (por supuesto, vale la pena considerar que este no es un legado antiguo, y todo es más o menos bueno con la arquitectura allí).
Que paso
Veamos cómo resolver problemas estándar en un nuevo enfoque.
BottomNavigationBar and NavigationDrawer
Usando la regla simple de que reemplazamos toda Actividad con FlowFragment, el menú lateral ahora estará en algún fragmento y cambiará fragmentos anidados en él:

Similar con BottomNavigationBar.
¡Es mucho más interesante que podamos invertir algunos FlowFragment en otros, ya que estos todavía son fragmentos ordinarios!

Esta opción se puede encontrar en GitFox .
Es la posibilidad de combinar simplemente algunos fragmentos dentro de otros lo que hace posible crear una interfaz de usuario dinámica para diferentes dispositivos sin ningún problema: tabletas + teléfonos inteligentes.
Alcances DI
Si tiene un flujo de compras de productos desde varias pantallas, y necesita mostrar el nombre del producto en cada pantalla, probablemente ya lo haya colocado en una Actividad separada que almacena el producto y lo proporciona a las pantallas.
Será lo mismo con FlowFragment: contendrá una escala DI con modelos para todas las pantallas anidadas. Este enfoque elimina el complicado control de la vida útil del osciloscopio al vincularlo con la vida útil del FlowFragment.

Enlaces profundos
Si utilizó filtros en el manifiesto para iniciar una pantalla específica en el enlace profundo, es posible que tenga problemas para iniciar la Actividad, sobre la que escribí en la primera parte. En el nuevo enfoque, todos los enlaces profundos caen en AppActivity.onNewIntent. Además, de acuerdo con los datos obtenidos, hay una transición a la pantalla requerida (o una cadena de pantallas. Propongo analizar dicha funcionalidad en Chicheron ).

Proceso de muerte
Si la aplicación está escrita en varias Actividades, debe saber que cuando la aplicación muera, y luego cuando se restaure el proceso, el usuario estará en la última Actividad, y todas las anteriores se restaurarán solo cuando se les devuelva.

Si no tiene esto en cuenta de antemano, pueden surgir problemas. Por ejemplo, si el alcance necesario en la última Actividad abierta en la anterior, nadie lo volverá a crear. Que hacer ¿Traer esto a la clase de aplicación? ¿Los puntos múltiples abren ámbitos?
Todo es más simple con fragmentos, ya que están dentro de una Actividad u otro FlowFragment, y cualquier contenedor se restaurará ANTES de volver a crear el fragmento.

Podemos discutir otras tareas prácticas en los comentarios, ya que de lo contrario existe la posibilidad de que el artículo resulte demasiado voluminoso.
Y ahora la parte más interesante.
Cuellos de botella (necesita recordar y pensar).
Aquí se recopilan cosas importantes en las que debe pensar en cualquier proyecto, pero todos están tan acostumbrados a "desgarrarlos" en proyectos en varias Actividades que vale la pena recordar y decir cómo resolverlos correctamente en el nuevo enfoque. Y primero en la lista
Rotación de la pantalla
Esa es la historia más terrible para los fanáticos llorones de que Android recrea la Actividad cuando se gira la pantalla. El método de solución más popular es arreglar la orientación vertical. Además, esta propuesta ya no es hecha por desarrolladores, sino por gerentes asustados por frases como " mantener un giro es muy difícil y cuesta varias veces más ".
No discutiremos sobre la exactitud de tal decisión. Otra cosa es importante: ¡ arreglar la rotación no está exento de la muerte de Activity! Dado que los mismos procesos ocurren con muchos otros eventos: modo dividido, cuando se muestran varias aplicaciones en la pantalla, conectando un monitor externo, cambiando la configuración de la aplicación sobre la marcha, etc.
Además, la rotación de la pantalla le permite verificar la correcta "elasticidad" del diseño, por lo tanto, en nuestro equipo de San Petersburgo, no desactivamos la rotación en todos los ensambles de ventas, incluso si no está en la versión de lanzamiento. Sin mencionar los errores típicos que aún se encontrarán durante la verificación.
Ya se han escrito muchas soluciones para convertir, comenzando desde Moxy y terminando con varias implementaciones de MVVM. No lo hagas más difícil que cualquier otra cosa.
Considere otro caso interesante.
Imagine una aplicación de catálogo de productos. Lo hacemos en una sola actividad. En todas partes, el modo de retrato es fijo, pero el cliente quiere una función cuando, al ver la galería de fotos, el usuario puede verlos en orientación horizontal. ¿Cómo apoyar esto?
Alguien ofrecerá la primera muleta :
<activity android:name=".AppActivity" android:configChanges="orientation" />
override fun onConfigurationChanged(newConfig: Configuration?) { if (newConfig?.orientation == Configuration.ORIENTATION_LANDSCAPE) {
Por lo tanto, no podemos llamar a super.onConfigurationChanged(newConfig)
, sino procesarlo nosotros mismos y rotar solo la vista necesaria en la pantalla.
Pero con API 23, el proyecto se bloqueará con una SuperNotCalledException
, por lo que es una mala elección .
Las declaraciones anteriores cometieron un error:
Me corrigieron razonablemente en los comentarios que agregar android: configChanges = "orientación | tamaño de pantalla" es suficiente, y luego puede llamar a super y la actividad no se volverá a crear en la rotación. Es útil usarlo cuando hay un WebView o un mapa en la pantalla que lleva mucho tiempo inicializar, y desea evitar esto.
Esto ayudará a resolver el caso descrito con la galería, pero el mensaje principal de esta sección: no ignore la recreación de la Actividad , esto puede suceder en muchos otros casos.
Alguien podría sugerir otra solución:
<activity android:name=".AppActivity" android:screenOrientation="portrait" /> <activity android:name=".RotateActivity" />
Pero de esta manera nos alejamos del enfoque de Actividad Única para resolver un problema simple y privarnos de todos los beneficios del enfoque. Esta es una muleta, y una muleta siempre es una mala elección .
Aquí está la solución correcta:
<activity android:name=".AppActivity" android:configChanges="orientation" />
override fun onResume() { super.onResume() activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR } override fun onPause() { super.onPause() activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT }
Es decir, cuando se abre el fragmento, la aplicación comienza a "girar" y, cuando regresa, se repara nuevamente. En mi experiencia, así es como funciona la aplicación AirBnB . Si abre una vista de fotos de la vivienda, se activa el procesamiento de giro, pero en orientación horizontal, puede arrastrar la foto hacia abajo para salir de la galería. Debajo, la pantalla anterior será visible en orientación horizontal, lo que generalmente no encontrará, ya que inmediatamente después de salir de la galería la pantalla se convertirá en un retrato y se reparará.

Aquí es donde ayudará la preparación oportuna para los giros de pantalla.
Barra de estado transparente
Solo Activity puede funcionar con la barra del sistema, pero ahora solo tenemos una, por lo que siempre debe especificar
<item name="android:windowTranslucentStatus">true</item>
Pero en algunas pantallas no hay necesidad de "arrastrarse" debajo, y debe mostrar todo el contenido a continuación. La bandera viene al rescate
android:fitsSystemWindows="true"
que indica el diseño que no debe dibujar debajo de la barra del sistema. Pero si lo especifica en el diseño del fragmento y luego intenta mostrar el fragmento a través de la transacción en el administrador de fragmentos, se sentirá decepcionado ... ¡no funcionará!
La respuesta es rápidamente google
Le recomiendo que se familiarice con una respuesta realmente completa y muchos enlaces útiles.
Una solución rápida y funcional ( pero no la correcta ) es ajustar el diseño en CoordinatorLayout
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> </android.support.design.widget.CoordinatorLayout>
Una mejor solución ayuda a procesar el teclado también.
Cambiar el diseño cuando aparece el teclado
Cuando el teclado se va, el diseño debe cambiar para que elementos importantes de la interfaz de usuario no queden fuera del alcance. Y si antes podíamos especificar diferentes modos de reacción para el teclado para diferentes actividades, ahora tenemos que hacer esto en Single-Activity. Por lo tanto, es necesario usar
android:windowSoftInputMode="adjustResize"
Si utiliza el enfoque de la sección anterior para procesar una barra de estado transparente, encontrará un desafortunado error: si un fragmento se arrastró con éxito debajo de la barra de estado, cuando aparezca el teclado, se encogerá arriba y abajo, a medida que la barra de estado y el teclado dentro del sistema funcionan SystemWindows
.
Presta atención al título.

Que hacer ¡Lee la documentación! Chris Banes WindowInsets .
WindowInsets
WindowInsets!
Splash screen
- , Splash screen — , , , , Activity . .

, Single-Activity, Splash screen. , deep-link Splash screen .
, , , .
, . Single-Activity. - , , .
...
Intent, , ...
Que sigue :
, . ? — «» «» .
Que hacer , .
Activity!
, : , — .
— , ( Activity), .
Activity — . Activity, . .

Conclusión
() , Activity, Android-. , .
: Google . — , , Activity .
, , , ! Gracias