Search code examples
androidkotlinandroid-fragmentsandroid-contextillegalstateexception

Android Kotlin - requireContext() throws IllegalStateException inside Android Fragment's onViewCreated


I am dealing with IllegalStateException crashes pertaining to the usage of getString in onViewCreated; looking at the stack trace this goes back through getResources into requireContext, which throws IllegalStateException if context is null.

I could probably resolve this by using getContext() and checking for nullability but this leads me to ask if there are certain scenarios where context is null in the onViewCreated step of a Fragment's lifecycle.

My stack trace is also showing these crashes happening inside my CoroutineScopes defined inside onViewCreated.

I would appreciate any feedback on this subject if available, as looking through the code, I'm calling getString function properly without any strange placements such as after navigation. Below is the stack trace. I'm running this app on an Android 13 device. Thanks.

Exception java.lang.IllegalStateException:
  at androidx.fragment.app.Fragment.requireContext (Fragment.java)
  at androidx.fragment.app.Fragment.getResources (Fragment.java)
  at androidx.fragment.app.Fragment.getString (Fragment.java:1053)
  at com.example.MyFragment$onViewCreated$1$2.invokeSuspend (MyFragment.kt) //this leads me to think inside Coroutines is where its crashing.
  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt)
  at kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt)
  at android.os.Handler.handleCallback (Handler.java:942)
  at android.os.Handler.dispatchMessage (Handler.java:99)
  at android.os.Looper.loopOnce (Looper.java:226)
  at android.os.Looper.loop (Looper.java:313)
  at android.app.ActivityThread.main (ActivityThread.java:8757)
  at java.lang.reflect.Method.invoke
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:604)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1067)

Solution

  • The stack trace is not pointing to onViewCreated. Notice the function name in the trace is actually onViewCreated$1$2. This is a virtual function name created for a coroutine you launched from within onViewCreated. So the crash is occurring some time after onViewCreated, and presumably after onDetach so the context is null at that point.

    It's an anti-pattern to create a CoroutineScope and immediately launch a coroutine from it without first storing the CoroutineScope in a property so you can cancel it when the stuff it works with goes out of scope. You're creating a global coroutine, and might as well use GlobalScope. CoroutineScope().launch is a code smell because it's a global coroutine not launched in GlobalScope, which suggests the author isn't sure of what they're doing.

    There are occasional uses for global coroutines, but in the vast majority of cases, you don't want your coroutine to be global. A coroutine usually works with stuff for more than a few milliseconds, so you want to be cancelling it if the work it's doing is becoming obsolete to avoid wasting resources and memory for longer than necessary. Even more importantly, there are cases like yours, where you are trying to get a Context when its no longer available so you trigger a crash.

    Android Fragments already provides viewLifecycleOwner.lifecycleScope that you can launch coroutines from. It will automatically cancel all the coroutines it has launched when the Fragment view is destroyed, which protects you from holding onto obsolete view references for longer than necessary or trying to get a context when it is no longer available.

    It should also be rare to launch a coroutine using Dispatchers.IO, especially on Android, where so many classes demand that they only be touched on the main thread. Dispatchers.Main should be your default, and it already is the default for lifecycleScope. Dispatchers.IO is for calling blocking code. You generally should only be using it with withContext(Dispatchers.IO) { } to wrap bits of blocking code inside your coroutines.

    Also note that suspend functions are not blocking if they are following convention. They internally use whichever threads/dispatchers they need so they never need to be wrapped in withContext when called.