Uma nova visão da exibição de diálogos no Android


A figura mostra o primeiro pensamento do leitor que se pergunta o que pode ser escrito sobre uma tarefa tão simples como exibir um diálogo. O gerente pensa da mesma forma: "Não há nada complicado aqui, nosso Vasya fará em 5 minutos". É claro que exagerei, mas na realidade nem tudo é tão simples quanto parece à primeira vista. Especialmente se estamos falando sobre Android.


Então, 2019 estava no quintal e ainda não sabemos como exibir diálogos corretamente .


Vamos fazê-lo em ordem e começar com a declaração do problema:


É necessário mostrar um diálogo simples com o texto para confirmar a ação e os botões "confirmar / cancelar". Ao clicar no botão "confirmar" - execute uma ação, no botão "cancelar" - feche a caixa de diálogo.

Solução na testa


Eu chamaria esse método de junior, porque não é a primeira vez que me deparei com um mal-entendido por que você não pode simplesmente usar o AlertDialog, como mostrado abaixo:


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

Uma maneira bastante comum para um desenvolvedor iniciante, é óbvio e intuitivo. Mas, como em muitos casos, ao trabalhar com o Android, esse método está completamente errado. Do nada, temos um vazamento de memória, basta ligar o dispositivo e você verá o seguinte erro nos logs:


stacktrace
 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) 

No Stackoverflow, a pergunta sobre esse problema é uma das mais populares. Em resumo, o problema é que mostramos a caixa de diálogo ou não a fechamos após a conclusão da ativação.


Obviamente, você pode cancelar a chamada na caixa de diálogo na atividade onPause ou onDestroy, conforme recomendado na resposta por referência . Mas isso não é exatamente o que precisamos. Queremos que o diálogo se recupere depois de ligar o dispositivo.


Maneira desatualizada


Antes de os fragmentos aparecerem no Android, as caixas de diálogo deveriam ser exibidas por meio de uma chamada para o método de ativação showDialog Nesse caso, a atividade gerencia corretamente o ciclo de vida do diálogo e o restaura após um turno. A criação do próprio diálogo teve que ser implementada no retorno de chamada 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); } } } 

Não é muito conveniente que você precise iniciar um identificador de diálogo e passar parâmetros pelo Bundle. E ainda podemos obter o problema da "janela vazada" se tentarmos exibir uma caixa de diálogo depois de chamar Destroy na atividade. Isso é possível, por exemplo, ao tentar mostrar um erro após uma operação assíncrona.


Em geral, esse problema é típico do Android, quando você precisa fazer algo após uma operação assíncrona, e a atividade ou fragmento já está destruído naquele momento. É provavelmente por isso que os padrões MV * são mais populares na comunidade Android do que entre os desenvolvedores do iOS.


Método da documentação


Apareceram fragmentos no Android Honeycomb , e o método descrito acima foi preterido, e o método showDialog da atividade foi marcado como preterido. Não, o AlertDialog não está desatualizado, pois muitos estão enganados. Agora mesmo existe o DialogFragment , que envolve o objeto de diálogo e controla seu ciclo de vida.


Os trechos nativos também foram descontinuados desde a API 28. Agora você deve usar apenas a implementação da Biblioteca de suporte (AndroidX).

Vamos realizar nossa tarefa, conforme prescrito na documentação oficial :


  1. Primeiro você precisa herdar de DialogFragment e implementar a criação de uma caixa de diálogo no método onCreateDialog.
  2. Descreva a interface do evento de diálogo e instancie o ouvinte no método onAttach.
  3. Implemente uma interface de evento de diálogo em uma atividade ou fragmento.

Se o leitor não está muito claro por que o ouvinte não pode ser passado pelo construtor, ele pode ler mais sobre isso aqui

Código do 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 Ativação:


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

Código suficiente acabou, certo?


Como regra, há algum tipo de MVP no projeto, mas decidi que as chamadas do apresentador podem ser omitidas nesse caso. No exemplo acima, vale a pena adicionar o método estático de criar a caixa de diálogo newInstance e passar parâmetros aos argumentos do fragmento, tudo conforme o esperado.


E tudo isso para que o diálogo oculte no tempo e seja restaurado corretamente. Não é de surpreender que essas questões surjam no Stackoverflow: uma e duas .


Encontrando a solução perfeita


O estado atual não nos convinha e começamos a procurar uma maneira de tornar o trabalho com diálogos mais confortável. Havia um sentimento de que você pode facilitar, quase como no primeiro método.


