During the implementation of the passing parameter solution, in navigation between modules, I came across a serialization error. Deeplinks, as far as I know, accepts custom argument types, which are Parcelables or Serializable.
Im using newest version of navigation 2.2.0
Error message:
java.lang.UnsupportedOperationException: Parcelables don't support default values.
Am I doing something wrong or this is still under development?
Here is short example:
<fragment
android:id="#+id/sampleFragment"
android:name="com.testapp.app.samples.navigation.SampleFragment"
android:label="SampleFragment">
<argument
android:name="Args"
app:argType="com.testapp.navigation.SampleArgs" />
<deepLink app:uri="app://app/samples/navigation/SampleFragment?Args={Args}"/>
</fragment>
#Parcelize
#Keep data class SampleArgs(
val text: String
) : NavArgs, Parcelable
val x = SampleArgs("TEST")
val uri = Uri.parse("app://app/samples/navigation/SampleFragment?Args=$x")
navController.navigate(uri)
I found something similar here Android Parcelable don't support default values App Crash
It's my first post on stack, so please be gentle :)
EDIT
Here is answer:
https://issuetracker.google.com/issues/148523779
Parcelables currently don't support default values so you need to pass your object as String value. Yes it is a work around.. So instead of passing object itself as Parcelize object we can turn that object into JSON (String) and pass it through navigation and then parse that JSON back to Object at destination. You can use GSON for object to json-string conversion.
Related
Is it possible to use a custom Serializable object as a fragment argument when using deep links?
I've tried the following - in my navigation graph XML file I've added following lines:
<fragment
android:id="#+id/eventFragment"
android:name="com.myapp.EventFragment"
android:label="EventFragment">
<argument
android:name="event"
app:argType="com.myapp.EventId" />
<deepLink app:uri="myapp://event/{event}" />
</fragment>
Where EventId is a serializable data class:
data class EventId(val value: Long) : Serializable
Then, when I'm trying to run my application with an URL myapp://event/4002, the following exception is thrown:
Caused by: java.lang.UnsupportedOperationException: Serializables don't support default values.
at androidx.navigation.NavType$SerializableType.parseValue(NavType.java:834)
at androidx.navigation.NavType$SerializableType.parseValue(NavType.java:787)
at androidx.navigation.NavType.parseAndPut(NavType.java:96)
at androidx.navigation.NavDeepLink.getMatchingArguments(NavDeepLink.java:99)
at androidx.navigation.NavDestination.matchDeepLink(NavDestination.java:366)
at androidx.navigation.NavGraph.matchDeepLink(NavGraph.java:79)
at androidx.navigation.NavController.handleDeepLink(NavController.java:540)
at androidx.navigation.NavController.onGraphCreated(NavController.java:499)
at androidx.navigation.NavController.setGraph(NavController.java:460)
at androidx.navigation.NavController.setGraph(NavController.java:425)
at androidx.navigation.NavController.setGraph(NavController.java:407)
at androidx.navigation.fragment.NavHostFragment.onCreate(NavHostFragment.java:236)
Replacing type with long (app:argType="long") solves the issue - there is no exception and everything works as expected.
It seems like the navigation library does not know how to convert the raw value from the URL to my EventId class. Is it possible to somehow register an adapter which knows how to convert it? Or maybe there is another solution?
As far as I am aware, you only have one other alternative to what you have done: Parcelable.
I think it will be sufficient in this case, but I am not sure, but even if it doesn't, this will be a good recommendation for any future Android development when you for example need to send data between Activities or fragments.
Especially as I can see you are using Kotlin, which has a helper method for implementing it correctly, see: Parcelize.
With it, your data class would simply look like this:
#Parcelize
data class EventId(val value: Long) : Parcelable
Side note:
Usage of the Serializable interface is discouraged for various reasons. From the book Effective Java written by Joshua Bloch who worked on Java for a long time:
Item 85: Prefer alternatives to Java serialization
If you still have to use Serializable, the book will cover the necessary details.
I am working on Retrofit in Kotlin.
Now, I want to create only one function ( API ) which accepts different pojos as the parameter.
I have created a function with "Any" as parameter.
Now If I try to pass one Model, it gives me compile time error of MyModel cannot convert to Any.
Any suggestions?
Change the parameter type to
Any?
This allows possible null values to be passed. More information about Any vs Any? and interaction with java is shown in this post: kotlin any or kotlin any?
The documentation discusses how to send simple integers and strings. For example:
<argument
android:name="myIntArg"
android:defaultValue="255"
app:argType="integer" />
In the origin Fragment:
val action = OriginFragmentDirections.myAction(myInt)
findNavController().navigate(action)
In the destination Fragment:
val receivedInt = DestinationFragmentArgs.fromBundle(arguments).myIntArg
But say instead of myIntArg, I wanted to send an enum (myEnumArg). How would I do that? What app:argType would I use in my argument?
Edit: As per the Navigation 1.0.0-alpha08 release notes:
Safe Args supports Serializable objects, including Enum values. Enum types can set a default value by using the enum literal without the class name (e.g. app:defaultValue="READ") b/111316353
So this is now possible - you would use the name of your Enum class (i.e., com.example.EnumClass) or a relative name (.EnumClass) which will automatically prepend your app's package name to the class name.
Previous answer:
This is not possible with the current version of Navigation (1.0.0-alpha07), but the existing feature request is marked as fixed and the ability to use enums as arguments will be available in alpha08
As #ianhanniballake mentioned in his updated answer, you need to use the latest version of Navigation.
Assume we have an Enum like below and your app package name is com.example.app.
package com.example.app.path.to.type.file
public enum class Type {
First,
Second,
Third
}
Now we just need to declare the Arg like this:
<fragment
...>
<argument
android:name="type"
android:defaultValue="Second"
app:argType=".path.to.type.file.Type" />
...
</fragment>
I am using the Safe Args plugin with the new Navigation components for my Android project. Now I have an argument that is an array list of parcelables, is there a way to use this with the Safe Args Plugin?
Something like app:argType=ParcelableArray. This should be possible since there are bundle methods like putParcelableArrayList().
Yes, since version 1.0.0-alpha08 you can now pass arrays of parcelable objects like this:
<argument
android:name="users"
app:argType="com.navigation.test.User[]"/>
For passing arrays of primitive types use for e.g. app:argType="integer[]"
Currently i don't think there is a simple way to use list of parcelables with safe args,
But i have found some "hack" to make this work.
For example, i have object 'User' and it parcelable, i am declaring a new parcelable object 'Users' that extending ArrayList().
#Parcelize
data class User(var name: String, val age: Int): Parcelable
#Parcelize
class Users: ArrayList<User>(), Parcelable
Now i can set 'Users' as argument in navigation
<argument
android:name="users"
app:argType="com.navigation.test.Users"/>
And passing array list of parcelables between destinations:
val user=User("Alex", 36)
val users= Users()
users.add(user)
val action=MainFragmentDirections.actionMainFragmentToSecondFragment(users)
NavHostFragment.findNavController(this#MainFragment).navigate(action)
And to retrieve them on other destination:
val users=SecondFragmentArgs.fromBundle(arguments).users
val user=users[0]
txtViewName.text=user.name
txtViewAge.text="${user.age}"
Update:
Support to list of objects coming in alpha8:
https://issuetracker.google.com/issues/111487504
Update 2:
The approach mentioned above will not work in case the activity is recreated, as #Parcelize will not be able to store/restore the list.
The object will be store in the state bundle, however, it will store an empty list of objects.
An improvement to #LaVepe suggestion: as for Android Studio 3.4.2 you can pass Parcelable array with in-built feature of navigation editor by specifying Arguments for chosen destination. Just check 'Array' option after choosing your custom Parcelable class which should not be wrapped in any collection beforehand:
EDIT
Here how it looks in xml:
<argument
android:name="items"
app:argType="com.company.domain.Item[]" />
In your Fragment/Activity code you might strictly pass a typed array of model Parcelable items. If you have non-array collection and write in Kotlin, it may be achieved with (if you have list beforehand) list.toTypedArray().
I have converted the following Swift code:
struct FooModel: Decodable {
public let id: String
public let bars: [[BarModel]]
}
to this Kotlin code:
data class FooModel (val id: String, val bars: List<List<BarModel>>)
The issue I am encountering, is my id is coming in null for the Kotlin code (via gson). Everything else in the Kotlin conversion is working fine and the entire JSON is populating all data classes, except for this tiny piece (the id variable).
I suspect my conversion here is the cause, any ideas?
If the id should be nullable do it like this:
data class FooModel (
val id: String?,
val bars: List<List<BarModel>>
)
The question mark makes this property nullable.
If the JSON you are getting is correct (the id value is there and coming to you as a string), your code should work. It's unclear what could be going wrong here if that's the case.
However, it is worth knowing that there is a big potential "gotcha" with Gson that you should be aware of: it's possible to declare a variable of a data class as non-nullable but still get a null after conversion. This can happen when an expected value is missing from the JSON response. In these cases Gson does not throw an error and I only found out later when I got a crash trying to access the non-nullable variable that should never have made it to me as null. I discovered this is a consequence of Gson using something like Class.newInstance() instead of a regular constructor when it creates these data classes, and then uses reflection to populate the data. More is written about this in another answer here: Why Kotlin data classes can have nulls in non-nullable fields with Gson?
Depending on your use case you might consider this to be a design flaw and a reason to avoid Gson in favor of other JSON serialization libraries. My personal favorite at the moment is Square's Moshi.
You can check if the value type you are getting from server matches with your variable id i.e. String on both the sides. Secondly you can try using SerializedName("id") included in library:
implementation 'com.google.code.gson:gson:2.9.0'