【问题标题】:View instance null after screen rotation and rxjava屏幕旋转和 rxjava 后查看实例 null
【发布时间】:2018-10-25 20:35:10
【问题描述】:

问题

我正在使用 Kotlin 合成绑定、RxJava、Retrofit 和 MVP 模式开发一个 Android 应用。在这个应用程序上有一个屏幕,它应该从 API 获取一些数据并使用返回的数据填充 RecyclerView

所有这些每次都有效,直到我尝试旋转屏幕...一旦重新创建 Activity 和 Fragment 并尝试访问 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 变量为空,应用程序崩溃,即使 Activity 和 Fragment 都已成功创建并且已经在手机上可见(我什至可以看到崩溃前的加载屏幕)。

那么为什么我的视图变为空,即使在 Activity 和 Fragment 已经可见之后?

编辑:我也在添加 MVP 文件以帮助了解这里发生的情况。

演示者

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

    // ...
}

到目前为止我已经尝试过什么

按照this answer 的建议,我尝试让片段保留实例,但没有奏效。

我还尝试放弃 Kotlin 合成绑定,并将 recyclerView 变量设置为 lateinit var,它在 onCreateView() 方法中膨胀,如下面的 sn-p:

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 suggested on his answer 的操作,我发现由于某种原因,当我旋转屏幕时,新的 Rx 调用仍在尝试使用旧的 Fragment 引用,即使尽管新的已成功创建并已显示。我不确定我应该在哪里清理它,或者它为什么会这样,也许它与supportFragmentManager有关?

【问题讨论】:

  • 您是否尝试过覆盖 Activity 清单标签中的旋转和 screenSize 配置更改?
  • 我想知道您的演示者在您的 MVP 中是如何工作的:您的演示者是否也引用了视图?如果是这样,您的演示者可能会指向一个在轮换中被破坏的旧视图。你能分享你的演示者和片段附件吗?
  • 演示者没有持有旧引用,我什至将 View 引用清空,并在 Activity 上的 onStart() 方法之后重新附加它
  • @Mauker 感谢您分享代码,我确信它与您的SectionsPagerAdapter 有关,我已经发布了答案。我希望它有效!

标签: android android-recyclerview kotlin rx-java2


【解决方案1】:

首先,尝试调试它并在旋转前和旋转后的onAtendimentosLoaded() 中查找片段(this)的哈希码。它们应该是不同的,假设您没有使用setRetainInstance。如果它们相同,则说明您没有正确清除某些内容,很难说出问题的确切原因,因为我们没有相关的片段、活动和演示者代码。

【讨论】:

  • 它们确实不同。
  • 我已经编辑了问题并添加了MVP相关代码。
【解决方案2】:

我认为问题出在您的SectionsPagerAdapter.getItem,您应该始终实例化一个新片段:

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

我知道FragmentPagerAdapter 的文档不清楚,但如果您查看它的instantiateItem 方法:

@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
}

我希望它有效,祝你好运!

【讨论】:

  • 好的,现在它甚至在第一次运行时就崩溃了:(
  • 我想是不同的崩溃? >_
  • 不,同样的问题,RV 为空
  • 哦,我错过了一点,你不应该在之后调用getItem,它只是由适配器使用,它会不断返回新的片段。让它从这里开始工作的最便宜的方法是提供一个类似于 getItem 的新函数。
  • 哦,确实。我会检查一下。
【解决方案3】:

我经常遇到这个问题。我每次都会简单地使用 Kotlin 的 null 安全特性来解决这个问题。

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

注意recyclerView后面的问号

【讨论】:

  • 不幸的是,这只会掩盖问题。我需要执行那部分代码。
  • 你试过了吗?因为我会在我的情况下做类似的事情并且它会起作用
  • 是的,我做到了,代码最终没有被执行,就像我说的那样,它只是掩盖了问题,问题出在其他地方
【解决方案4】:

经过一番挖掘,以及这里的一些答案的帮助,我找到了this post on 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
}

至于为什么会这样,this answer on SO 解释得很好。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多