A seguir estão as considerações que nos guiaram:


  • Preciso salvar e restaurar o diálogo depois de encerrar o processo de inscrição?
    Na maioria dos casos, isso não é necessário, como no nosso exemplo, quando você precisa mostrar uma mensagem simples ou perguntar alguma coisa. Esse diálogo é relevante até que a atenção do usuário seja perdida. Se você restaurá-lo após uma longa ausência no aplicativo, o usuário perderá o contexto com a ação planejada. Portanto, você só precisa suportar as curvas do dispositivo e lidar corretamente com o ciclo de vida do diálogo. Caso contrário, a partir do movimento desajeitado do dispositivo, o usuário poderá perder a mensagem que acabou de ser aberta sem lê-la.
  • Ao usar DialogFragment, muito código clichê aparece, a simplicidade é perdida. Portanto, seria bom se livrar do fragmento como um invólucro e usar o Dialog diretamente . Para fazer isso, você precisará armazenar o estado da caixa de diálogo para mostrá-la novamente após recriar a View e ocultá-la quando a View morrer.
  • Todo mundo está acostumado a perceber a exibição do diálogo como uma equipe, especialmente se você trabalha apenas com o MVP. A tarefa da restauração subseqüente do estado é assumida pelo FragmentManager. Mas você pode olhar para essa situação de maneira diferente e começar a perceber o diálogo como um estado . Isso é muito mais conveniente ao trabalhar com padrões PM ou MVVM.
  • Dado que a maioria dos aplicativos agora usa abordagens reativas, é necessário que os diálogos sejam reativos . A principal tarefa não é quebrar a cadeia que inicia a exibição do diálogo e anexar um fluxo reativo de eventos para obter um resultado disso. Isso é muito conveniente no lado PresentationModel / ViewModel quando você está manipulando vários fluxos de dados.

Levamos em conta todos os requisitos acima e criamos uma maneira de exibir reativamente as caixas de diálogo, que implementamos com sucesso em nossa biblioteca RxPM (há um artigo separado sobre isso).


A solução em si não requer uma biblioteca e pode ser feita separadamente. Guiado pela idéia de "diálogo como estado", você pode tentar criar uma solução baseada no moderno ViewModel e LiveData. Mas deixarei isso para o leitor e depois falaremos sobre uma solução pronta da biblioteca.


Método reativo


Mostrarei como a tarefa inicial é resolvida no RxPM, mas primeiro algumas palavras sobre os principais conceitos da biblioteca:


  • PresentationModel - armazena um estado de reação, contém lógica da interface do usuário, sobrevive às curvas.
  • Estado é um estado reativo. Você pode pensar nisso como um invólucro sobre BehaviorRelay.
  • Ação - um wrapper sobre PublishRelay, serve para transferir eventos de View para PresentationModel.
  • Estado e Ação têm observável e consumidor.

A classe DialogControl é responsável pelo estado da caixa de diálogo. Possui dois parâmetros: o primeiro para o tipo de dados que deve ser exibido na caixa de diálogo, o segundo para o tipo de resultado. No nosso exemplo, o tipo de dados será Unit, mas pode ser uma mensagem para o usuário ou qualquer outro tipo.


DialogControl possui os seguintes métodos:


  • show(data: T) - apenas fornece um comando para exibir.
  • showForResult(data: T): Maybe<R> - mostre uma caixa de diálogo e abra o fluxo para obter o resultado.
  • sendResult(result: R) - envia o resultado, é chamado do lado de exibição.
  • dismiss() - apenas oculta a caixa de diálogo.

O DialogControl armazena o estado - existe ou não um diálogo na tela (Exibido / Ausente). É assim que fica no código da classe:


 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() } // ... } 

Crie um PresentationModel simples:


 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() } } 

Observe que o processamento de cliques, a confirmação de confirmação e o processamento de ações são implementados na mesma cadeia. Isso permite que você concentre o código e não espalhe a lógica por vários retornos de chamada.


Em seguida, simplesmente ligamos DialogControl à View usando a extensão bindTo.
Coletamos o AlertDialog usual e enviamos o resultado via 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 } } 

Em um cenário típico, algo assim acontece sob o capô:


  1. Clicamos no botão, o evento através da ação "buttonClicks" entra no PresentationModel.
  2. Para este evento, iniciamos a exibição da caixa de diálogo através da chamada para showForResult.
  3. Como resultado, o estado no DialogControl muda de ausente para exibido.
  4. Quando o evento Displayed é recebido, o lambda que passamos na ligação bindTo é chamado. Um objeto de diálogo é criado nele, que é exibido.
  5. O usuário pressiona o botão Confirmar, o ouvinte é acionado e o resultado do clique é enviado ao DialogControl chamando sendResult.
  6. Em seguida, o resultado cai na Ação interna "resultado" e o estado de Exibido muda para Ausente.
  7. Quando um evento ausente é recebido, a caixa de diálogo atual é fechada.
  8. O evento do "resultado" da Ação cai no fluxo que foi aberto pela chamada para showForResult e é processado pela cadeia no PresentationModel.

Vale ressaltar que a caixa de diálogo é fechada mesmo quando o View é desatado do PresentationModel. Nesse caso, o status permanece exibido. Ele será recebido na próxima ligação e o diálogo será restaurado.


Como você pode ver, a necessidade de DialogFragment se foi. A caixa de diálogo é mostrada quando a Visualização é anexada ao PresentationModel e oculta quando a Visualização é desatada. Como o estado é armazenado no DialogControl, que por sua vez é armazenado no PresentationModel, a caixa de diálogo é restaurada após a rotação do dispositivo.


Escreva diálogos corretamente


Examinamos várias maneiras de exibir caixas de diálogo. Se você ainda está aparecendo da primeira maneira, eu imploro, não faça mais isso. Para os amantes do MVP, não resta nada além de usar o método padrão, descrito na documentação oficial. Infelizmente, a tendência à imperatividade desse padrão não permite fazer o contrário. Bem, eu recomendo aos fãs do RxJava um olhar mais atento ao método reativo e à nossa biblioteca RxPM .

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


All Articles