Исключение NullPointerException при отклонении диалогового фрагмента

#android #kotlin #nullpointerexception #android-dialogfragment

#Android #kotlin #исключение nullpointerexception #android-dialogfragment

Вопрос:

Я получаю довольно много ошибок в crashlitycs, когда пытаюсь отклонить диалоговый фрагмент. Это ошибка, которую я получаю:

 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 amp;amp; popUpErrorCard?.isVisible == false amp;amp; manager.findFragmentByTag(ERROR_DIALOG_TAG) == null) {
        popUpErrorCard?.show(manager, ERROR_DIALOG_TAG)
        manager.executePendingTransactions()
    }
}
 

Строка, в которой я получаю ошибку actionOnDismiss?.invoke()

И, наконец, этот диалоговый фрагмент:

 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 amp;amp; 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 amp;amp; 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 amp;amp; 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
        }
    }
}
 

}

В диалоговом фрагменте ошибка здесь dismissLitener?.onDismiss()

Как вы можете видеть, во всех строках, вызывающих ошибку, есть безопасные вызовы (?), Поэтому я не знаю, почему я получаю исключение NullPointerException, и я не смог воспроизвести его, поэтому я не могу дать более подробную информацию о проблеме.

Комментарии:

1. Скорее всего, это что-то из самого onBackPressed() звонка. Рассмотрите возможность изучения всей трассировки стека, а не только самой верхней строки.

2. onBackPressed — это метод, который является частью Android sdk, а не пользовательский метод.

3. Ну android.app.FragmentHostCallback.getHandler , вызов, в котором происходит NPE, также, вероятно, происходит из какого-то кода платформы. Обратите внимание, что там есть устаревшие фрагменты android.app, а не фрагменты jetpack androidx.app.

4. это странно, поскольку я использую фрагменты androidx.app. Я не знаю, кто (библиотека или что-то еще) использует android.фрагменты приложения Я могу показать вам импорт, который я использую, если вы считаете, что это может помочь найти ошибку.

5. как вы звоните showGenericError ? что вы передаете actionOnDismiss ?

Ответ №1:

Не следует

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

на самом деле быть

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

Комментарии:

1. это функция расширения, которую я создал, и с тем, что я пишу, я делаю то же самое, что вы рекомендуете.

Ответ №2:

Я получаю довольно много ошибок в crashlitycs

Я предполагаю, что вы не знаете точных шагов для воспроизведения сбоя, не так ли? Сбой может произойти после того, как ваша активность будет воссоздана, пока диалоговый фрагмент все еще отображается. Например: открыть диалоговое окно -> повернуть экран -> отклонить диалоговое окно.

Что произойдет после поворота экрана? Будет создано новое действие, диалоговый фрагмент будет отделен от старого действия и присоединен к новому. Но ваш диалоговый фрагмент сохраняет ссылку на старое действие в своем dismissListener обратном вызове. Итак, вы пытаетесь вызвать .onBackPressed() действие, этап жизненного цикла которого «уничтожен». Это запрещено в Android Framework, и это может быть причиной вашего NullPointerException .

Итак, что вы можете с этим сделать?

Я вижу здесь три разных решения:

  1. Лучший выбор с точки зрения чистого кода — не сохранять ссылку на действие в вашем диалоговом фрагменте. Это плохая практика, потому что даже без сбоев это может привести к утечке памяти. Рассмотрите возможность использования шаблона архитектуры MVVM: вы можете сохранить ссылку на ViewModel, потому что она не создается заново, это всегда один и тот же экземпляр. Итак, в вашем обратном вызове вы можете вызвать что-то вроде viewModel.closeScreen() , и ViewModel найдет текущую активность и вызовет .onBackPressed() ее.
  2. Вероятно, лучший выбор для вас в этой ситуации — найти ваш диалоговый фрагмент при повторном создании Activity и обновить его обратный вызов. Например.
 override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val dialog = childFragmentManager.findFragmentByTag(TAG)
    if (dialog != null) {
        dialog.setOnDismissListener {
            activity?.onBackPressed()
        }
    }
    ...
}
 
  1. И, наконец, то, что я не рекомендую делать (но, вероятно, в большинстве случаев исправит ваш сбой), — это запретить повторное воспроизведение вашей активности при поворотах экрана. Просто добавьте android:configChanges="orientation" атрибуты вашей активности в AndroidManifest.xml . Но не забывайте, что есть много причин, по которым активность может быть воссоздана, и поворот экрана — только одна из них!

Комментарии:

1. спасибо за ответ, но в моем приложении не разрешен поворот экрана. Всегда находится в портретном режиме. Таким образом, действие не будет воссоздано, по крайней мере, для поворота экрана. Есть ли какая-либо другая причина, по которой действие может быть воссоздано заново?

2. Да, конечно. Любые изменения конфигурации вызывают перезапуск действий (например, язык, разрешение). Также, если ваша активность приостановлена, а в ОС мало памяти, она уничтожит активность, а затем воссоздаст ее из пакета. Это также можно сделать принудительно с помощью флага «не сохранять действия» в настройках разработчика. И я почти уверен, что автоматические тесты в Google Play могут принудительно выполнять действия, которые могут привести к сбоям в crashlytics.

3. Привет, @IbanArriola, я был бы очень признателен, если бы вы приняли мой ответ, если бы он вам помог. Я не вижу никаких других причин сбоев, кроме того, что я объяснил.

Ответ №3:

Быстрый обходной путь — использовать интерфейс / контракт вместо прямого доступа к активности хостинга, это запрещенное действие в мире SOLID. Итак, вместо:

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

Использовать

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

затем реализуйте интерфейс в родительском действии.

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

Код фреймворка, вызывающий этот метод, передает значение, полученное из a WeakReference , что означает, что dialog иногда оно будет равно null . Следовательно, вы должны определить метод для принятия значения null DialogInterface? .

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

Если вы не сделаете dialog nullable , ваше приложение будет аварийно завершать работу каждый раз, когда система передает нулевую ссылку на ваш 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;
        }
    }
}