How to ignore fields when using #Parcelize annotation in Kotlin - android

I want to ignore a field when using the #Parcelize annotation in Kotlin so that the field is not parceled, since this field does not implement the Parcelable interface.
Starting with this, we get an error because PagedList is not parcelable:
#Parcelize
data class LeaderboardState(
val progressShown: Boolean = true,
val pagedList: PagedList<QUser>? = null
) : Parcelable
Gives:
Type is not directly supported by 'Parcelize'. Annotate the parameter type with '#RawValue' if you want it to be serialized using 'writeValue()'
Marking as #Transient gives the same error as above:
#Parcelize
data class LeaderboardState(
val progressShown: Boolean = true,
//Same error
#Transient
val pagedList: PagedList<QUser>? = null
) : Parcelable
There is an undocumented annotation I found called #IgnoredOnParcel which gives the same error, and a lint error on the annotation:
#Parcelize
data class LeaderboardState(
val progressShown: Boolean = true,
//Same error plus lint error on annotation
#IgnoredOnParcel
val pagedList: PagedList<QUser>? = null
) : Parcelable
The lint error in that case is:
#IgnoredOnParcel' is inapplicable to properties declared in the primary constructor
Is there really no way to do this with #Parcelize?

Use a regular class and move the property out of the primary constructor:
#Parcelize
class LeaderboardState(
val progressShown: Boolean = true,
pagedList: PagedList<QUser>? = null
) : Parcelable {
#IgnoredOnParcel
val pagedList: PagedList<QUser>? = pagedList
}
This is apparently the only solution. Make sure to override equals, hashCode, toString, copy, etc as you need them because they won't be defined for a regular class.
EDIT: Here's another solution so you don't lose the features of the data class and you don't lose the automatic parcelization. I'm using a general example here.
data class Person(
val info: PersonInfo
val items: PagedList<Item>? = null)
#Parcelize
data class PersonInfo(
val firstName: String,
val lastName: String,
val age: Int
) : Parcelable
You save only Person.info and recreate it from that.

Related

Serialize class with abstract parameter to JSON

I have 3 classes:
GameResultList which is basically ArrayList with some helper methods in it
GameResult with an abstract value gameMode
GameMode
public class GameResultList extends ArrayList<GameResult> {
...
}
class GameResult(
val gameMode: GameMode,
val score: Int,
timeSpentInSeconds: Int,
val completionDateTime: Date
) {
...
}
GameMode class:
abstract class GameMode(
val suggestionsActivated: Boolean,
val screenOrientation: ScreenOrientation // enum: PORTRAIT, HORIZONTAL
) {
...
}
I need to serialize GameResultList into JSON.
Since the parameter gameMode is abstract, Gson throws an exception. After some research, I decided to give Moshi a try. I have added PolymorphicJsonAdapterFactory and KotlinJsonAdapterFactory, but the result is always empty ({}).
How I set up Moshi:
private val moshi =
Moshi.Builder().add(PolymorphicJsonAdapterFactory.of(GameMode::class.java, "GameMode")
.withSubtype(GameOnTime::class.java, "GameOnTime")
.withSubtype(GameOnCount::class.java, "GameOnCount"))
.add(KotlinJsonAdapterFactory())
.build()
private val jsonAdapter: JsonAdapter<GameResultList> = moshi.adapter(GameResultList::class.java)
This returns empty JSON response:
jsonAdapter.toJson(gameResultList)
So how can I serialize the GameResultList? Is there an easy way? Also, it's not necessary to use Moshi, it can be anything else for the sake of easiness.
After some investigation, I found out that the main problem is that array lists require explicit converters.
class GameResultListToJsonAdapter {
#ToJson
fun arrayListToJson(list: GameResultList): List<GameResult> = list
#FromJson
fun arrayListFromJson(list: List<GameResult>): GameResultList = GameResultList(list)
}
Also, there is a problem with handling the Date type, I have replaced it with Long to not make another explicit converter.

Moshi adapter for annotated model

