
该图显示了读者的第一个想法,他想知道关于显示对话这样的简单任务可以写些什么。 经理也这样认为:“这里没有什么复杂的,我们的Vasya将在5分钟内完成。” 我当然会夸大其词,但实际上一切都不像乍看起来那样简单。 特别是在我们谈论Android的时候。
因此,2019年就在院子里, 我们仍然不知道如何正确显示对话框 。
让我们按顺序进行,并从问题的陈述开始:
需要显示一个简单的对话,以确认操作和“确认/取消”按钮。 通过单击“确认”按钮-执行操作,通过“取消”按钮-关闭对话框。
前额解决方案
我将此方法称为初级,因为这不是我第一次遇到误解,为什么您不能只使用AlertDialog,如下所示:
AlertDialog.Builder(this) .setMessage("Please, confirm the action") .setPositiveButton("Confirm") { dialog, which ->
对于新手开发人员来说,这是一种相当常见的方式,它显而易见且直观。 但是,就像在使用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;
必须启动对话框标识符并通过Bundle传递参数并不是很方便。 如果在活动上调用onDestroy后尝试显示对话框,我们仍然会遇到“泄漏的窗口”问题。 例如,当尝试在异步操作后显示错误时,这是可能的。
通常,此问题对于Android来说很典型,当您需要在异步操作之后执行某项操作,并且此时活动或片段已被销毁。 这可能就是为什么MV *模式在Android社区中比在iOS开发人员中更受欢迎的原因。
文档中的方法
片段出现在Android Honeycomb中 ,并且不建议使用上述方法,并且将该活动的showDialog方法标记为不建议使用。 不,AlertDialog并没有过时,因为很多人都把它弄错了。 刚才有DialogFragment ,它包装对话框对象并控制其生命周期。
自28 API以来,本机摘要也已弃用。 现在,您应该只使用支持库(AndroidX)中的实现。
让我们按照官方文档的规定完成任务:
- 首先,您需要继承DialogFragment并在onCreateDialog方法中实现对话框的创建。
- 描述对话框事件接口,并在onAttach方法中实例化侦听器。
- 在活动或片段中实现对话事件接口。
如果读者不太清楚为什么不能通过构造函数传递侦听器,那么他可以在此处阅读更多内容
对话框片段代码:
class ConfirmationDialogFragment : DialogFragment() { interface ConfirmationListener { fun confirmButtonClicked() fun cancelButtonClicked() } private lateinit var listener: ConfirmationListener override fun onAttach(context: Context?) { super.onAttach(context) try {
激活码:
class MainActivity : AppCompatActivity(), ConfirmationListener { private fun showConfirmationDialog() { ConfirmationDialogFragment() .show(supportFragmentManager, "ConfirmationDialogFragmentTag") } override fun confirmButtonClicked() {
原来足够的代码,对不对?
通常,项目中有某种MVP,但是我决定在这种情况下可以省略主持人呼叫。 在上面的示例中,值得添加静态方法来创建newInstance对话框并将参数传递给片段参数,所有操作均符合预期。
如此一来,对话就可以及时隐藏并正确恢复。 在Stackoverflow上出现这样的问题不足为奇: 1和2 。
寻找完美的解决方案
当前的事务状况不适合我们,因此我们开始寻找一种使对话工作更舒适的方法。 感觉就像第一种方法一样,您可以简化它。
以下是指导我们的注意事项:
- 终止申请程序后,我是否需要保存和恢复对话?
在大多数情况下,这不是必需的,例如在我们的示例中,当您需要显示简单消息或提出问题时。 这种对话是有意义的,直到用户的注意力消失为止。 如果您在应用程序中长时间缺席后将其还原,则用户将丢失计划的操作的上下文。 因此, 您只需要支持设备的转弯并正确处理对话的生命周期即可。 否则,由于设备的笨拙运动,用户可能会丢失刚打开的消息而无法阅读。 - 当使用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 }
请注意,点击处理,确认确认和操作处理是在同一链中实现的。 这使您可以使代码集中精力,而不会将逻辑分散在多个回调中。
接下来,我们只需使用bindTo扩展将DialogControl绑定到View。
我们收集通常的AlertDialog,并通过sendResult发送结果:
class SampleActivity : PmSupportActivity<SamplePresentationModel>() { override fun providePresentationModel() = SamplePresentationModel()
在典型情况下,类似的情况在后台发生:
- 我们单击按钮,事件通过Action“ buttonClicks”进入PresentationModel。
- 对于此事件,我们通过调用showForResult启动对话框的显示。
- 结果,DialogControl中的状态从“不存在”更改为“已显示”。
- 收到Displayed事件时,将调用我们在bindTo绑定中传递的lambda。 在其中创建一个对话框对象,然后将其显示。
- 用户按下“确认”按钮,将触发侦听器,并且通过调用sendResult将单击结果发送到DialogControl。
- 接下来,结果属于内部操作“结果”,并且状态从“已显示”更改为“不存在”。
- 收到缺席事件时,当前对话框关闭。
- 动作“结果”中的事件落入由showForResult调用打开的流中,并由PresentationModel中的链处理。
值得注意的是,即使将View从PresentationModel中解开,对话框也会关闭。 在这种情况下,状态保持为显示。 它将在下一个绑定中收到,并且对话将恢复。
如您所见,不再需要DialogFragment。 当视图附加到PresentationModel时,将显示该对话框,而取消绑定视图时,该对话框将被隐藏。 由于状态存储在DialogControl中,而状态又存储在PresentationModel中,因此旋转设备后将恢复对话框。
正确编写对话框
我们研究了几种显示对话框的方法。 如果您仍然以第一方式进行展示,那么请您不要再这样做了。 对于MVP爱好者,除了使用标准方法外,别无他法,这在官方文档中有描述。 不幸的是,这种模式势在必行的趋势不允许这样做。 好吧,我建议RxJava爱好者进一步了解反应式方法和我们的RxPM库。