2

The problem

I'm developing an Android app using Kotlin synthetic binding, RxJava, Retrofit and the MVP pattern. On this app there's a screen that's supposed to fetch some data from an API and populate a RecyclerView with the returned data.

All of that is working every single time, until I try rotate the screen... Once the Activity and Fragment are recreated and try to access the RecyclerView the app crashes with the following exception:

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)

This happens when I try to call this method:

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

Since, for some reason, the recyclerView variable is null, the app crashes, even though the Activity and Fragment are both successfully created and are already visible on the phone (I can even see the loading screen before the crash).

So why is my view getting null, even after the Activity and Fragment are already visible?

EDIT: I'm adding the MVP files as well to help understand what's happening here.

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

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

    // ...

}

Fragment

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

    // ...
}

What I've tried so far

As suggested on this answer, I tried to make the fragment retain the instance, but that didn't work.

I've also tried to ditch the Kotlin synthetic binding and make the recyclerView variable a lateinit var that's inflated inside the onCreateView() method, like the snippet below:

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
}

But now I'm getting the following error:

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)

EDIT: I did what Xite suggested on his answer, and I found out that for some reason when I'm rotating the screen, the new Rx call is still trying to use the old Fragment reference, even though new ones have been successfully created and are already displayed. I'm not sure where am I supposed to clean that up, or why is it behaving that way, perhaps it has something to do with the supportFragmentManager?

Mauker
  • 11,237
  • 7
  • 58
  • 76
  • Have you tried overriding the rotation and screenSize config changes in your Activity's Manifest tag? – TheWanderer Oct 25 '18 at 21:02
  • I wonder how does your presenter work in your MVP: does your presenter has a reference to the view too? If so, your presenter could be pointing to an old view which was destroyed in rotation. Could you share your presenter and fragment attachment? – Aaron Oct 25 '18 at 21:08
  • The presenter is not holding an old reference, I went as far as nulling the View reference, and reattaching it after the `onStart()` method on the Activity – Mauker Oct 25 '18 at 21:10
  • @Mauker Thanks for sharing the code, I'm sure it has something to do with your `SectionsPagerAdapter`, and I've posted an answer. I hope it works! – Aaron Oct 26 '18 at 13:12

4 Answers4

2

First of all, try to debug it and look for hashcode of fragment (this) inside onAtendimentosLoaded() before rotation and after rotation. They should be different, assuming you are not using setRetainInstance. If they are the same, you are not clearing something properly, hard to tell what exactly causes the issue, as we do not have relevant fragment, activity and presenter code.

Xite
  • 78
  • 5
1

I think the problem is in your SectionsPagerAdapter.getItem, you should always instantiate a new fragment:

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

I know the documentation for FragmentPagerAdapter is unclear, but if you look at its 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;
}

It is clear that we need to provide a new fragment instance whenever SectionsPagerAdapter.getItem is called. So in your case, when your screen rotates, your adapter (assume the adapter is persisted) will return old reference of the fragments, which their views may have already been destroyed.

And you shouldn't call getItem afterwards, it should only be used by the adapter, it will only keep returning new fragment instance when you call it.

If you really need to get the reference, then try something like:

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
}

I hope it works, good luck!

Aaron
  • 3,764
  • 2
  • 8
  • 25
  • Ok, now it's crashing even on the first run :( – Mauker Oct 26 '18 at 13:15
  • Different crash I suppose? >_ – Aaron Oct 26 '18 at 13:20
  • Nope, same problem, the RV is null – Mauker Oct 26 '18 at 13:21
  • Oh I missed a point, you shouldn't call getItem afterwards, it is only used by the adapter, and it will keep returning new fragment. The cheapest way to make it work from here is to provide a new function similar to getItem. – Aaron Oct 26 '18 at 13:27
  • Ohh, indeed. I'll check that out. – Mauker Oct 26 '18 at 13:29
  • Ok, but then how am I going to use the callback to pass the data to the fragments? – Mauker Oct 26 '18 at 13:32
  • I've updated my answer, it's not a good practice but it should get you somewhere. Perhaps you can move the data logic to the fragments instead, just a thought! – Aaron Oct 26 '18 at 13:38
  • That's just a sample code.. sorry for being lazy, but try to provide your own way to store the fragments when getItem is called, maybe a SparseArray will help. Also getFragment should return nullable. – Aaron Oct 26 '18 at 13:52
  • Found a solution here: https://medium.com/@roideuniverse/android-viewpager-fragmentpageradapter-and-orientation-changes-256c23bee035 – Mauker Oct 26 '18 at 13:55
  • I've updated the answer again, but feel free to use other solution too :) – Aaron Oct 26 '18 at 13:57
0

I often face this issue. I would simply use the null safety feature of Kotlin to solve this problem every time.

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

Notice the question mark after recyclerView

Yaswant Narayan
  • 1,297
  • 1
  • 15
  • 20
0

After some digging, and the help of some of the answers here, I found this post on Medium which led me to the solution.

So yes, the problem was on the FragmentPagerAdapter, which was returning an old fragment instance, which lead to the null pointer.

To solve this issue, first I changed how I was creating the Fragments to this:

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
}

And then, I had to override the instantiateItem(container: ViewGroup, position: Int) method inside the adapter, and made it like this:

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

As of why is this happening, this answer on SO explains it quite well.

Mauker
  • 11,237
  • 7
  • 58
  • 76