I'm currently using a Moshi adapter to convert some json raw to a given type. It works fine until I used an annotated model.
I'm guessing I should add another params to my adapter beside Player::class.java but I don't know what.
Here is an exemple:
data class Player(
val username: String,
#Json(name = "lucky number")
val luckyNumber: Int
)
private val playerStubRaw = "{\n" +
" \"username\": \"jesse\",\n" +
" \"lucky number\": 32\n" +
"}"
#Test
fun doSomething() {
val moshi = Moshi.Builder().build()
val player = moshi.adapter(Player::class.java).fromJson(playerStubRaw)
// player.luckyNumber == 0
}
luckyNumber value is 0 and not 32.
Any idea what I should do to make it work?
Thanks in advance,
To work with Kotlin, Moshi requires either the reflective KotlinJsonAdapterFactory (from the moshi-kotlin artifact) or code-gen adapters (from the moshi-kotlin-codegen artifact).
https://github.com/square/moshi#kotlinIn a future release of Moshi, a proper error will be thrown to state this requirement.
With the moshi-kotlin-codegen artifact you also need to add #JsonClass(generateAdapter = true) on the class for the decoding to work correctly and not set the property to a default of 0
So after adding the kotlin-kapt plugin and the dependency kapt "com.squareup.moshi:moshi-kotlin-codegen:1.8.0" to the app build gradle annotate the class as below:
#JsonClass(generateAdapter = true)
data class Player(
val username: String,
#Json(name = "lucky number")
val luckyNumber: Int
)

Can the parameter in data class be var in Kotlin?

I'm a beginner of Kotlin, I have read some sample code about data class, it seems that the parameter are all val type just like Code A
I need to change some values of data class MSetting, so I design the Code B, could you tell me whether the Code B is good way?
Code A
data class MSetting (
val _id: Long,
val name: String,
val createdDate: Long,
val description: String
)
Code B
data class MSetting (
var _id: Long,
var name: String,
var createdDate: Long,
var description: String
)
it seems that the parameter are all val type...
NO
could you tell me whether the Code B is good way?
The difference between val and var: Properties declared with val can't be updated over time; its just like constants in java. Properties declared with var can be changed overtime.
It totally depends on your requirement. If you need to change properties over time then go for var; val otherwise. You can mix both in a object without any issue.
Read more about properties in Kotlin documentation here https://kotlinlang.org/docs/reference/properties.html
I like to use val as a rule of thumb because it makes data class immutable. Of course I am using var but only if it is necessary. Why it is better to make data class immutable? read this.
The accepted answer is misleading. Yes, you can use var arg in data class constructor. But you should avoid it at all costs because it defeats the purpose of data class.
Consider this example:
data class Text(
val x: String,
var y: Int,
)
fun main() {
val t = Text("4124", 1)
val a = t.hashCode()
t.y = 125
val b = t.hashCode()
println(a)
println(b)
}
The output will be something like:
49532514
49532638
This means, object t cannot be used in Map/Set, because it's hashCode isn't constant which will lead to ghost-objects in these collections.
The proper way to have modifiable field in data class is:
data class Text(
val x: String,
) {
var y: Int
}
This way y isn't included in generated equals, hashCode, toString and problem with Map/Set is avoided.
See kotlin doc for this: https://kotlinlang.org/docs/data-classes.html#properties-declared-in-the-class-body

Kotlin Gson Deserializing

