关闭 DialogFragment 时出现 NullPointerException

Posted

技术标签:

【中文标题】关闭 DialogFragment 时出现 NullPointerException【英文标题】:NullPointerException on dismiss DialogFragment 【发布时间】:2021-04-11 08:33:01 【问题描述】:

当我尝试关闭 DialogFragment 时,我在 crashlititycs 中遇到很多错误。这是我得到的错误:

Attempt to invoke virtual method 'android.os.Handler android.app.FragmentHostCallback.getHandler()' on a null object reference 

我得到的线路是这个showGenericError activity?.onBackPressed()

viewLifecycleOwner.observe(viewModel.showErrorAndExit, 
    showGenericError  activity?.onBackPressed() 
)

这是初始化对话框的方法:

fun showGenericError(actionOnDismiss: (() -> Unit)? = null) 
    val manager = childFragmentManager

    if (popUpErrorCard == null) 
        popUpErrorCard = PopupCard.Builder(R.string.button_try_later)?.apply 
            setDescription(R.string.error_card_description_text)
            setTitle(R.string.subscribe_error_dialog_title)
            setImage(R.drawable.channels_error_popup)
        .build()?.apply 
            setDismissListener(object : PopupCard.DismissListener 
                override fun onDismiss() 
                    actionOnDismiss?.invoke()
                
            )
        
    

    if (popUpErrorCard?.isAdded == false && popUpErrorCard?.isVisible == false && manager.findFragmentByTag(ERROR_DIALOG_TAG) == null) 
        popUpErrorCard?.show(manager, ERROR_DIALOG_TAG)
        manager.executePendingTransactions()
    

我收到错误的行是actionOnDismiss?.invoke()

最后的 DialogFragment 就是这个:

class PopupCard private constructor() : DialogFragment() 

private lateinit var dialog: AlertDialog
private var negativeListener: View.OnClickListener? = null
private var positiveListener: View.OnClickListener? = null
private var dismissLitener: DismissListener? = null

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog 
    val builder = AlertDialog.Builder(requireActivity())
    val inflater = requireActivity().layoutInflater
    val view = inflater.inflate(R.layout.popup_card, null)

    @Suppress("UNCHECKED_CAST")
    arguments?.let args@ bundle ->
        val negativeText: Int? = bundle.getInt(NEGATIVE_BUTTON_TEXT)

        if (negativeText != null && negativeText != 0) 
            view.negativeButton.setText(negativeText)
         else 
            view.negativeButton.visibility = View.GONE
        

        val image: Int? = bundle.getInt(IMAGE_RESOURCE)
        image?.let 
            view.imageHeader.setImageResource(it)
         ?: run 
            view.imageHeader.visibility = View.GONE
        

        val titleRes: Int? = bundle.getInt(TITLE_RES)
        val titleText: String? = bundle.getString(TITLE)
        when 
            !titleText.isNullOrBlank() -> 
                view.title.text = titleText
            
            titleRes != null && titleRes != 0 -> 
                view.title.setText(titleRes)
            
            else -> view.title.visibility = View.GONE
        

        val descriptionRes: Int? = bundle.getInt(DESCRIPTION_RES)
        val descriptionText: String? = bundle.getString(DESCRIPTION)
        when 
            !descriptionText.isNullOrBlank() -> 
                view.description.text = descriptionText
            
            descriptionRes != null && descriptionRes != 0 -> 
                view.description.setText(descriptionRes)
            
            else -> view.description.visibility = View.GONE
        

        val actionPair = bundle.getInt(POSITIVE_BUTTON_TEXT)
        view.positiveButton.setText(actionPair)
    

    builder.setView(view)

    dialog = builder.create()

    view.positiveButton.setOnClickListener 
        positiveListener?.onClick(it)
        dialog.dismiss()
    

    view.negativeButton.setOnClickListener 
        negativeListener?.onClick(it)
        dialog.dismiss()
    

    return dialog


fun setOnPositiveClickListener(listener: View.OnClickListener) 
    this.positiveListener = listener


fun setOnNegativeClickListener(listener: View.OnClickListener) 
    this.negativeListener = listener


fun setDismissListener(listener: DismissListener) 
    this.dismissLitener = listener


override fun onDismiss(dialog: DialogInterface) 
    super.onDismiss(dialog)
    dismissLitener?.onDismiss()


interface DismissListener 
    fun onDismiss()


