#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
.
Итак, что вы можете с этим сделать?
Я вижу здесь три разных решения:
- Лучший выбор с точки зрения чистого кода — не сохранять ссылку на действие в вашем диалоговом фрагменте. Это плохая практика, потому что даже без сбоев это может привести к утечке памяти. Рассмотрите возможность использования шаблона архитектуры MVVM: вы можете сохранить ссылку на ViewModel, потому что она не создается заново, это всегда один и тот же экземпляр. Итак, в вашем обратном вызове вы можете вызвать что-то вроде
viewModel.closeScreen()
, и ViewModel найдет текущую активность и вызовет.onBackPressed()
ее. - Вероятно, лучший выбор для вас в этой ситуации — найти ваш диалоговый фрагмент при повторном создании Activity и обновить его обратный вызов. Например.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val dialog = childFragmentManager.findFragmentByTag(TAG)
if (dialog != null) {
dialog.setOnDismissListener {
activity?.onBackPressed()
}
}
...
}
- И, наконец, то, что я не рекомендую делать (но, вероятно, в большинстве случаев исправит ваш сбой), — это запретить повторное воспроизведение вашей активности при поворотах экрана. Просто добавьте
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;
}
}
}