Сбой приложения после активности в фоновом режиме

1

У меня проблема с приложением, которое использует ViewPager для отображения фрагмента. Все работает нормально, пока приложение не перейдет в фоновый режим и не будет убито из ОС. Кажется, что после восстановления у меня есть 2 IncidentScreenFragment, которые обрабатывают события, один с нулевым презентатором (MVP), который вылетает из моего приложения.

Мой HomeActivity выглядит так:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)
        presenter.onViewCreated()
        initViews(savedInstanceState)
    }

    private fun initViews(savedInstanceState: Bundle?){
        mapView.onCreate(savedInstanceState)
        mapView.getMapAsync(this)
        initFragment()
        initMenu()
    }
    private fun initFragment(){
        homeFragment = HomeScreenFragment.newInstance()
        incidentFragment = IncidentScreenFragment.newInstance()
        chatFragment = ChatFragment.newInstance()
        weatherFragment = WeatherFragment.newInstance()

        viewPager.adapter = ViewPagerAdapter(supportFragmentManager, this)
        viewPager.offscreenPageLimit = 4

        viewPager?.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
            override fun onPageScrollStateChanged(state: Int) {}
            override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
            override fun onPageSelected(position: Int) {bottom_navigation.currentItem = position}
        })
    }

    override fun getFragmentByPos(pos: Int): Fragment {
        return when(pos){
            0 -> homeFragment
            1 -> incidentFragment
            2 -> chatFragment
            3 -> weatherFragment
            else -> {
                homeFragment
            }
        }
    }

И мой адаптер:

class ViewPagerAdapter internal constructor(fm: FragmentManager, activity:infinite_software.intelligence_center.intelligencecenter.ui.home.FragmentManager) : FragmentPagerAdapter(fm) {

    private val COUNT = 4
    private val activity = activity

    override fun getItem(position: Int): Fragment{
        var fragment: Fragment? = null
        when (position) {
            0 -> fragment = activity.getFragmentByPos(0)
            1 -> fragment = activity.getFragmentByPos(1)
            2 -> fragment = activity.getFragmentByPos(2)
            3 -> fragment = activity.getFragmentByPos(3)
        }

        return fragment!!
    }

    override fun destroyItem(container: ViewGroup, position: Int, 'object': Any) {
        super.destroyItem(container, position, 'object')
    }

    override fun getCount(): Int {
        return COUNT
    }

    override fun getPageTitle(position: Int): CharSequence? {
        return "Section " + (position + 1)
    }
}

Каждый фрагмент имеет статический метод, который возвращает новый фрагмент:

    companion object {
        fun newInstance(): HomeScreenFragment {
            return HomeScreenFragment()
        }
    }

Когда приложение было убито в фоновом режиме, я обнаружил, что есть 2 объекта (фрагмента), которые прослушивают событие, один с правильно созданным Presenter, а другой - без.

Ниже моего абстрактного класса BaseFragment:

abstract class BaseFragment<P : BasePresenter<BaseView>> : BaseView,Fragment() {
    protected lateinit var presenter: P

    override fun getContext(): Context {
        return activity as Context
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return super.onCreateView(inflater, container, savedInstanceState)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        presenter = instantiatePresenter()
    }

    override fun showError(error: String) {
        (activity as BaseActivity<BasePresenter<BaseView>>).showError(error)
    }

    override fun showError(errorResId: Int) {
        (activity as BaseActivity<BasePresenter<BaseView>>).showError(errorResId)
    }

    abstract fun onBackPressed(): Boolean

    /**
     * Instantiates the presenter the Fragment is based on.
     */
    protected abstract fun instantiatePresenter(): P
    abstract val TAG: String

Код фрагмента инцидента:

class IncidentScreenFragment: BaseFragment<IncidentScreenPresenter>(), BaseView, IncidentView, AlertFilterListener, AlertItemClickListener, IncidentDetailListener {

    var rvAdapter : IncidentAdapter? = null

    var state : Int = LIST_STATE

    override fun instantiatePresenter(): IncidentScreenPresenter {
        return IncidentScreenPresenter(this)
    }

    override val TAG: String
        get() = "INCIDENT"