companion object 

    private const val NEGATIVE_BUTTON_TEXT = "PopupCard#NEGATIVE_BUTTON_TEXT"
    private const val IMAGE_RESOURCE = "PopupCard#IMAGE_RESOURCE"
    private const val TITLE = "PopupCard#TITLE"
    private const val TITLE_RES = "PopupCard#TITLE_RES"
    private const val DESCRIPTION = "PopupCard#DESCRIPTION"
    private const val DESCRIPTION_RES = "PopupCard#DESCRIPTION_RES"
    private const val POSITIVE_BUTTON_TEXT = "PopupCard#POSITIVE_BUTTON_TEXT"


class Builder(
    @StringRes private val positiveText: Int
) 

    private var negativeText: Int? = null
    @DrawableRes
    private var image: Int? = null
    @StringRes
    private var titleRes: Int? = null
    private var titleText: String? = null
    @StringRes
    private var descriptionRes: Int? = null
    private var descriptionText: String? = null

    fun setTitle(@StringRes title: Int): Builder 
        this.titleRes = title
        return this
    

    fun setTitle(title: String): Builder 
        this.titleText = title
        return this
    

    fun setDescription(@StringRes description: Int): Builder 
        this.descriptionRes = description
        return this
    

    fun setDescription(description: String): Builder 
        this.descriptionText = description
        return this
    

    fun setNegativeText(@StringRes negativeText: Int): Builder 
        this.negativeText = negativeText
        return this
    

    fun setImage(@DrawableRes image: Int): Builder 
        this.image = image
        return this
    

    fun build(): PopupCard 
        val bundle = Bundle().apply 
            negativeText?.let 
                putInt(NEGATIVE_BUTTON_TEXT, it)
            
            image?.let 
                putInt(IMAGE_RESOURCE, it)
            
            titleRes?.let 
                putInt(TITLE_RES, it)
            
            titleText?.let 
                putString(TITLE, it)
            
            descriptionRes?.let 
                putInt(DESCRIPTION_RES, it)
            
            descriptionText?.let 
                putString(DESCRIPTION, it)
            
            putInt(POSITIVE_BUTTON_TEXT, positiveText)
        
        return PopupCard().apply 
            arguments = bundle
        
    

在 DialogFragment 中出现错误dismissLitener?.onDismiss()

正如您在所有导致错误的行中看到的那样,都有安全调用 (?) 所以我不知道为什么我会收到 NullPointerException 并且我无法重现它,所以我无法提供有关该问题的更多详细信息.

【问题讨论】:

这可能是onBackPressed() 调用本身中的某些内容。考虑检查完整的堆栈跟踪,而不仅仅是最上面的行。 onBackPressed 是 android sdk 的一部分的方法,不是自定义方法。 好吧,发生 NPE 的 android.app.FragmentHostCallback.getHandler 调用也可能来自某些平台代码。请注意,它有已弃用的 android.app 片段,而不是 jetpack androidx.app 片段。 这很奇怪,因为我使用的是 androidx.app 片段。我不知道谁(库或其他)在使用 android.app 片段,如果您认为它可以帮助找到错误,我可以向您展示我正在使用的导入。 你怎么打电话给showGenericError?你在actionOnDismiss 中传递了什么? 【参考方案1】:

不应该

    viewLifecycleOwner.observe(viewModel.showErrorAndExit, 
        showGenericError  activity?.onBackPressed() 
)

其实是

    viewModel.showErrorAndExit.observe(viewLifecycleOwner, Observer 
        showGenericError  activity?.onBackPressed() 
)

【讨论】:

这是我制作的扩展功能,我写的内容与您推荐的相同。【参考方案2】:

我在 crashlititycs 中遇到很多错误

我假设您不知道重现崩溃的确切步骤,是吗? 在仍然显示 DialogFragment 的情况下重新创建 Activity 后,可能会发生崩溃。例如:打开对话框 -> 旋转屏幕 -> 关闭对话框。

屏幕旋转后会发生什么?将创建新 Activity,DialogFragment 将从旧 Activity 中分离并附加到新 Activity。但是您的 DialogFragment 在其 dismissListener 回调中保留对旧 Activity 的引用。因此,您尝试在生命周期阶段被“破坏”的 Activity 上调用 .onBackPressed()。这在 Android 框架中是被禁止的,这可能是你 NullPointerException 的原因。

那么,你能用它做什么呢?

我在这里看到了三种不同的解决方案:

    从干净代码的角度来看,最佳选择是不要保留对 DialogFragment 中 Activity 的引用。这是一种不好的做法,因为即使没有崩溃也可能导致内存泄漏。考虑使用 MVVM 架构模式:您可以保留对 ViewModel 的引用,因为它不会重新创建,它始终是同一个实例。因此,在您的回调中,您可以调用 viewModel.closeScreen() 之类的东西,ViewModel 会找到当前活动并在其上调用 .onBackPressed()。 在这种情况下,您最好的选择可能是在重新创建 Activity 时找到您的 DialogFragment 并更新它的回调。例如
