Просмотр экземпляра null после поворота экрана и rxjava - PullRequest
0 голосов
/ 25 октября 2018

Проблема

Я разрабатываю приложение для Android с использованием синтетического связывания Kotlin, RxJava, Retrofit и шаблона MVP.В этом приложении есть экран, который должен извлечь некоторые данные из API и заполнить RecyclerView возвращенными данными.

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

10-25 18:20:00.368 4580-4580/com.sample.app E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.sample.app, PID: 4580
    java.lang.IllegalStateException: recyclerView must not be null
        at com.sample.app.features.atendimento.ClientesAtendimentosFragment.onAtendimentosLoaded(ClientesAtendimentosFragment.kt:56)
        at com.sample.app.features.atendimento.ClienteAtendimentoActivity.showClientesAtendimento(ClienteAtendimentoActivity.kt:126)
        at com.sample.app.features.atendimento.ClienteAtendimentoPresenter$getClientesAtendimento$3.accept(ClienteAtendimentoPresenter.kt:38)
        at com.sample.app.features.atendimento.ClienteAtendimentoPresenter$getClientesAtendimento$3.accept(ClienteAtendimentoPresenter.kt:18)
        at io.reactivex.internal.observers.ConsumerSingleObserver.onSuccess(ConsumerSingleObserver.java:63)
        at io.reactivex.internal.operators.single.SingleDoAfterTerminate$DoAfterTerminateObserver.onSuccess(SingleDoAfterTerminate.java:71)

Это происходит, когда я пытаюсь вызвать этот метод:

override fun onAtendimentosLoaded(atendimentos: List<UsuarioAtendimento>) {
    recyclerView.visibility = View.VISIBLE
    recyclerView.adapter = ClienteAtendimentoAdapter(atendimentos, this)
}

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

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

РЕДАКТИРОВАТЬ: Я также добавляю файлы MVP, чтобы помочь понять, что здесь происходит.

Presenter

class ClienteAtendimentoPresenter(private val repository: ClienteAtendimentoRepository): ClienteAtendimentoContract.Presenter {

    private var view: ClienteAtendimentoContract.View? = null

    private var disposable: Disposable? = null

    override fun getClientesAtendimento() {
        try {
            disposable = repository.getClientesEmAtendimento()
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .doOnSubscribe {
                        view?.hideErrors()
                        view?.showLoading()
                    }
                    .doAfterTerminate {
                        view?.hideLoading()
                    }
                    .subscribe(
                            {
                                view?.showClientesAtendimento(it)
                            },
                            {
                                handleError(it)
                            }
                    )
        } catch (e: NoConnectionException) {
            view?.hideLoading()
            handleError(e)
        }
    }

    override fun stop() {
        view = null
        disposable?.dispose()
    }

    override fun attachView(view: BaseView<BasePresenter>) {
        if (view is ClienteAtendimentoContract.View)
            this.view = view
    }
}

Просмотр

class ClienteAtendimentoActivity : BaseActivity(), ClienteAtendimentoContract.View {

    override val presenter: ClienteAtendimentoContract.Presenter by inject()

    private lateinit var mSectionsPagerAdapter: SectionsPagerAdapter

    inner class SectionsPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {
        private val fragAtendimento = ClientesAtendimentosFragment.newInstance()
        private val fragEspera = ClientesAtendimentosFragment.newInstance()

        override fun getItem(position: Int): Fragment {
            return if (position == 0) fragAtendimento else fragEspera
        }

        override fun getCount(): Int = 2
    }

    private fun setupTabs() {
        mSectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager)

        // Set up the ViewPager with the sections adapter.
        container.adapter = mSectionsPagerAdapter

        container.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabs))
        tabs.addOnTabSelectedListener(TabLayout.ViewPagerOnTabSelectedListener(container))
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_cliente_atendimento)

        // ...

        setupTabs()
    }

    override fun onResume() {
        super.onResume()
        presenter.attachView(this)
        presenter.getClientesAtendimento()
    }

    override fun onPause() {
        super.onPause()
        presenter.stop()
    }

    // ...

    override fun showClientesAtendimento(atendimentos: AtendimentosLista) {
        val atendimentosFrag = mSectionsPagerAdapter.getItem(0) as ClientesAtendimentosFragment
        val filaEsperaFrag = mSectionsPagerAdapter.getItem(1) as ClientesAtendimentosFragment

        atendimentosFrag.onAtendimentosLoaded(atendimentos.atendimentos) // This is where I'm getting the error.
        filaEsperaFrag.onAtendimentosLoaded(atendimentos.espera)
    }

    // ...

}

Фрагмент

class ClientesAtendimentosFragment : Fragment(), AtendimentosFragmentCallback, OnClickAtendimentoCallback {

    private lateinit var recyclerView: RecyclerView

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val rootView = inflater.inflate(R.layout.fragment_single_rv, container, false)

        recyclerView = rootView.findViewById(R.id.recyclerView) // I did this to test if Kotlin synthetic binding was the problem.

        return rootView
    }

    // ...

    override fun onAtendimentosLoaded(atendimentos: List<UsuarioAtendimento>) {
        recyclerView.visibility = View.VISIBLE // App is crashing here.
        recyclerView.adapter = ClienteAtendimentoAdapter(atendimentos, this)
    }

    // ...
}

То, что я пробовал до сих пор

Как предложено для этого ответа Я пытался сохранить фрагмент, ноat не работает.

