Una nueva mirada a la visualización de cuadros de diálogo en Android


La imagen muestra el primer pensamiento del lector que se pregunta qué se puede escribir sobre una tarea tan simple como mostrar un diálogo. El gerente piensa de manera similar: "No hay nada complicado aquí, nuestro Vasya lo hará en 5 minutos". Por supuesto, exagero, pero en realidad no todo es tan simple como parece a primera vista. Especialmente si estamos hablando de Android.


Entonces, 2019 estaba en el patio, y todavía no sabemos cómo mostrar los diálogos correctamente .


Hagámoslo en orden y comencemos con la declaración del problema:


Es necesario mostrar un diálogo simple con el texto para confirmar la acción y los botones "confirmar / cancelar". Al hacer clic en el botón "confirmar", realice una acción, mediante el botón "cancelar", cierre el cuadro de diálogo.

Solución de frente


Yo llamaría a este método junior, porque esta no es la primera vez que me encuentro con un malentendido por qué no puedes usar AlertDialog, como se muestra a continuación:


AlertDialog.Builder(this) .setMessage("Please, confirm the action") .setPositiveButton("Confirm") { dialog, which -> // handle click } .setNegativeButton("Cancel", null) .create() .show() 

Una forma bastante común para un desarrollador novato, es obvio e intuitivo. Pero, como en muchos casos cuando se trabaja con Android, este método es completamente incorrecto. De la nada, obtenemos una pérdida de memoria, solo gira el dispositivo y verás el siguiente error en los registros:


traza
 E/WindowManager: android.view.WindowLeaked: Activity com.example.testdialog.MainActivity has leaked window DecorView@71b5789[MainActivity] that was originally added here at android.view.ViewRootImpl.<init>(ViewRootImpl.java:511) at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:346) at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93) at android.app.Dialog.show(Dialog.java:329) at com.example.testdialog.MainActivity.onCreate(MainActivity.kt:27) at android.app.Activity.performCreate(Activity.java:7144) at android.app.Activity.performCreate(Activity.java:7135) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2931) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3086) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78) at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1816) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:193) at android.app.ActivityThread.main(ActivityThread.java:6718) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858) 

En Stackoverflow, la pregunta sobre este tema es una de las más populares. En resumen, el problema es que mostramos el cuadro de diálogo o no lo cerramos después de que se completa la activación.


Por supuesto, puede llamar a descartar en el diálogo en la actividad onPause o onDestroy, como se recomienda en la respuesta por referencia . Pero esto no es exactamente lo que necesitamos. Queremos que el diálogo se recupere después de encender el dispositivo.


Manera anticuada


