在Android中显示对话框的全新外观


该图显示了读者的第一个想法,他想知道关于显示对话这样的简单任务可以写些什么。 经理也这样认为:“这里没有什么复杂的,我们的Vasya将在5分钟内完成。” 我当然会夸大其词,但实际上一切都不像乍看起来那样简单。 特别是在我们谈论Android的时候。


因此,2019年就在院子里, 我们仍然不知道如何正确显示对话框


让我们按顺序进行,并从问题的陈述开始:


需要显示一个简单的对话,以确认操作和“确认/取消”按钮。 通过单击“确认”按钮-执行操作,通过“取消”按钮-关闭对话框。

前额解决方案


我将此方法称为初级,因为这不是我第一次遇到误解,为什么您不能只使用AlertDialog,如下所示:


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

对于新手开发人员来说,这是一种相当常见的方式,它显而易见且直观。 但是,就像在使用Android的许多情况下一样,这种方法是完全错误的。 出乎意料的是,我们遇到了内存泄漏,只需打开设备,您就会在日志中看到以下错误:


堆栈跟踪
 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) 

在Stackoverflow上,关于此问题的问题是最受欢迎的问题之一。 简而言之,问题在于激活完成后我们要么显示对话框,要么不关闭对话框。


当然,您可以按照on reference的建议在onPause或onDestroy活动中的对话框上调用dismiss。 但这并不是我们真正需要的。 我们希望在打开设备后恢复对话。


过时的方式


在片段出现在Android中之前,应该已经通过调用showDialog激活方法来显示对话框。 在这种情况下,活动可以正确管理对话的生命周期,并在转弯后恢复对话。 对话本身的创建必须在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); } } } 

必须启动对话框标识符并通过Bundle传递参数并不是很方便。 如果在活动上调用onDestroy后尝试显示对话框,我们仍然会遇到“泄漏的窗口”问题。 例如,当尝试在异步操作后显示错误时,这是​​可能的。


通常,此问题对于Android来说很典型,当您需要在异步操作之后执行某项操作,并且此时活动或片段已被销毁。 这可能就是为什么MV *模式在Android社区中比在iOS开发人员中更受欢迎的原因。


文档中的方法


片段出现在Android Honeycomb中 ,并且不建议使用上述方法,并且将该活动showDialog方法标记为不建议使用。 不,AlertDialog并没有过时,因为很多人都把它弄错了。 刚才有DialogFragment ,它包装对话框对象并控制其生命周期。


自28 API以来,本机摘要也已弃用。 现在,您应该只使用支持库(AndroidX)中​​的实现。

让我们按照官方文档的规定完成任务:


  1. 首先,您需要继承DialogFragment并在onCreateDialog方法中实现对话框的创建。
  2. 描述对话框事件接口,并在onAttach方法中实例化侦听器。
  3. 在活动或片段中实现对话事件接口。

如果读者不太清楚为什么不能通过构造函数传递侦听器,那么他可以在此处阅读更多内容

对话框片段代码:


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

激活码:


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

原来足够的代码,对不对?


通常,项目中有某种MVP,但是我决定在这种情况下可以省略主持人呼叫。 在上面的示例中,值得添加静态方法来创建newInstance对话框并将参数传递给片段参数,所有操作均符合预期。


如此一来,对话就可以及时隐藏并正确恢复。 在Stackoverflow上出现这样的问题不足为奇: 12


寻找完美的解决方案


当前的事务状况不适合我们,因此我们开始寻找一种使对话工作更舒适的方法。 感觉就像第一种方法一样,您可以简化它。


以下是指导我们的注意事项:


  • 终止申请程序后,我是否需要保存和恢复对话?
    在大多数情况下,这不是必需的,例如在我们的示例中,当您需要显示简单消息或提出问题时。 这种对话是有意义的,直到用户的注意力消失为止。 如果您在应用程序中长时间缺席后将其还原,则用户将丢失计划的操作的上下文。 因此, 您只需要支持设备的转弯并正确处理对话的生命周期即可。 否则,由于设备的笨拙运动,用户可能会丢失刚打开的消息而无法阅读。
  • 当使用DialogFragment时,出现太多样板代码,简单性丧失了。 因此,最好将片段作为包装器删除并直接使用Dialog 。 为此,您必须存储对话框的状态,以便在重新创建视图后再次显示该对话框,并在视图死后将其隐藏。
  • 每个人都习惯于以团队的方式感知对话的表现,特别是如果您仅与MVP合作时。 FragmentManager承担随后恢复状态的任务。 但是您可以从不同的角度看待这种情况,并开始将对话视为一种状态 。 使用PM或MVVM模式时,这更加方便。
  • 鉴于大多数应用程序现在都使用反应性方法,因此需要对话框是反应性的 。 主要任务是不中断启动对话显示的链,并附加事件的反应流以从中获取结果。 当您处理多个数据流时,这在PresentationModel / ViewModel一侧非常方便。

我们考虑了以上所有要求,并提出了一种反应式显示对话框的方法,该方法已在RxPM库中成功实现(有单独的文章 )。


该解决方案本身不需要库,可以单独完成。 在“以状态对话”的思想指导下,您可以尝试基于流行的ViewModel和LiveData构建解决方案。 但是我将保留此权利给读者,然后我们将讨论库中的现成解决方案。


反应方式


我将展示如何在RxPM中解决初始任务,但首先要介绍一下库中的关键概念:


  • PresentationModel-存储反应状态,包含UI逻辑,幸免于难。
  • 状态是一种反应性状态 。 您可以将其视为BehaviorRelay的包装。
  • 动作 -PublishRelay的包装,用于将事件从View传输到PresentationModel。
  • 国家行动具有可观察性和消费性。

DialogControl类负责对话框的状态。 它有两个参数:第一个用于对话框中应显示的数据类型,第二个用于结果类型。 在我们的示例中,数据类型将为Unit,但它可以是发给用户的消息或任何其他类型。


DialogControl具有以下方法:


  • show(data: T) -仅给出显示命令。
  • showForResult(data: T): Maybe<R> -显示一个对话框并打开流以获取结果。
  • sendResult(result: R) -发送结果,从View端调用。
  • dismiss() -仅隐藏对话框。

DialogControl存储状态-屏幕上是否有对话框(显示/不存在)。 这是在类代码中的外观:


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

创建一个简单的PresentationModel:


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

请注意,点击处理,确认确认和操作处理是在同一链中实现的。 这使您可以使代码集中精力,而不会将逻辑分散在多个回调中。


接下来,我们只需使用bindTo扩展将DialogControl绑定到View。
我们收集通常的AlertDialog,并通过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 } } 

在典型情况下,类似的情况在后台发生:


  1. 我们单击按钮,事件通过Action“ buttonClicks”进入PresentationModel。
  2. 对于此事件,我们通过调用showForResult启动对话框的显示。
  3. 结果,DialogControl中的状态从“不存在”更改为“已显示”。
  4. 收到Displayed事件时,将调用我们在bindTo绑定中传递的lambda。 在其中创建一个对话框对象,然后将其显示。
  5. 用户按下“确认”按钮,将触发侦听器,并且通过调用sendResult将单击结果发送到DialogControl。
  6. 接下来,结果属于内部操作“结果”,并且状态从“已显示”更改为“不存在”。
  7. 收到缺席事件时,当前对话框关闭。
  8. 动作“结果”中的事件落入由showForResult调用打开的流中,并由PresentationModel中的链处理。

值得注意的是,即使将View从PresentationModel中解开,对话框也会关闭。 在这种情况下,状态保持为显示。 它将在下一个绑定中收到,并且对话将恢复。


如您所见,不再需要DialogFragment。 当视图附加到PresentationModel时,将显示该对话框,而取消绑定视图时,该对话框将被隐藏。 由于状态存储在DialogControl中,而状态又存储在PresentationModel中,因此旋转设备后将恢复对话框。


正确编写对话框


我们研究了几种显示对话框的方法。 如果您仍然以第一方式进行展示,那么请您不要再这样做了。 对于MVP爱好者,除了使用标准方法外,别无他法,这在官方文档中有描述。 不幸的是,这种模式势在必行的趋势不允许这样做。 好吧,我建议RxJava爱好者进一步了解反应式方法和我们的RxPM库。

Source: https://habr.com/ru/post/zh-CN440284/


All Articles