Я также пытался отказаться от синтетического связывания Kotlin и сделать переменную recyclerView lateinit var, которая надувается внутри метода onCreateView(), как показано ниже:

private lateinit var recyclerView: RecyclerView

//...

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                          savedInstanceState: Bundle?): View? {
    val rootView = inflater.inflate(R.layout.fragment_single_rv, container, false)

    recyclerView = rootView.findViewById(R.id.recyclerView)

    return rootView
}

Но теперь я получаю следующую ошибку:

10-25 18:32:42.261 10572-10572/com.sample.app E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.sample.app, PID: 10572
    io.reactivex.exceptions.UndeliverableException: kotlin.UninitializedPropertyAccessException: lateinit property recyclerView has not been initialized
        at io.reactivex.plugins.RxJavaPlugins.onError(RxJavaPlugins.java:367)
        at io.reactivex.internal.observers.ConsumerSingleObserver.onSuccess(ConsumerSingleObserver.java:66)
        at io.reactivex.internal.operators.single.SingleDoAfterTerminate$DoAfterTerminateObserver.onSuccess(SingleDoAfterTerminate.java:71)
        at io.reactivex.internal.operators.single.SingleDoOnSubscribe$DoOnSubscribeSingleObserver.onSuccess(SingleDoOnSubscribe.java:77)
        at io.reactivex.internal.operators.single.SingleObserveOn$ObserveOnSingleObserver.run(SingleObserveOn.java:81)
        at io.reactivex.android.schedulers.HandlerScheduler$ScheduledRunnable.run(HandlerScheduler.java:119)
        at android.os.Handler.handleCallback(Handler.java:739)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:145)
        at android.app.ActivityThread.main(ActivityThread.java:6939)
        at java.lang.reflect.Method.invoke(Native Method)
        at java.lang.reflect.Method.invoke(Method.java:372)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1404)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1199)
     Caused by: kotlin.UninitializedPropertyAccessException: lateinit property recyclerView has not been initialized
        at com.sample.app.features.atendimento.ClientesAtendimentosFragment.onAtendimentosLoaded(ClientesAtendimentosFragment.kt:59)

РЕДАКТИРОВАТЬ: Я сделал то, что Xite предложил в своем ответе , и я нашелчто по какой-то причине, когда я поворачиваю экран, новый вызов Rx все еще пытается использовать старую ссылку Fragment, даже если новые были успешно созданы и уже отображаются.Я не уверен, где я должен это убрать, или почему он так себя ведет, возможно, это как-то связано с supportFragmentManager?

Ответы [ 4 ]

0 голосов
/ 26 октября 2018

После некоторого поиска и помощи некоторых ответов здесь я нашел этот пост на Medium , который привел меня к решению.

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

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

inner class SectionsPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {
    private val fragList = ArrayList<Fragment>()

    fun addFragment(frag: Fragment) {
        fragList.add(frag)
    }

    override fun getItem(position: Int): Fragment {
        return fragList[position]
    }

    override fun getCount(): Int = fragList.size
}

А потом мне пришлось переопределить метод instantiateItem(container: ViewGroup, position: Int) внутри адаптера и сделать его следующим образом:

override fun instantiateItem(container: ViewGroup, position: Int): Any {
    val ret = super.instantiateItem(container, position)
    fragList[position] = ret as Fragment
    return ret
}

На то, почему это происходит, этот ответ на SO объясняетэто довольно хорошо.

0 голосов
/ 26 октября 2018

Я часто сталкиваюсь с этой проблемой.Я бы просто использовал нулевую функцию безопасности Kotlin для решения этой проблемы каждый раз.

override fun onAtendimentosLoaded(atendimentos: List<UsuarioAtendimento>) {
    recyclerView?.visibility = View.VISIBLE
    recyclerView?.adapter = ClienteAtendimentoAdapter(atendimentos, this)
}

Обратите внимание на знак вопроса после recyclerView

0 голосов
/ 26 октября 2018

Я думаю, что проблема в вашем SectionsPagerAdapter.getItem, вы всегда должны создавать новый фрагмент:

override fun getItem(position: Int): Fragment {
    return if (position == 0) ClientesAtendimentosFragment.newInstance() else ClientesAtendimentosFragment.newInstance()
}

Я знаю, что документация для FragmentPagerAdapter неясна, но если вы посмотрите на ее instantiateItem method:

@Override
public Object instantiateItem(ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    final long itemId = getItemId(position);

    // 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); // @Mauker it requires a new fragment instance!
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;
}

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

И вам не следует вызывать getItem впоследствии он должен использоваться только адаптером, он будет продолжать возвращать новый экземпляр фрагмента только при вызове.

Если вам действительно нужно получить ссылку, попробуйте что-то вроде:

val fragments = SparseArray<Fragment>()

fun getFragment(position: Int): Fragment? { // call this instead
    return fragments.get(position)
}

override fun getItem(position: Int): Fragment {
    val fragment = SomeFragment()
    fragments.put(position, fragment) // store it for later use
    return fragment
}

Я надеюсь, что это работает, удачи!

0 голосов
/ 26 октября 2018

Прежде всего, попробуйте отладить его и найдите хеш-код фрагмента (this) внутри onAtendimentosLoaded() до вращения и после вращения.Они должны отличаться, если вы не используете setRetainInstance.Если они одинаковы, вы не очищаете что-то должным образом, трудно сказать, что именно вызывает проблему, поскольку у нас нет соответствующего фрагмента, кода активности и кода докладчика.

...