I am receiving a JSON data model that has a map wrapper Table. I'm trying to use generics to pass in the type that is beyond the wrapper but it's not translating well at runtime. Here's an example of my JSON file:
{
"Table": [
{
"paymentmethod_id": 1,
"paymentmethod_description": "Cash",
"paymentmethod_code": "Cash",
"paymentmethod_is_ach_onfile": false,
"paymentmethod_is_element": false,
"paymentmethod_is_reward": false,
"paymentmethod_is_openedgeswipe": false,
"paymentmethod_update_user_id": 1,
"paymentmethod_insert_user_id": 1,
"paymentmethod_insertdate": "2014-10-07 14:53:16",
"paymentmethod_deleted": false,
"paymentmethod_is_mobile_visible": true
}
]
}
The wrapper class I'm using is called Table.
data class Table<T>(
#SerializedName("Table") val models : Array<T>
)
The actual model class is PaymentMethod.
data class PaymentMethod(
#SerializedName("paymentmethod_id") val idNumber : Int = -1
)
I have created a generic data manager class that takes < T > type. I think use subclasses of the data manager to localize the input and results (such as declaring the model class PaymentMethod.
open class NXDataManager<T>(manager: NXNetworkManager? = null, rpc : String?, parameters: List<Pair<String, String>>? = null, method : String = "get")
{
...
open fun sendRequest(completionHandler: (models:Array<T>) -> Unit, errorHandler: (error:FuelError) -> Unit) {
val request = NXNetworkRequest(rpc, parameters, method)
request.send(manager, completionHandler = { s: String ->
val table: Table<T> = Gson().fromJson(s)
completionHandler(table.models)
}, errorHandler = errorHandler)
}
inline fun <reified T> Gson.fromJson(json: String) = this.fromJson<T>(json, object: TypeToken<T>() {}.type)
}
My subclassed data manager specifies the model to parse into.
final public class PaymentMethodsDataManager : NXDataManager<PaymentMethod>
{
constructor () : super("genGetPaymentMethods")
}
When I run the code as:
val table: Table<T> = Gson().fromJson(s)
I get an error message java.lang.ClassCastException: java.lang.Object[] cannot be cast to Networking.PaymentMethod[]. However, when I pass in an explicit type it works as expected--parsing the array into PaymentMethod models:
val table: Table<PaymentMethod> = Gson().fromJson(s)
Any ideas of how I can still use the generic type T?
Data Class :
data class Table<T>(
#SerializedName("Table") val models : Array<T>
)
to JSON:
val gson = Gson()
val json = gson.toJson(table)
from JSON:
val json = getJson()
val table = gson.fromJson(json, Table::class.java)
Method fromJson is generic, so when you call it for Table<T> variable it creates Array<Any> as most suitable. You need to notice that PaymentMethod class extends T generic, but I don't know is it even possible. If you find out how to make it, use something like following:
val table: Table<T> = Gson().fromJson<Table<PaymentMethod>>(s)
In your case I'm using gson adapters. Following function creates object with specified type parameter:
fun getObjectFromString(type: Type, string: String) =
Gson().getAdapter(TypeToken.get(type)).fromJson(string)
To use it write something following:
val table: Table<T> = getObjectFromString(Table<PaymentMethod>::class.java, s) as Table<PaymentMethod>
Update
To avoid spare class cast you can use reified generic function:
inline fun <reified T> getObjectFromString(string: String): T =
getGsonConverter().getAdapter(TypeToken.get(T::class.java)).fromJson(string)!!
In that case using would be easier:
val table: Table<T> = getObjectFromString<Table<PaymentMethod>>(s)
I used first solution in cases where I don't know what type the object would be - I've got only Type variable with information about that object.
java.lang.ClassCastException: java.lang.Object[] cannot be cast to
Networking.PaymentMethod[]
Your JSON is
{
"Table": [
{
"paymentmethod_id": 1,
"paymentmethod_description": "Cash",
"paymentmethod_code": "Cash",
"paymentmethod_is_ach_onfile": false,
"paymentmethod_is_element": false,
"paymentmethod_is_reward": false,
"paymentmethod_is_openedgeswipe": false,
"paymentmethod_update_user_id": 1,
"paymentmethod_insert_user_id": 1,
"paymentmethod_insertdate": "2014-10-07 14:53:16",
"paymentmethod_deleted": false,
"paymentmethod_is_mobile_visible": true
}
]
}
Create a data class, PaymentMethod.
We frequently create classes whose main purpose is to hold data. In
such a class some standard functionality and utility functions are
often mechanically derivable from the data.
data class PaymentMethod(#SerializedName("Table") val table:ArrayList<PaymentData> )
data class PaymentData
(
#SerializedName("paymentmethod_id") val paymentmethod_id: Int,
#SerializedName("paymentmethod_description") val paymentmethod_description: String,
#SerializedName("paymentmethod_code") val paymentmethod_code:String,
#SerializedName("paymentmethod_is_ach_onfile") val paidStatus:Boolean,
#SerializedName("paymentmethod_is_element") val paymentmethod_is_element:Boolean,
#SerializedName("paymentmethod_is_reward") val paymentmethod_is_reward:Boolean,
#SerializedName("paymentmethod_is_openedgeswipe") val paymentmethod_is_openedgeswipe:Boolean,
#SerializedName("paymentmethod_update_user_id") val paymentmethod_update_user_id:Int,
#SerializedName("paymentmethod_insert_user_id") val paymentmethod_insert_user_id:Int,
#SerializedName("paymentmethod_insertdate") val paymentmethod_insertdate:String,
#SerializedName("paymentmethod_deleted") val paymentmethod_deleted:Boolean),
#SerializedName("paymentmethod_is_mobile_visible") val paymentmethod_is_mobile_visible:Boolean
)
You can call this way
val paymentDATA = Gson().fromJson<PaymentMethod>("JSON_RESPONSE", PaymentMethod::class.java)
val _adapterPaymentHistory = paymentDATA.table

Cannot find setter for field - using Kotlin with Room database

I'm integrating with the Room persistence library. I have a data class in Kotlin like:
#Entity(tableName = "story")
data class Story (
#PrimaryKey val id: Long,
val by: String,
val descendants: Int,
val score: Int,
val time: Long,
val title: String,
val type: String,
val url: String
)
The #Entity and #PrimaryKey annotations are for the Room library. When I try to build, it is failing with error:
Error:Cannot find setter for field.
Error:Execution failed for task ':app:compileDebugJavaWithJavac'.
> Compilation failed; see the compiler error output for details.
I also tried providing a default constructor:
#Entity(tableName = "story")
data class Story (
#PrimaryKey val id: Long,
val by: String,
val descendants: Int,
val score: Int,
val time: Long,
val title: String,
val type: String,
val url: String
) {
constructor() : this(0, "", 0, 0, 0, "", "", "")
}
But this doesn't work as well. A thing to note is that it works if I convert this Kotlin class into a Java class with getters and setters. Any help is appreciated!
Since your fields are marked with val, they are effectively final and don't have setter fields.
Try switching out the val with var.
You might also need to initialize the fields.
#Entity(tableName = "story")
data class Story (
#PrimaryKey var id: Long? = null,
var by: String = "",
var descendants: Int = 0,
var score: Int = 0,
var time: Long = 0L,
var title: String = "",
var type: String = "",
var url: String = ""
)
EDIT
The above solution is a general fix for this error in Kotlin when using Kotlin with other Java libraries like Hibernate where i've seen this as well. If you want to keep immutability with Room, see some of the other answers which may be more specific to your case.
In some cases immutability with Java libraries is simply not working at all and while making sad developer noises, you have to switch that val for a var unfortunately.
Hey I don't know if everyone know or not, but you can not have column which is starting from is into Room.
For example you can't have like this
#Entity(tableName = "user")
data class User (
#PrimaryKey var id: Long? = null,
var userName: String = "",
var isConnectedToFB: Boolean = false,
)
If you have #Ignore field in the data class constructor you need to move it to class body like this:
#Entity(primaryKeys = ["id"])
data class User(
#field:SerializedName("id")
val id: Int,
#field:SerializedName("name")
val name: String,
#field:SerializedName("age")
val age: Int
) {
#Ignore
val testme: String?
}
All kudos go to marianperca on GitHub: https://github.com/android/architecture-components-samples/issues/421#issuecomment-442763610
There is an issue in room db library java code generation.
I was using optional field isFavorite. It gives me same error then I change my field name to favorite then compiled.
before
var isFavorite: Int? = 0,
after changing working fine
var favorite: Int? = 0,
Thanks
According to https://stackoverflow.com/a/46753804/2914140 if you have an autogenerated primary key, you should write so:
#Entity(tableName = "story")
data class Story (
val by: String,
val descendants: Int,
val score: Int,
val time: Long,
val title: String,
val type: String,
val url: String
) {
#PrimaryKey(autoGenerate = true)
var id: Int = 0
}
Note that #PrimaryKey is written inside the class body and contains modifier var.
If you later want to update a row in a database with different parameters, use these lines:
val newStory = story.copy(by = "new author", title = "new title") // Cannot use "id" in object cloning
newStory.id = story.id
dao.update(newStory)
UPDATE
I still don't use AndroidX, and Room is 'android.arch.persistence.room:runtime:1.1.1'.
You can extend this class from Serializable. But if you want to extend it from Parcelable, you will get a warning (over id variable): Property would not be serialized inro a 'Parcel'. Add '#IgnoredOnParcel' annotation to remove this warning:
Then I moved an id from the body to the constructor. In Kotlin I use #Parcelize to create Parcelable classes:
#Parcelize
#Entity(tableName = "story")
data class Story (
#PrimaryKey(autoGenerate = true)
var id: Int = 0,
val by: String,
val descendants: Int,
val score: Int,
val time: Long,
val title: String,
val type: String,
val url: String
) : Parcelable
Had this error in Java.
You cannot have a column starting with is or is_ in Java.
Try renaming the column.
Another solution:
You either have to pass the field in the constructor and initialize it with the constructor argument, or create a setter for it.
Example:
public MyEntity(String name, ...) {
this.name = name;
...
}
public void setName(String name) {
this.name = name;
}
This error will be thrown if your column starts with Is:
#ColumnInfo(name = "IsHandicapLeague")
#NonNull
var isHandicapLeague: String = "Y"
Add a default set() function to eliminate
fun setIsHandicapLeague(flag:String) {
isHandicapLeague = flag
}
Just make the variables mutable, change val into var for Kotlin, Or private into public for Java
This is a bug and is fixed in Room 2.1.0-alpha01
https://developer.android.com/jetpack/docs/release-notes#october_8_2018
Bug Fixes
Room will now properly use Kotlin’s primary constructor in
data classes avoiding the need to declare the fields as vars.
b/105769985
I've found that another cause of this compilation error can be due to the use of the Room's #Ignore annotation on fields of your entity data class:
#Entity(tableName = "foo")
data class Foo(
// Okay
#PrimaryKey
val id: String,
// Okay
val bar: String,
// Annotation causes compilation error, all fields of data class report
// the "Cannot find setter for field" error when Ignore is present
#Ignore
val causeserror: String
)
The same error also seems to happens when using the #Transient annotation.
I've noticed this issue using version 2.2.2 of Room:
// build.gradle file
dependencies {
...
kapt "androidx.room:room-compiler:2.2.2"
...
}
Hope that helps someone!
You can try to rename id variable to another name. It worked for me ;
var id: Long? = null
to
var workerId: Long? = null
If you have to name as id and you are using retrofit, then you may need to add SerializedName("id")
Another cause of this may be the naming of the field. If you use any of the pre-defined keywords, you will get the same error.
For instance, you can not name your column "is_active".
Reference: http://www.sqlite.org/lang_keywords.html
It seems like Room and Kotlin versions need to be matched. I have same issue with Room 2.3.0 and Kotlin 1.6.10 but it's ok with Kotlin 1.5.20. It looks ok after I updated Room to 2.4.2.
https://youtrack.jetbrains.com/issue/KT-45883
Also there is a possible solution to use #JvmOverloads constructor for better Java compability.
Updating Room library to the latest version 2.4.2 solve the issue
The correct way to fix this issue would be simply updating to Room v2.4.3 or higher.
Workaround
If you're running on an older version of Room, one that uses an old version of the kotlinx-metadata-jvm library which doesn't understand 1.5.x metadata, a simple workaround would be adding the following line to your build.gradle:
kapt "org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.5.0"
Source: https://youtrack.jetbrains.com/issue/KT-45883/KAPT-Cannot-find-setter-for-field-compiling-projects-with-Room-db-breaks-using-150-M2
Just an update if somebody comes across this thread in 2019, after spending hours digging online on why this should work, but it doesn't.
Using val works as expected if you are using the AndroidX version ( androidx.room:room-<any>:2.*) but it doesn't when using the old android.arch.persistence.room:<any>:1.1.1 and it seems that version 2.* wasn't released on this latter repo.
Edit: typos
If you want the val immutability available for your entity, it is possible.
You should update to AndroidX room current version.
Check for the related issue here it is marked as Won't Fix
Now they have release a fix related to the issue with version 2.0.0-beta01
Now you can use immutable val with default value e.g:
#Entity("tbl_abc")
data class Abc(
#PrimaryKey
val id: Int = 0,
val isFavourite: Boolean = false
)
Previously, the above snippet will throw an error of Cannot find setter for field. Changing into var is a great workaround, but I prefer for the entity class to be immutable from outside invocation
You can now start your field with is but you can't have a number next to the is like : is2FooSelected, you have to rename to isTwoFooSelected.
I think that the variable we wrote as id is getting mixed up with the id in the system. Therefore, when I define it as uuid, my error is resolved. I think it will be solved too. Also, try using var instead of val.
#PrimaryKey(autoGenerate = true)
var uuid:Int=0
Just use var instead of val and if you are using private keyword, make it public.
#Entity(tableName = "story")
data class Story (
#PrimaryKey val id: Long,
var by: String,
var descendants: Int,
var score: Int,
var time: Long,
var title: String,
var type: String,
var url: String
)

Categories

Resources