Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jetpack navigation component throws an IllegalStateException when loading a start destination with nullable argument

I added a nullable argument to my start destination:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/nav_graph"
        app:startDestination="@id/startDest">

    <fragment android:id="@+id/startDest"
          android:name="com.myapp.MyStartFragment"
          android:label="Start"
          tools:layout="@layout/fragment_start">
        <argument
            android:name="dataObject"
            app:argType="com.myapp.MyDataObject"
            android:defaultValue="@null"
            app:nullable="true"/>
        ...
    </fragment>
    ...
</navigation>

But when I load my app, I get the following exception:

java.lang.IllegalStateException: Fragment MyStartFragment{a4ffd1f (ca52d4dc-ff36-4a93-8ebf-f11af7b7d5aa) id=0x7f080145} has null arguments
    at com.myapp.MyStartFragment$$special$$inlined$navArgs$1.invoke(FragmentNavArgsLazy.kt:42)
    at com.myapp.MyStartFragment$$special$$inlined$navArgs$1.invoke(Unknown Source:0)
    at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:44)
    at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:34)
    at com.myapp.MyStartFragment.getArgs(Unknown Source:27)
    at com.myapp.MyStartFragment.onAttach(MyStartFragment.kt:85)

And the exception is triggered by this piece of code in MyStartFragment:

private val args: MyStartFragmentArgs by navArgs()
override fun onAttach(context: Context) {
    super.onAttach(context)
    val title = if(this.args.dataObject == null) getString(R.string.start_list_title) else this.args.dataObject!!.name
    ...
}

And here is the code for MyDataObject:

@Parcelize
data class MyDataObject (
    val id: String,
    val name: String,
    val externalIdentifier: String
    val type: MyDataEnumType,
    var responsibleUser: SomeOtherParcelableClass?
): Parcelable 

What I don't understand is that my start destination doesn't get passed arguments properly by the navigation controller. Am I missing something here?

like image 878
Sebastien Avatar asked Aug 19 '19 15:08

Sebastien


2 Answers

Hello I am assuming you want to achieve something like below

BeforeFragment --arg--> StartFragment --> AfterFragment

sample nav graph

This flows are similar first time user, returning user flows. Here BeforeFragment is last fragment in login_nav_graph nested graph. StartFragment is the starting destination of main_nav_graph. StartFragment is the first screen returning user sees.

So in BeforeFragment you may set args as follows

val userJohn:User = User(34, "John", 645, UserType.TYPE2, Guardian("Mike"))
val action = BeforeFragmentDirections.actionGlobalStart(userJohn)
findNavController().navigate(action)

and in StartFragment you may do following as you already did

title = if(this.args.user == null)
   getString(R.string.user_name) // mocks loading saved user name
else
   this.args.user?.name // when user is first time user read from passed args

Sample Repo can be found here


My best guess

This issue is due to bug in older navigation version, so use 2.2.0-alpha01 which is I am using in the sample repo.

In order to fix errors that occurs when moved to new navigation version in you module gradle file add following

android {
    ...
    kotlinOptions {
        jvmTarget = "1.8" // set your Java version here
    }
}

This fixes the error

Cannot inline byte code ...


Keep in mind passing complex objects as arguments is not recommended. Quoting from docs.

In general, you should strongly prefer passing only the minimal amount of data between destinations. For example, you should pass a key to retrieve an object rather than passing the object itself, as the total space for all saved states is limited on Android. If you need to pass large amounts of data, consider using a ViewModel as described in Share data between fragments.

If this fixes your issue please confirm the answer, since I spent lot of time preparing this post.

like image 92
user158 Avatar answered Sep 20 '22 02:09

user158


If you create a fragment with a bundle, but without NavController.navigate, and you still want to keep "by navArgs()" in destination fragment e.g because you want to use it in launchFragmentInContainer when testing, then use try/catch. The IllegalStateException is thrown before you start checking nullable type.

private fun updateArguments(){
    this.title = try{
        val args: DestinationFragmentArgs by navArgs()

        if (args.dataObject?.name == ""){
            // default value provided so check whether bundle was used
            arguments?.getString("name") ?: ""
        }else {
            args?.venueId
        }
    }catch (ex: Exception){
        // fragment created without using NavController.navigate
        arguments?.getString("venueId") ?: ""
    }
}

Room is very fast, so retrieving an object from the id as an argument should not be an issue, so try using primitive types for your arguments if possible, otherwise use Parcelable or Serialize for inexpensive objects.

like image 37
Pitos Avatar answered Sep 21 '22 02:09

Pitos