【问题标题】:NullPointerException on dismiss DialogFragment关闭 DialogFragment 时出现 NullPointerException
【发布时间】: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 中传递了什么?

标签: android kotlin nullpointerexception android-dialogfragment


【解决方案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 的原因。

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

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

  1. 从干净代码的角度来看,最佳选择是不要保留对 DialogFragment 中 Activity 的引用。这是一种不好的做法,因为即使没有崩溃也可能导致内存泄漏。考虑使用 MVVM 架构模式:您可以保留对 ViewModel 的引用,因为它不会重新创建,它始终是同一个实例。因此,在您的回调中,您可以调用 viewModel.closeScreen() 之类的东西,ViewModel 会找到当前活动并在其上调用 .onBackPressed()
  2. 在这种情况下,您最好的选择可能是在重新创建 Activity 时找到您的 DialogFragment 并更新它的回调。例如
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val dialog = childFragmentManager.findFragmentByTag(TAG)
    if (dialog != null) {
        dialog.setOnDismissListener {
            activity?.onBackPressed()
        }
    }
    ...
}
  1. 最后,我不建议这样做(但在大多数情况下可能会解决您的崩溃问题)是禁止在屏幕旋转时重新创建您的 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;
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-06-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-11-15
      • 1970-01-01
      • 2014-08-10
      • 1970-01-01
      相关资源
      最近更新 更多