    override fun getContext(): Context {
        return activity as Context
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        super.onCreateView(inflater, container, savedInstanceState)
        return inflater.inflate(R.layout.fragment_incident, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initViews()
        presenter.onViewCreated()
        initObserve()
    }

    private fun initViews(){
        //Reclycler view
        alertRV.layoutManager = LinearLayoutManager(context)
        rvAdapter = IncidentAdapter(ArrayList(), context, this)
        alertRV.adapter = rvAdapter

        //Apply Listeners
        headerBox.setFilterListener(this)
        incidentDetailView.setListener(this)
    }

    override fun initObserve() {
        //Init observe presenter model
        val alertObserver = Observer<ArrayList<AlertModel>> { alerts ->
            Timber.d("Data received from Presenter [$alerts]")
            showAlertList(alerts)
        }
        presenter.filteredAlertList.observe(context as BaseActivity<BasePresenter<BaseView>>,alertObserver)
    }

    override fun updateThisFilters(boxState: Boolean, level: Int) {
        presenter.updateFilterList(boxState,level)
    }

    fun showOnlyThisLevel(level:Int){
        presenter.showOnlyThisLevel(level)
        headerBox.disableBoxExcept(level)
    }

    fun showAlertList(list: ArrayList<AlertModel>){
        rvAdapter?.updateData(list)
    }

    override fun onItemClick(model: AlertModel) {
        presenter.loadAlertDetail(model)
    }

    override fun showAlertDetail(model: AlertModel) {
        incidentDetailView.setUpFromModel(model)
        WhiteWizard.slideLeftEffect(incidentDetailView,incidentListRootElement)
        state = DETAIL_STATE
    }

    override fun onbackFromDetailPressed() {
        WhiteWizard.slideRightEffect(incidentListRootElement,incidentDetailView)
        state = LIST_STATE
    }

    override fun showLoader() {
        loaderIncident.visibility = View.VISIBLE
    }

    override fun hideLoader() {
        loaderIncident.visibility = View.INVISIBLE
    }

    override fun onBackPressed(): Boolean {
        when(state){
            LIST_STATE -> return false
            DETAIL_STATE -> {
                onbackFromDetailPressed()
                return true
            }
            else -> return false
        }
    }

    fun newInstance(): IncidentScreenFragment {
            return  IncidentScreenFragment()
    }

}

Когда я нажимаю на кнопку в homePage для отображения фрагмента контента, я получил:

 Process: XXXXXX, PID: 3192
    kotlin.UninitializedPropertyAccessException: lateinit property presenter has not been initialized
        at infinite_software.intelligence_center.intelligencecenter.base.BaseFragment.getPresenter(BaseFragment.kt:11)
        at XXXXXX.ui.home.incidentScreen.IncidentScreenFragment.showOnlyThisLevel(IncidentScreenFragment.kt:78)
        at XXXXXX.ui.home.HomeActivity.filterDataWithSeverity(HomeActivity.kt:110)
        at XXXXXX.ui.home.homeScreen.HomeScreenFragment.filterBy(HomeScreenFragment.kt:76)
        at XXXXXX.ui.home.homeScreen.HomeScreenFragment$initViews$5.onClick(HomeScreenFragment.kt:56)

Если я пытаюсь напечатать идентификатор фрагмента, я получаю 2 разных идентификатора из вызова методов showOnlyThisLevel() и onBackPressed(). Что я скучаю?

Теги:
kotlin
fragment
android-viewpager

1 ответ

0
Лучший ответ

После тщательного поиска доказательств я должен сделать вывод, что проблема связана с неправильным присвоением имени метода FragmentPagerAdapter от имени авторов библиотеки поддержки Android, в котором неясно указано, что абстрактный метод getItem(int position) должен возвращать новый экземпляр объекта. фрагмент, а не просто "получить экземпляр одного".

Конечно, мы мало что можем сделать с неправильным именем после того, как оно появилось в дикой природе в течение 7 лет, но, по крайней мере, мы можем исправить ошибку, связанную с этой проблемой, в вашем коде;)


Без лишних слов причина вашего NPE заключается в том, что onCreateView (где onCreateView ваш Presenter) никогда не вызывается.

Это происходит потому, что вы создаете фрагмент здесь:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContentView(R.layout.activity_main)
    ...
    homeFragment = HomeScreenFragment.newInstance()
    incidentFragment = IncidentScreenFragment.newInstance()
}

Вы возвращаете этот фрагмент изнутри getItem(int position) в вашем FragmentPagerAdapter:

override fun getItem(position: Int): Fragment = when(position) {
     ...
     1 -> activity.incidentFragment
     ...
}

Итак, что мы знаем о activity.incidentFragment это то, что в нем onCreateView() никогда не вызывается.

Это связано с тем, что он никогда не добавлялся в FragmentManager и никогда не отображался на экране.

Это потому, что super.onCreate(savedInstanceState) в Activity воссоздает все фрагменты, используя их конструктор без аргументов, посредством отражения, сохраняя их тег (см. findFragmentByTag).

Итак, как вы можете видеть в этом ответе, или, как я могу процитировать здесь:

    // Do we already have this fragment?
    String name = makeFragmentName(container.getId(), itemId);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);
    } else {
        fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));

Метод getItem(position) вызывается только в том случае, если Fragment не найден тегом фрагмента, который FragmentPagerAdapter устанавливает для фрагмента, который автоматически воссоздается после того, как нехватка памяти убивает ваше приложение.

Следовательно, ВАШ новый фрагмент (созданный вами вручную в Activity) НИКОГДА не используется, и поэтому он не имеет представления, никогда не инициализируется, никогда не добавляется в FragmentManager, это не тот экземпляр, который фактически находится внутри вашего ViewPager, и он аварийно завершает работу, когда Вы называете это. Boom!



Решением является создание фрагмента внутри метода getItem(position) FragmentPagerAdapter. Чтобы получить экземпляр фрагмента, используйте этот ответ.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню