关闭 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 时出现问题