override fun onCreate(savedInstanceState: Bundle?) 
    super.onCreate(savedInstanceState)
    val dialog = childFragmentManager.findFragmentByTag(TAG)
    if (dialog != null) 
        dialog.setOnDismissListener 
            activity?.onBackPressed()
        
    
    ...

    最后,我不建议这样做(但在大多数情况下可能会解决您的崩溃问题)是禁止在屏幕旋转时重新创建您的 Activity。只需将android:configChanges="orientation" 添加到AndroidManifest.xml 中的Activity 属性即可。但不要忘记,可以重新创建 Activity 的原因有很多,而屏幕旋转只是其中之一!

【讨论】:

感谢您的回答,但我的应用中不允许屏幕旋转。始终处于纵向模式。因此,至少不会为屏幕旋转重新创建活动。还有其他原因可以重新创建活动吗? 是的,当然。任何配置更改都会触发活动重启(例如语言、分辨率)。此外,如果您的 Activity 暂停并且操作系统内存不足,它将破坏 Activity,然后从 Bundle 重新创建。这也可以通过开发人员设置中的“不保留活动”标志来强制执行。而且我很确定 Google Play 中的自动化测试可能会强制重新创建活动,这可能会导致您在 crashlytics 中崩溃。 嘿@IbanArriola 如果您接受我的回答,如果它对您有帮助,我将不胜感激。除了我已经解释过的原因之外,我没有看到任何其他导致崩溃的原因。【参考方案3】:

一种快速的解决方法是使用接口/合同而不是直接访问托管活动,这是 SOLID 世界中禁止的行为。 所以,而不是:

viewLifecycleOwner.observe(viewModel.showErrorAndExit, 
showGenericError  activity?.onBackPressed() )

使用

viewLifecycleOwner.observe(viewModel.showErrorAndExit, 
showGenericError  loosedCopuledActivity?.onBackPressTriggered() )

然后在父activity中实现接口。

class SomeHostActivity: AppCompatActivity(), OnBackPressCallback

然后在某个地方定义你的界面:

interface OnBackPressCallback
    fun onBackPressTriggered() 

您还需要在对话框中的某处定义您的loosedCoupledActivity,所以我们可以这样做:

fun onActivityCreate(bluhbluh: BluhBluh)
    super.onActivityCreate(bluhbluh)
    if(requireActivity() is OnBackPressCallback)
        loosedCoupledActivity = requireActivity()

【讨论】:

【参考方案4】:

在您的片段代码中,我看到了这个:

override fun onDismiss(dialog: DialogInterface) 
    super.onDismiss(dialog)
    dismissLitener?.onDismiss()

调用此方法的框架代码传入一个从WeakReference 检索的值,这意味着dialog 有时会为空。因此,您必须定义方法以接受可为空的DialogInterface?

override fun onDismiss(dialog: DialogInterface?) 
    super.onDismiss(dialog)
    dismissLitener?.onDismiss()

如果您不将dialog 设为可空,那么只要系统将空引用传递给您的onDismiss() 回调,您的应用就会崩溃,即使您从未实际使用过dialog 的值。


来自源代码:

private static final class ListenersHandler extends Handler 
    private final WeakReference<DialogInterface> mDialog;

    public ListenersHandler(Dialog dialog) 
        mDialog = new WeakReference<>(dialog);
    

    @Override
    public void handleMessage(Message msg) 
        switch (msg.what) 
            case DISMISS:
                ((OnDismissListener) msg.obj).onDismiss(mDialog.get());
                break;
            case CANCEL:
                ((OnCancelListener) msg.obj).onCancel(mDialog.get());
                break;
            case SHOW:
                ((OnShowListener) msg.obj).onShow(mDialog.get());
                break;
        
    

【讨论】:

以上是关于关闭 DialogFragment 时出现 NullPointerException的主要内容,如果未能解决你的问题,请参考以下文章

在 DialogFragment 中为 AlertDialog 膨胀自定义视图时出现问题

从适配器 Android 中的按钮显示 DialogFragment 时出现问题

方向更改时的 DialogFragment 回调

关闭 dialogFragment 时键盘未关闭

如何在 DialogFragment 中显示现有的 ListFragment

DialogFragment 中实现的 videoview 的 MediaController 未正确更新