Мое приложение Kotlin аварийно завершает работу при изменении фрагментов

Мое приложение Kotlin аварийно завершает работу, когда я меняю фрагменты, позвольте мне объяснить.

В моем приложении есть несколько фрагментов, которые являются моими страницами. В своих фрагментах я ссылаюсь на представление XML с помощью viewBinding вот так:

private var binding: MyFragmentBinding by autoCleared()

Я сделал несколько вызовов API и другую обработку в onViewCreated (которая работает отлично), пока все не стало хорошо, но теперь, если я нажму кнопку «Назад» на телефоне до того, как элементы завершат загрузку, приложение вылетит с ошибкой:

FATAL EXCEPTION: main (Ask Gemini)
Process: fr.trecobat.brick, PID: 6594
java.lang.IllegalStateException: should never call auto-cleared-value get when it might not be available
at fr.trecobat.brick.utils.AutoClearedValue.getValue(AutoClearedValue.kt:32)
fr.trecobat.brick.ui.itineraire_chantiers.ItineraireChantiersFragment.getBinding(ItineraireChantiersFragment.kt:198)

Проблема в том, что я пытаюсь получить доступ к привязке, когда фрагмент уничтожен.

Точно так же, если спамить кнопку назад телефона, то думаю, что предыдущий фрагмент не успевает загрузиться и возвращаюсь к предыдущему фрагменту, поэтому приложение вылетает с такой ошибкой:

FATAL EXCEPTION: main (Ask Gemini)
Process: fr.trecobat.brick, PID: 26518
java.lang.IllegalArgumentException: Navigation action/destination fr.trecobat.brick:id/action_tourneeFragment_to_selectionChantierFragment cannot be found from the current destination Destination(fr.trecobat.brick:id/selectionChantierFragment) label=SelectionChantierFragment class=fr.trecobat.brick.ui.selection_chantier.SelectionChantierFragment
at fr.trecobat.brick.ui.tournee.TourneeFragment$onViewCreated$3.handleOnBackPressed(TourneeFragment.kt:176)

На данный момент я попробовал эти два решения.

Я заменил строку, которая автоматически очищает привязку, на эту:

private var binding: MyFragmentBinding by notNull()

Кажется, это работает, но я теряю самоочистку и потенциально подвергаюсь утечкам памяти, верно?

Для второго варианта в моем onBackPressedDispatcher я установил таймер:

requireActivity().onBackPressedDispatcher.addCallback(
    viewLifecycleOwner,
    object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            if (canNavigate) {
                canNavigate = false
                handler.postDelayed({ canNavigate = true }, 300)  // 300ms

                findNavController().navigate(
                    R.id.action_itineraireChantiersFragment_to_tourneeAffairesFragment,
                    bundleOf(
                        "touId" to touId,
                    )
                )
            }else{
                Timber.e("Stop back")
            }
        }
    }
)

Это позволяет избежать агрессивного переключения страниц, но является ли это правильным решением?

Есть ли у вас лучшие решения моих проблем?

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
0
67
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

В этой документации представители Android учат вас, как самостоятельно определять утечку памяти, чтобы им не приходилось отвечать на каждый тестовый пример, который может выполнить пользователь. Кроме того, вы можете использовать LeakCanary, который отлично справляется с обнаружением утечек памяти.

Представьте, что у вас есть фрагмент и он имеет ссылки на некоторые свойства. Нам нужно очистить ссылки на эти поля, присвоив значение null в методе жизненного цикла onDestroyView(), чтобы Fragment не содержал ссылок на них.

Итак, в вашем случае нет необходимости использовать autoCleared() и notNull(), вы можете попробовать что-то простое:

    private var _binding: ProfileBinding? = null
    private val binding get() = _binding!!
    private val mAdapter = MyListAdapter()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
       _binding = ProfileBinding.inflate(inflater, container, false)
       val view = binding.root
       binding.recyclerView.adapter = mAdapter
       return view
    }

    override fun onDestroyView() {
       super.onDestroyView()
       binding.recyclerView.adapter = null
       _binding = null
    }

Нет смысла очищать адаптер таким образом. Помните, что вся структура представления (включая RecyclerView) выбрасывается.

Ryan M 23.07.2024 11:32

Да, именно, вы правы, но основная идея в том, что Фрагменты переживают свои взгляды. поэтому обязательно удалите все ссылки на экземпляр класса привязки в методе onDestroyView() фрагмента.

Youssef Islem 23.07.2024 11:40
Ответ принят как подходящий

Проблема в том, что я пытаюсь получить доступ к привязке, когда фрагмент уничтожен.

Да, вызовы API могут занять некоторое время, и они могут оставаться активными, даже если ваш фрагмент будет уничтожен. Когда они завершат работу и попытаются получить доступ к привязке, она может оказаться нулевой, поскольку ваш фрагмент уже может быть уничтожен.

Кажется, это работает, но я теряю самоочистку и потенциально подвергаюсь утечкам памяти, верно?

Да, из-за того, как были разработаны фрагменты, они могут пережить свои представления и могут быть повторно использованы с другим представлением. Поэтому нам нужно обнулить привязку при уничтожении фрагмента.

Рекомендуемое решение

private class YourViewModel : ViewModel() {
    private val _apiResult = MutableStateFlow<String?>(null)
    val apiResult get() = _apiResult.asStateFlow()

    fun loadDataFromApi() {
        val hello = "world" // load data from api
        _apiResult.update { hello }
    }
}

И в твоем фрагменте

private val viewModel by viewModels<YourViewModel>()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    viewLifecycleOwner.lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            launch {
                viewModel.apiResult.collect { apiResult ->
                    // set data to your views here
                }
            }
        }
    }

    viewModel.loadDataFromApi()
}
  • Хакерский трюк

Вы можете проверить, жив ли фрагмент и безопасен ли он для использования привязки, прежде чем получить доступ к привязке.

fun Fragment.isSafe(): Boolean {
        return !(this.isRemoving || this.activity == null || this.isDetached || !this.isAdded || this.view == null)
}

Что касается второй проблемы, то, похоже, для ее выявления требуется больше контекста. Возможно, этот ответ поможет.

Другие вопросы по теме