Antes de que aparecieran fragmentos en Android, los cuadros de diálogo deberían haberse mostrado mediante una llamada al método de activación showDialog . En este caso, la actividad controla correctamente el ciclo de vida del diálogo y lo restaura después de un turno. La creación del diálogo en sí tuvo que implementarse en la devolución de llamada onCreateDialog:


 public class MainActivity extends Activity { private static final int CONFIRMATION_DIALOG_ID = 1; // ... @Override protected Dialog onCreateDialog(int id, Bundle args) { if (id == CONFIRMATION_DIALOG_ID) { return new AlertDialog.Builder(this) .setMessage("Please, confirm the action") .setPositiveButton("Confirm", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // handle click } }) .create(); } else { return super.onCreateDialog(id, args); } } } 

No es muy conveniente que tenga que iniciar un identificador de diálogo y pasar parámetros a través del paquete. Y todavía podemos obtener el problema de la "ventana filtrada" si intentamos mostrar un cuadro de diálogo después de llamar a Destroy sobre la actividad. Esto es posible, por ejemplo, cuando se intenta mostrar un error después de una operación asincrónica.


En general, este problema es típico de Android, cuando necesita hacer algo después de una operación asincrónica, y la actividad o fragmento ya está destruido en ese momento. Esta es probablemente la razón por la cual los patrones MV * son más populares en la comunidad de Android que entre los desarrolladores de iOS.


Método de la documentación.


Los fragmentos aparecieron en Android Honeycomb , y el método descrito anteriormente está en desuso, y el método showDialog de la actividad está marcado como en desuso. No, AlertDialog no está desactualizado, ya que muchos están equivocados. Justo ahora hay DialogFragment , que envuelve el objeto de diálogo y controla su ciclo de vida.


Los fragmentos nativos también están en desuso desde la API 28. Ahora debe usar solo la implementación de la Biblioteca de soporte (AndroidX).

Realicemos nuestra tarea, según lo prescrito por la documentación oficial :


  1. Primero debe heredar de DialogFragment e implementar la creación de un diálogo en el método onCreateDialog.
  2. Describa la interfaz del evento de diálogo e instancia el oyente en el método onAttach.
  3. Implemente una interfaz de evento de diálogo en una actividad o fragmento.

Si el lector no tiene muy claro por qué el oyente no puede pasar por el constructor, puede leer más sobre esto aquí

Código de fragmento de diálogo:


 class ConfirmationDialogFragment : DialogFragment() { interface ConfirmationListener { fun confirmButtonClicked() fun cancelButtonClicked() } private lateinit var listener: ConfirmationListener override fun onAttach(context: Context?) { super.onAttach(context) try { // Instantiate the ConfirmationListener so we can send events to the host listener = activity as ConfirmationListener } catch (e: ClassCastException) { // The activity doesn't implement the interface, throw exception throw ClassCastException(activity.toString() + " must implement ConfirmationListener") } } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return AlertDialog.Builder(context!!) .setMessage("Please, confirm the action") .setPositiveButton("Confirm") { _, _ -> listener.confirmButtonClicked() } .setNegativeButton("Cancel") { _, _ -> listener.cancelButtonClicked() } .create() } } 

Código de activación:


 class MainActivity : AppCompatActivity(), ConfirmationListener { private fun showConfirmationDialog() { ConfirmationDialogFragment() .show(supportFragmentManager, "ConfirmationDialogFragmentTag") } override fun confirmButtonClicked() { // handle click } override fun cancelButtonClicked() { // handle click } } 

Suficiente código resultó, ¿verdad?


Como regla, hay algún tipo de MVP en el proyecto, pero decidí que las llamadas de presentador pueden omitirse en este caso. En el ejemplo anterior, vale la pena agregar el método estático para crear el cuadro de diálogo newInstance y pasar parámetros a los argumentos del fragmento, todo como se esperaba.


Y esto es todo para que el diálogo se oculte a tiempo y se restaure correctamente. No es sorprendente que tales preguntas surjan en Stackoverflow: uno y dos .


Encontrar la solución perfecta


El estado actual de las cosas no nos convenía, y comenzamos a buscar una manera de hacer que trabajar con diálogos fuera más cómodo. Hubo la sensación de que puedes hacerlo más fácil, casi como en el primer método.


Las siguientes son las consideraciones que nos guiaron:


  • ¿Necesito guardar y restaurar el diálogo después de finalizar el proceso de solicitud?
    En la mayoría de los casos, esto no es obligatorio, como en nuestro ejemplo, cuando necesita mostrar un mensaje simple o preguntar algo. Tal diálogo es relevante hasta que se pierde la atención del usuario. Si lo restaura después de una larga ausencia en la aplicación, el usuario perderá el contexto con la acción planificada. Por lo tanto, solo necesita soportar los giros del dispositivo y manejar correctamente el ciclo de vida del diálogo. De lo contrario, debido al movimiento incómodo del dispositivo, el usuario puede perder el mensaje que se acaba de abrir sin leerlo.
  • Cuando se usa DialogFragment, aparece demasiado código repetitivo, se pierde simplicidad. Por lo tanto, sería bueno deshacerse del fragmento como envoltorio y usar Dialog directamente . Para hacer esto, debe almacenar el estado del cuadro de diálogo para mostrarlo nuevamente después de volver a crear la Vista y ocultarlo cuando la Vista muera.
  • Todos están acostumbrados a percibir la visualización del diálogo como un equipo, especialmente si trabajas solo con MVP. La tarea de la restauración posterior del estado es asumida por el FragmentManager. Pero puede ver esta situación de manera diferente y comenzar a percibir el diálogo como un estado . Esto es mucho más conveniente cuando se trabaja con patrones PM o MVVM.
  • Dado que la mayoría de las aplicaciones ahora usan enfoques reactivos, es necesario que los diálogos sean reactivos . La tarea principal es no romper la cadena que inicia la visualización del diálogo, y adjuntar una secuencia reactiva de eventos para obtener un resultado del mismo. Esto es muy conveniente en el lado PresentationModel / ViewModel cuando está manipulando múltiples flujos de datos.

Tomamos en cuenta todos los requisitos anteriores y se nos ocurrió una forma de mostrar reactivamente los diálogos que implementamos con éxito en nuestra biblioteca RxPM (hay un artículo separado al respecto).


La solución en sí no requiere una biblioteca y se puede hacer por separado. Guiado por la idea del "diálogo como estado", puede intentar construir una solución basada en el moderno ViewModel y LiveData. Pero dejaré esto al lector, y luego hablaremos sobre una solución preparada de la biblioteca.


Método reactivo


Mostraré cómo se resuelve la tarea inicial en RxPM, pero primero algunas palabras sobre los conceptos clave de la biblioteca:


  • PresentationModel : almacena un estado de reacción, contiene lógica de interfaz de usuario, sobrevive a los turnos.
  • El estado es un estado reactivo. Puede pensarlo como un contenedor sobre BehaviorRelay.
  • Acción : un contenedor sobre PublishRelay, sirve para transferir eventos de View a PresentationModel.
  • Estado y Acción tienen observable y consumidor.

La clase DialogControl es responsable del estado del diálogo. Tiene dos parámetros: el primero para el tipo de datos que deben mostrarse en el cuadro de diálogo, el segundo para el tipo de resultado. En nuestro ejemplo, el tipo de datos será Unidad, pero puede ser un mensaje para el usuario o cualquier otro tipo.


DialogControl tiene los siguientes métodos:


  • show(data: T) : solo da un comando para mostrar.
  • showForResult(data: T): Maybe<R> : muestra un cuadro de diálogo y abre la secuencia para obtener el resultado.
  • sendResult(result: R) : envía el resultado, se llama desde el lado Vista.
  • dismiss() : solo oculta el diálogo.

DialogControl almacena el estado: ¿hay un diálogo en la pantalla o no? (Visualizado / Ausente). Así es como se ve en el código de clase:


 class DialogControl<T, R> internal constructor(pm: PresentationModel) { val displayed = pm.State<Display>(Absent) private val result = pm.Action<R>() sealed class Display { data class Displayed<T>(val data: T) : Display() object Absent : Display() } // ... } 

Cree un PresentationModel simple:


 class SamplePresentationModel : PresentationModel() { enum class ConfirmationDialogResult { CONFIRMED, CANCELED } //        enum    val confirmationDialog = dialogControl<Unit, ConfirmationDialogResult>() val buttonClicks = Action<Unit>() override fun onCreate() { super.onCreate() buttonClicks.observable .switchMapMaybe { //           confirmationDialog.showForResult(Unit) .filter { it == ConfirmationDialogResult.CONFIRMED } } .subscribe { //   } .untilDestroy() } } 

Tenga en cuenta que el procesamiento de clics, la confirmación de confirmación y el procesamiento de acciones se implementan en la misma cadena. Esto le permite enfocar el código y no dispersar la lógica en varias devoluciones de llamada.


Luego, simplemente vinculamos DialogControl a la Vista usando la extensión bindTo.
Recopilamos el AlertDialog habitual y enviamos el resultado a través de sendResult:


 class SampleActivity : PmSupportActivity<SamplePresentationModel>() { override fun providePresentationModel() = SamplePresentationModel() //     View  PresentationModel override fun onBindPresentationModel(pm: SamplePresentationModel) { pm.confirmationDialog bindTo { data, dialogControl -> AlertDialog.Builder(this@SampleActivity) .setMessage("Please, confirm the action") .setPositiveButton("Confirm") { _, _ -> dialogControl.sendResult(CONFIRMED) } .setNegativeButton("Cancel") { _, _ -> dialogControl.sendResult(CANCELED) } .create() } button.clicks() bindTo pm.buttonClicks } } 

En un escenario típico, algo así sucede debajo del capó:


  1. Hacemos clic en el botón, el evento a través de la acción "buttonClicks" entra en PresentationModel.
  2. Para este evento, iniciamos la visualización del cuadro de diálogo a través de la llamada a showForResult.
  3. Como resultado, el estado en DialogControl cambia de Ausente a Visualizado.
  4. Cuando se recibe el evento Displayed, se llama a la lambda que pasamos en el enlace bindTo. Se crea un objeto de diálogo en él, que luego se muestra.
  5. El usuario presiona el botón Confirmar, el oyente dispara y el resultado del clic se envía a DialogControl llamando a sendResult.
  6. A continuación, el resultado cae en el "resultado" interno de la acción, y el estado de Visualizado cambia a Ausente.
  7. Cuando se recibe un evento ausente, se cierra el diálogo actual.
  8. El evento del "resultado" de la acción cae en la secuencia que abrió la llamada a showForResult y es procesada por la cadena en PresentationModel.

Vale la pena señalar que el cuadro de diálogo se cierra incluso cuando la vista se desata de PresentationModel. En este caso, el estado permanece Visualizado. Se recibirá en el próximo enlace y se restablecerá el diálogo.


Como puede ver, la necesidad de DialogFragment ha desaparecido. El cuadro de diálogo se muestra cuando la Vista se adjunta al Modelo de presentación y se oculta cuando la Vista se desata. Debido al hecho de que el estado se almacena en DialogControl, que a su vez se almacena en PresentationModel, el diálogo se restaura después de que se gira el dispositivo.


Escribir diálogos correctamente


Hemos examinado varias formas de mostrar cuadros de diálogo. Si sigues apareciendo de la primera manera, te lo ruego, no lo hagas más. Para los amantes de MVP no queda nada más que usar el método estándar, que se describe en la documentación oficial. Desafortunadamente, la tendencia al imperativo de este patrón no permite hacer lo contrario. Bueno, les recomiendo a los fanáticos de RxJava que observen más de cerca el método reactivo y nuestra biblioteca RxPM .

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


All Articles