Can generics be used with android room Entities? - android

Consider the following Entity:
#Entity(tableName = "media")
data class Media(
#PrimaryKey(autoGenerate = true) val id: Long = 0,
// Stored as a JSON blob in SQLite using some TypeAdapter magic
val content: Content,
) {
sealed class Content {
data class Image(val width: Int, val height: Int): Content()
data class Video(val framerate: Int): Content()
}
}
To access Media.Content.Image.width I would have to do
val media: Media = // { ... } - returns image media
(media.content as Media.Content.Image).width
This gets old pretty quick and seems error-prone.
With generics I would be able to do something like the following:
#Entity(tableName = "media")
data class Media<T: Media.Content>(
#PrimaryKey(autoGenerate = true) val id: Long = 0,
val content: T,
) {
sealed class Content {
data class Image(val width: Int, val height: Int): Content()
data class Video(val framerate: Int): Content()
}
}
val media: Media<Media.Content.Image> = // { ... } - returns image media
media.content.width
However, this style seems problematic:
error: Cannot use unbound fields in entities.
private final T content = null;
error: Cannot figure out how to save this field into database. You can consider adding a type converter for it.
private final T content = null;
I'm not sure what a TypeConverter for T would look like - is there a way to get Room to handle this type of generics or is it simply not supported?

Why not use a reified inline method that handles the type cast?
Since you already don't have any type safety guarantees from the compiler.
Even better would be to also serialize the type and use a switch statement to
handle every possible returned type.
inline fun <reified T> Media.contentAccess(): T {
return this.content as T
}
val media: Media = Media(id = 1, content = Media.Content.Image(width = 1, height = 1))
media.contentAccess<Media.Content.Image>().width

Related

Save complex JSON response in SQLite with Room

I'm trying to implement caching of a JSON API response with Room.
The response I get in JSON follows this data class structure:
#Serializable
data class ApiDataResponse(
val success: Boolean,
val message: String? = null,
val albums: List<AlbumResponse> = emptyList()
)
#Serializable
data class AlbumResponse(
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
val keywords: List<String>,
val pics: List<PicResponse>
)
#Serializable
data class PicResponse(
val picUrl: String,
val emojis: List<String>
)
Notes:
#Serializable is from kotlinx.serialization library to parse the JSON response.
These response data classes are only used inside my datasource layer, the view layer doesn't care about an ApiDataResponse and only knows a "pure" version of AlbumResponse called Album and a "pure" version of PicResponse called Pic (by "pure" I mean a data class without external library annotations).
So to implement this cache with Room I could discard the ApiDataResponse and save only the contents of AlbumResponse (and consequently PicResponse), having new data classes for Room entities following this idea:
#Entity(tableName = "albums")
data class AlbumEntity(
#PrimaryKey(autoGenerate = false)
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
val keywords: List<String>, // obstacle here
val pics: List<PicEntity> // obstacle here
)
// obstacle here
// #Entity
data class PicEntity(
val picUrl: String,
val emojis: List<String>
)
I already know how to save simple data in Room, with the simplest JSON I was able to do this task, the problem is that in this more complex scenario I have no idea how to achieve this goal. So I wish someone could guide me in this situation.
Maybe it's a little late, but I would still like to add some interesting information regarding MikeT's answer.
It is not necessary to create a new data class just to transform a custom object into a JSON with TypeConverter, for example:
#Entity(tableName = "albums")
data class AlbumEntity(
#PrimaryKey(autoGenerate = false)
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
val keywords: List<String>,
val pics: List<PicEntity> // can be converted directly
)
import kotlinx.serialization.Serializable
#Serializable // to be able to do the serialize with the kotlinx.serialization
data class PicEntity(
val picUrl: String,
val emojis: List<String>
)
With just these two data classes we can build the TypeConverters as follows:
import androidx.room.TypeConverter
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class DatabaseConverter {
private val json = Json
#TypeConverter
fun convertStringListToString(strings: List<String>): String =
json.encodeToString(strings)
#TypeConverter
fun convertStringToStringList(string: String): List<String> =
json.decodeFromString(string)
#TypeConverter
fun convertPicEntityListToString(picsEntity: List<PicEntity>): String =
json.encodeToString(picsEntity)
#TypeConverter
fun convertStringToPicEntityList(string: String): List<PicEntity> =
json.decodeFromString(string)
}
Code to create an example dummy list:
object DummyAlbums {
fun createList(): List<AlbumEntity> = listOf(
AlbumEntity(
id = "0001",
title = "Album AB",
createdBy = "Created by AB",
enabled = true,
keywords = listOf("ab"),
pics = dummyPics(albumId = "0001", size = 0)
),
AlbumEntity(
id = "0002",
title = "Album CD",
createdBy = "Created by CD",
enabled = false,
keywords = listOf("cd", "c", "d"),
pics = dummyPics(albumId = "0002", size = 1)
),
AlbumEntity(
id = "0003",
title = "Album EF",
createdBy = "Created by EF",
enabled = true,
keywords = listOf(),
pics = dummyPics(albumId = "0003", size = 2)
)
)
private fun dummyPics(
albumId: String,
size: Int
) = List(size = size) { index ->
PicEntity(
picUrl = "url.com/$albumId/${index + 1}",
emojis = listOf(":)", "^^")
)
}
}
So we can have the following data in table:
I wanted to highlight this detail because maybe it can be important for someone to have a table with the cleanest data. And in even more specific cases, to have it clean, you can do the conversion manually using Kotlin functions, such as joinToString(), split(), etc.
I believe the issue is with columns as lists.
What you could do is add the following classes so the Lists are embedded within a class:-
data class StringList(
val stringList: List<String>
)
data class PicEntityList(
val picEntityList: List<PicEntity>
)
and then change AlbumEntity to use the above instead of the Lists, as per:-
#Entity(tableName = "albums")
data class AlbumEntity(
#PrimaryKey(autoGenerate = false)
val id: String,
val title: String,
val createdBy: String,
val enabled: Boolean,
//val keywords: List<String>, // obstacle here
val keywords: StringList, /// now not an obstacle
//val pics: List<PicEntity> // obstacle here
val emojis: PicEntityList// now not an obstacle
)
To be able to store the "complex" (single object) you need to convert this so some TypeConverters e.g.
class RoomTypeConverters{
#TypeConverter
fun convertStringListToJSON(stringList: StringList): String = Gson().toJson(stringList)
#TypeConverter
fun convertJSONToStringList(json: String): StringList = Gson().fromJson(json,StringList::class.java)
#TypeConverter
fun convertPicEntityListToJSON(picEntityList: PicEntityList): String = Gson().toJson(picEntityList)
#TypeConverter
fun convertJSONToPicEntityList(json: String): PicEntityList = Gson().fromJson(json,PicEntityList::class.java)
}
note this utilises the dependency com.google.code.gson
You then need to have the #TypeConverters annotation to cover the appropriate scope (at the #Database level is the most scope). Note the plural rather than singular, they are different.
To demonstrate the above works, First some functions in an interface annotated with #Dao :-
#Dao
interface AlbumDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(albumEntity: AlbumEntity): Long
#Query("SELECT * FROM albums")
fun getAllAlbums(): List<AlbumEntity>
}
Second an #Database annotated class (note the #TypeConverters annotation) :-
#TypeConverters(RoomTypeConverters::class)
#Database(entities = [AlbumEntity::class], exportSchema = false, version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getAlbumDao(): AlbumDao
companion object {
#Volatile
private var instance: TheDatabase?=null
fun getInstance(context: Context): TheDatabase {
if (instance==null) {
instance = Room.databaseBuilder(context,TheDatabase::class.java,"album.db")
.allowMainThreadQueries()
.build()
}
return instance as TheDatabase
}
}
}
Third some activity code to actually do something (insert some Albums and then extract them writing the extracted data to the Log) :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: AlbumDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getAlbumDao()
dao.insert(AlbumEntity(
"Album001", "The First Album","Fred",false,
StringList(listOf("The","First","Album")),
PicEntityList(
listOf(
PicEntity("PE001", listOf("emoji1","emoji2","emoji3")),
PicEntity("PE002",listOf("emoji10")),
PicEntity("PE003", listOf("emoji20","emoji21"))
))
))
dao.insert(AlbumEntity(
"Album002","This is the Second Album","Mary", true,
StringList(listOf("keya","keyb","keyc","keyd","keye")),
PicEntityList(
listOf(
PicEntity("PE011", listOf("emoji30","emoji31")),
PicEntity("PE012", listOf("emoji1","emoji10","emoji20","emoji30"))
))
))
for (a in dao.getAllAlbums()) {
logAlbum(a)
}
}
fun logAlbum(albumEntity: AlbumEntity) {
val keywords = StringBuilder()
for(s in albumEntity.keywords.stringList) {
keywords.append("\n\t$s")
}
val pelog = StringBuilder()
for (pe in albumEntity.emojis.picEntityList) {
pelog.append("\n\tURL is ${pe.picUrl}")
for (emoji in pe.emojis) {
pelog.append("\n\t\tEmoji is ${emoji}")
}
}
Log.d(
"ALBUMINFO",
"Album id is ${albumEntity.id} " +
"Title is ${albumEntity.title} " +
"CreateBy ${albumEntity.createdBy} " +
"Enabled=${albumEntity.enabled}. " +
"It has ${albumEntity.keywords.stringList.size} keywords. " +
"They are $keywords\n. " +
"It has ${albumEntity.emojis.picEntityList.size} emojis. " +
"They are ${pelog}"
)
}
}
Run on the main thread for convenience and brevity
When run then the log contains:-
D/ALBUMINFO: Album id is Album001 Title is The First Album CreateBy Fred Enabled=false. It has 3 keywords. They are
The
First
Album
. It has 3 emojis. They are
URL is PE001
Emoji is emoji1
Emoji is emoji2
Emoji is emoji3
URL is PE002
Emoji is emoji10
URL is PE003
Emoji is emoji20
Emoji is emoji21
D/ALBUMINFO: Album id is Album002 Title is This is the Second Album CreateBy Mary Enabled=true. It has 5 keywords. They are
keya
keyb
keyc
keyd
keye
. It has 2 emojis. They are
URL is PE011
Emoji is emoji30
Emoji is emoji31
URL is PE012
Emoji is emoji1
Emoji is emoji10
Emoji is emoji20
Emoji is emoji30
i.e. the 2 albums have been extracted along with the appropriate embedded lists.
The Albums table itself (via App Inspection) consists of :-
An Alternative, and from a Database perspective, better approach, instead of embedding lists as a single value (String), would have the lists as related tables (with a one-many or a many-many relationship).

How to define values in code block uses data class with room?

I fetch data from API and I would like to reuse it in data class like this (ItemStateRoom is data with String, Int etc.):
#Entity(tableName = "item")
data class ItemRoom(
#PrimaryKey(autoGenerate = true) var id: Int = 0,
var type: String = "",
var title: String = "",
var template: String = "",
#ColumnInfo(name = "state")
#JsonAdapter(ItemStateAdapter::class)
var state: ItemStateRoom?
{
var itemState: ItemStateRoom
get() = state!!
set(value) {
state = value
}
}
When I set state to null of course I have null, but program compiles well.
If it is like at the top error occurs:
error: Entities and POJOs must have a usable public constructor. You can have an empty constructor or a constructor whose parameters match the fields (by name and type).
I also tried to do like this:
#Ignore
#JsonAdapter(ItemStateAdapter::class)
#ColumnInfo(name = "state")
var state: ItemStateRoom?,
Then I have errors:
error: Cannot figure out how to read this field from a cursor.
.ItemStateRoom state;
Cannot figure out how to save this field into database. You can consider adding a type converter for it.
Entities and POJOs must have a usable public constructor. You can have an empty constructor or a constructor whose parameters match the fields (by name and type).
EDIT1:
Description: I make realm to room migration, realmobjects are downloaded as well and they are not null, but some of objects(data classes) I have changed to Room i.e. ItemStateRoom or ItemSubStatus. For sure it is not null, I download it from backend in postman it gives value for sure.
#TypeConverters(Converters::class)
#Entity(tableName = "itemsubstatus")
data class ItemSubStatusRoom(
#PrimaryKey(autoGenerate = true) var id: Int = 0,
#ColumnInfo(name = "title") val title: String = "",
#ColumnInfo(name = "description") val description: String? = null
)
TypeConverters:
class Converters {
private val gson = GsonBuilder().serializeNulls().create()
#TypeConverter
fun typeSubStatusToString(type: ItemSubStatusRoom?): String =
gson.toJson(type)
#TypeConverter
fun typeItemSubStatusFromString(value: String): ItemSubStatusRoom? =
gson.fromJson(value, ItemSubStatusRoom::class.java) as ItemSubStatusRoom?
}
My response data is simple:
class ItemsDataResponse (
#SerializedName("items")
val items: ArrayList<ItemRoom>,
val total: Int
)
in my ItemData it is also simple
#TypeConverters(Converters::class)
#Entity(tableName = "item")
data class ItemRoom #JvmOverloads constructor(#PrimaryKey(autoGenerate = true) var id: Int = 0,
[...]
#ColumnInfo(name = "sub_status")
var subStatus: ItemSubStatusRoom?,
``
When it is saying there is no "public constructor", it is saying that it does not know how to construct the ItemRoom object that does not include the state field/member.
So with state #Ignore'd you would need to have a constructor that doesn't expect the state as a parameter.
with a Data Class the definition within the parenthesises is the constructor as such.
e.g.
constructor(type: String, title: String, template: String): this(type = type, title = title, template = template, state = null)
in which case Room would always construct an ItemRoom where state is null.
However, I suspect that the above is not what you want. Rather that you want the state to be saved in the database. As such, as it is not a type that can be stored directly and hence the need for a TypeConverter (actually 2)
only String, integer (Int, Long, Byte, Boolean etc types), decimal (Float, Double etc types) or byte streams (ByteArray) can be stored directly
Then if you want the state field to be saved in the database, then you will need 2 TypeConverters.
One which will convert from the ItemStateRoom to one of the types that can be stored (so it is passed an ItemStateRoom and returns one of the types that can be stored directly).
The other will convert from the type stored to an ItemStateRoom object.
So assuming that you want to store the state you could have, something like :-
#TypeConverters(value = [Converters::class])
#Entity(tableName = "item")
data class ItemRoom(
#PrimaryKey(autoGenerate = true) var id: Int = 0,
var type: String = "",
var title: String = "",
var template: String = "",
//#Ignore /*<<<<< if used a constructor is needed that doesn't require the state, as the state is not available */
/* if not used then state has to be converted to a type that room can store */
#ColumnInfo(name = "state")
#JsonAdapter(ItemStateAdapter::class)
var state: ItemStateRoom?
)
{
//constructor(type: String, title: String, template: String): this(type = type, title = title, template = template, state = null)
var itemState: ItemStateRoom
get() = state!!
set(value) {
state = value
}
}
data class ItemStateRoom(
/* made up ItemStateRoom */
var roomNumber: Int,
var roomType: String
)
class ItemStateAdapter() {
/* whatever .... */
}
class Converters {
#TypeConverter
fun convertItemStateRoomToJSONString(itemStateRoom: ItemStateRoom): String = Gson().toJson(itemStateRoom)
#TypeConverter
fun convertFromJSONStringToItemStateRoom(jsonString: String): ItemStateRoom = Gson().fromJson(jsonString,ItemStateRoom::class.java)
}
note the commented out #Ignore and constructor (for if #Ignore'ing the state field).
how you implement, if at all, the #JsonAdapter, would be as you have it.
note the #TypeConverters (which you would omit if #Ignoreing the state field)
It might be preferable to include the #TypeConverters at the #Database level, where it has full scope. (see https://developer.android.com/reference/androidx/room/TypeConverters)
Example
Here's an example of storing the state (ItemStateRoom) that uses the code above, with a pretty standard #Database annotated class and an #Dao annotated interface :-
#Dao
interface ItemRoomDao {
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(itemRoom: ItemRoom): Long
#Query("SELECT * FROM item")
fun getAllItems(): List<ItemRoom>
}
Two items are inserted and then extracted and written to the log using the following activity code :-
class MainActivity : AppCompatActivity() {
lateinit var db: TheDatabase
lateinit var dao: ItemRoomDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = TheDatabase.getInstance(this)
dao = db.getItemRoomDao()
dao.insert(ItemRoom(type = "Bedroom", template = "The Template", title = "Bedroom1", state = ItemStateRoom(101,"King size - Shower - Bath - Kitchenette")))
dao.insert(ItemRoom(type = "Bedroom", template = "The Template", title = "Bedroom2", state = ItemStateRoom(102,"Queen size - Shower - Bath")))
for(i in dao.getAllItems()) {
Log.d("DBINFO","Item ID is ${i.id} Title is ${i.title} Room Number is ${i.state!!.roomNumber} Room Type is ${i.state!!.roomType}")
}
}
}
The result in the log:-
D/DBINFO: Item ID is 1 Title is Bedroom1 Room Number is 101 Room Type is King size - Shower - Bath - Kitchenette
D/DBINFO: Item ID is 2 Title is Bedroom2 Room Number is 102 Room Type is Queen size - Shower - Bath
The database via App Inspection :-
as can be seen the ItemStateRoom has been converted to (and from according to the output in the log) a JSON String.
Ignore the weatherforecast table, that was from another answer that was used for providing this answer.
Alternative Approach
Instead of converting the ItemState to a JSON representation (which is unwieldly from a database perspective) consider the potential advantages of instead embedding the ItemState. The difference in this approach is that the fields of the ItemState are each saved as individual columns.
e.g.
#Entity(tableName = "item")
data class ItemRoom(
#PrimaryKey(autoGenerate = true) var id: Int = 0,
var type: String = "",
var title: String = "",
var template: String = "",
//#Ignore /*<<<<< if used a constructor is needed that doesn't require the state, as the state is not available */
/* if not used then state has to be converted to a type that room can store */
//#ColumnInfo(name = "state")
//#JsonAdapter(ItemStateAdapter::class)
//var state: ItemStateRoom?
#Embedded
var itemStateRoom: ItemStateRoom
)
{
//constructor(type: String, title: String, template: String): this(type = type, title = title, template = template, state = null)
/*
var itemState: ItemStateRoom
get() = state!!
set(value) {
state = value
}
*/
}
Thus to update, at least from the database aspect, you are freed from trying to manipulate a representation of the data. You can update the actual data directly.
Using the above, with the example code (changed to use itemState instead of state) then the database looks like:-
You say
But to reassign values dynamically I have to use #JsonAdapter.
The JsonAdapter is NOT going to magically update the data in the database, to update the data you will have to use a function in an interface (or an abstract class) that is annotated with #Dao which will either be annotated with #Update (convenience) or #Query (with appropriate UPDATE SQL).

Parsing Nested Polymorphic Objects with GSON and Retrofit

I am trying to show list of messages with different types of ViewHolders i.e. Text, ImageText, Video etc. I get a list of these objects from API somewhat in this format:
{
"message":"success",
"total_pages":273,
"current_page":1,
"page_size":10,
"notifications":[
{
"id":4214,
"notification_message":"test notification 1",
"meta_data":{
"messageId":"19819189",
"viewHolderType":"textOnly",
"body":{
"time":"10-06-21T02:31:29,573",
"type":"notification",
"title":"Hi, Welcome to the NT experience",
"description":"This is the welcome message",
"read":true
}
}
},
{
"id":9811,
"notification_message":"test vss notification",
"meta_data":{
"messageId":"2657652",
"viewHolderType":"textWithImage",
"body":{
"time":"11-06-21T02:31:29,573",
"type":"promotions",
"title":"Your Package - Premium",
"description":"Thank you for subscribing to the package. Your subscription entitles you to Premium 365 Days Plan (worth $76.61)",
"headerImage":"www.someurl.com/image.jpg",
"read":true
}
}
}
]
}
Now I have to parse this list from network module for client module which will use only the objects inside meta_data. To that end I have created following classes:
open class BaseMessageListItem
internal data class MessageListResponse(
#field:SerializedName("current_page")
val current_page: Int,
#field:SerializedName("notifications")
val notifications: List<MessageListItem>,
#field:SerializedName("message")
val message: String,
#field:SerializedName("page_size")
val page_size: Int,
#field:SerializedName("total_page")
val total_page: Int
)
internal data class MessageListItem(
#field:SerializedName(“id”)
val id: String,
#field:SerializedName("notification_message")
val notification_message: String,
#field:SerializedName("meta_data")
val meta_data: MessageListMetaDataItem,
)
internal data class MessageListMetaDataItem(
#field:SerializedName("messageId")
val messageId: String = "",
#field:SerializedName("viewHolderType")
val viewHolderType: String = "",
#field:SerializedName("body")
val body: BaseMessageListItem = BaseMessageListItem()
)
internal data class ImageMessageListItem(
#field:SerializedName("description")
val description: String,
#field:SerializedName("headerImage")
val headerImage: String,
#field:SerializedName("read")
val read: Boolean,
#field:SerializedName("time")
val time: String,
#field:SerializedName("title")
val title: String,
#field:SerializedName("type")
val type: String
): BaseMessageListItem()
internal data class TextMessageListItem(
#field:SerializedName("description")
val description: String,
#field:SerializedName("read")
val read: Boolean,
#field:SerializedName("time")
val time: String,
#field:SerializedName("title")
val title: String,
#field:SerializedName("type")
val type: String
): BaseMessageListItem()
The notifications>meta_data>body can be polymorphic. I have set of classes (for ImageItem, ImageWithTextItem, VideoItem etc) which extend to BaseMessageListItem.
private var runtimeTypeAdapterFactory: RuntimeTypeAdapterFactory<BaseMessageListItem> = RuntimeTypeAdapterFactory
.of(BaseMessageListItem::class.java, "viewHolderType")
.registerSubtype(ImageMessageListItem::class.java, MessageListItemTypes.TEXT_WITH_IMAGE.value)
.registerSubtype(TextMessageListItem::class.java, MessageListItemTypes.TEXT_ONLY.value)
private var gson: Gson = GsonBuilder()
.registerTypeAdapterFactory(runtimeTypeAdapterFactory)
.create()
I tried parsing it using viewHolderType in RuntimeTypeAdapterFactory but since it's not a property of BaseMessageListItem, it is not able to parse it.
Any one has any experience dealing with this type of JSON, please do share any pointers.
RuntimeTypeAdapterFactory requires the viewHolderType field to be put right into the body objects. In order to fix this, you have
either patch RuntimeTypeAdapterFactory (it is not even published as a compiled JAR, but rather still retains in the public repository as source code free to modify), or fix your class hierarchy to lift up the missing field because it can only work with fields on the same nest level.
internal var gson: Gson = GsonBuilder()
.registerTypeAdapterFactory(
RuntimeTypeAdapterFactory.of(BaseMessageListMetaDataItem::class.java, "viewHolderType")
.registerSubtype(TextWithImageMessageListMetaDataItem::class.java, "textWithImage")
.registerSubtype(TextOnlyMessageListMetaDataItem::class.java, "textOnly")
)
.create()
internal data class MessageListItem(
#field:SerializedName("meta_data")
val metaData: BaseMessageListMetaDataItem<*>?,
)
internal abstract class BaseMessageListMetaDataItem<out T>(
#field:SerializedName("viewHolderType")
val viewHolderType: String?,
#field:SerializedName("body")
val body: T?
) where T : BaseMessageListMetaDataItem.Body {
internal abstract class Body
}
internal class TextOnlyMessageListMetaDataItem
: BaseMessageListMetaDataItem<TextOnlyMessageListMetaDataItem.Body>(null, null) {
internal data class Body(
#field:SerializedName("title")
val title: String?
) : BaseMessageListMetaDataItem.Body()
}
internal class TextWithImageMessageListMetaDataItem
: BaseMessageListMetaDataItem<TextWithImageMessageListMetaDataItem.Body>(null, null) {
internal data class Body(
#field:SerializedName("title")
val title: String?,
#field:SerializedName("headerImage")
val headerImage: String?
) : BaseMessageListMetaDataItem.Body()
}
I might be understanding you wrong, but I would like to suggest a different approach. I am assuming you would like to assign to get a ViewHolder type directly from what you get in your API response.
There are two approaches I would like to suggest:
First, if it is possible to get the API response modified, I would suggest to change viewHolderType from a String to an Int so as you can be clear with your mapping and then you can directly compare it.
Second what I would suggest is to keep another key in your data class which sets value as per the viewHolderType it receives which would be something of as follows.
internal data class MessageListMetaDataItem(
#field:SerializedName("messageId")
val messageId: String = "",
#field:SerializedName("viewHolderType")
val viewHolderType: String = "",
#field:SerializedName("body")
val body: BaseMessageListItem = BaseMessageListItem()
) {
val viewHolderMapping: Int
get() = when(viewHolderType){
"textOnly" -> MessageListItemTypes.TEXT_ONLY
"textWithImage" -> MessageListItemTypes.TEXT_WITH_IMAGE
else -> MessageListItemTypes.UNKNOWN_TYPE
}
}

How to ignore field when Serializing in moshi?

Im new to Moshi and Kotlin and i actually made two methods which returns data from my Room database and make from two objects only one which then i'm going to serialize via Moshi.
Is there a way to ignore or remove some fields from serialization? i tried by using #Transient but that throw lot of errors cause of my model structure.
Here is how my models looks like:
#JsonClass(generateAdapter = true)
#Entity(tableName = "corpo", foreignKeys = [
ForeignKey(
entity = Testata::class,
parentColumns = ["id"],
childColumns = ["id_testata"],
onDelete = CASCADE
)
], indices = [ Index("id_testata") ], primaryKeys = [
"barcode", "id_testata"
])
data class Corpo(
var barcode: String,
var desc: String?, // ignore this
#ColumnInfo(defaultValue = "PZ")
var um: String, // ignore this
var qta: Float,
var id_testata: Int // i have to ignore this
)
And
#JsonClass(generateAdapter = true)
#Entity(tableName = "testata")
data class Testata(
#PrimaryKey(autoGenerate = true)
var id: Int,
var cod: String,
var tipo: String,
var cod_fornitore: String,
var desc_fornitore: String, // ignore this
var data: String,
var inviato: Boolean // ignore this
){
constructor(cod: String, tipo: String, cod_fornitore: String, desc_fornitore: String, data: String, inviato: Boolean)
: this(0, cod, tipo, cod_fornitore, desc_fornitore, data, inviato)
}
#JsonClass(generateAdapter = true) // this is my class which combine the two classes from Room db
data class Documento(
var testata: Testata,
var corpo: List<Corpo>
)
And here i get my serialized object via Moshi:
val moshi = Moshi.Builder().build()
val jsonAdapter: JsonAdapter<Documento> = moshi.adapter(Documento::class.java)
val documento = Documento(corpoViewModel.selectTestata(testata.id), corpoViewModel.selectCorpo(testata.id))
val json = jsonAdapter.toJson(documento)
At this point how can i ignore the fields i commented and remove them from my serialization?
I see 2 viable options for your use case.
Before you go you will need 2 separate classes for the desired serialized json and you can choose:
Using a Mapper to map one dto to the other.
Using a Moshi's Custom Type Adapter for handling the mapping of both Testata and Corpo
First of all, define the two desired models:
data class JsonCorpo(
var barcode: String,
var qta: Float,
)
data class JsonTestata(
var id: Int,
var cod: String,
var tipo: String,
var cod_fornitore: String,
var data: String,
)
And use JsonTestata and JsonCorpo instead of Testata and Corpo, inside your combined Documento class
Option 1
I usually have an interface for mappers.
This is handy if you use an injection framework like Dagger because you don't have to remember the name of the mapper class.
/**
* Base mapper to convert [Input] type to [Output] type.
*/
interface Mapper<Input, Output> {
/**
* Transforms [input] into [Output].
*
* #param input the input to be transformed
* #return transformation result [Output]
*/
fun map(input: Input): Output
/**
* Transforms a [List] of [Input] into a [List] of [Output].
*
* #param input The input to be transformed
* #return transformation result
*/
fun map(input: List<Input>): List<Output> {
val result: MutableList<Output> = LinkedList()
for (item in input) {
result.add(map(item))
}
return result
}
}
And declare each mapping:
class CorpoToJsonCorpoMapper: Mapper<Corpo, SerializedCorpo> {
override fun map(input: Corpo): SerializedCorpo = with(input) {
SerializedCorpo(barcode, qta)
}
}
class TestataToJsonTestataMapper: Mapper<Testata, SerializedTestata> {
override fun map(input: Testata): SerializedTestata = with(input) {
SerializedTestata(id, cod, tipo, cod_fornitore, data)
}
}
And then you can use the two mappers to map the result from
corpoViewModel.selectTestata(testata.id)
and
corpoViewModel.selectCorpo(testata.id)
Option 2
You let a Moshi's adapter take care of the mapping:
class CorpoJsonAdapter {
#FromJson Event corpoFromJson(JsonCorpo jsonCorpo) {
return Corpo(
...
// Here handle the deserialization
);
}
#ToJson JsonCorpo corpoToJson(Corpo corpo) {
return JsonCorpo(corpo.barcode, corpo.qta)
}
}
and similar declaring a TestataJsonAdapter to handle the serialization and deserialization for the Testata class.

Serialized Data Class combined with built-in modifications

I am working on updating the parsing of an API response that uses a Serialized Data Class to parse the JSON response. The serialization works perfectly fine right now, but the new data that I'm attempting to parse into data class is not fully reliant on data in the json. Here is what I mean by that:
The data class is Career, and the new data I need to parse is a set of skills and each have a rating. The json data is very simple and contains the skills as such:
{
// other career data
...
"mathematics_skill": 8,
"critical_thinking_skill": 6
... // the remaining skills
}
Using straight serialization, I would only be able to store the data as such:
data class Career(
// Other career data
#serializableName("mathematic_skill") val mathSkill: Int,
#serializableName("critical_thinking_skill") val mathSkill: Int,
// remaining skills
)
However, I would like to store all skills in an array variable of a custom skills class that not only contains the rating, but also the name of the skill and a color. Basically, when I access the skills data of a career, I would like to access it like such:
val careerMathSkill = career.skills[0]
val mathRating = careerMathSkill.rating
val mathColor = careerMathSkill.color
Is it possible to use the serialized data from the data class to add non-serialized data to the same data class? (Sorry for the weird wording, not sure how else to explain it)
EDIT: Here is what I have:
class CareersRemote(
#SerializedName("careers") val careers: List<Career>
) {
companion object {
fun parseResponse(response: Response<CareersRemote>): CareersResponse {
return if (response.isSuccessful) {
response.body()!!.format()
} else
CareersResponse(listOf(CareersResponse.ErrorType.Generic()))
}
}
fun format(): CareersResponse {
val careers = topCareers.map {
Career(
id = it.id,
title = it.title,
)
}.toMutableList()
return CareersResponse(CareersResponse.SuccessData(careers = careers))
}
data class Career(
#SerializedName("id") val id: String,
#SerializedName("title") val title: String,
)
}
Here is what I am hoping to do in a way
class CareersRemote(
#SerializedName("careers") val careers: List<Career>
) {
companion object {
fun parseResponse(response: Response<CareersRemote>): CareersResponse {
return if (response.isSuccessful) {
response.body()!!.format()
} else
CareersResponse(listOf(CareersResponse.ErrorType.Generic()))
}
}
fun format(): CareersResponse {
val careers = topCareers.map {
Career(
id = it.id,
title = it.title,
)
}.toMutableList()
return CareersResponse(CareersResponse.SuccessData(careers = careers))
}
data class Career(
#SerializedName("id") val id: String,
#SerializedName("title") val title: String,
// skills array that will need to be filled out based on the data I got in the json
var skills: List<Skill>
)
}
EDIT: The suggested solution
class CareersRemote(
#SerializedName("careers") val careers: List<Career>
) {
companion object {
fun parseResponse(response: Response<CareersRemote>): CareersResponse {
return if (response.isSuccessful) {
response.body()!!.format()
} else
CareersResponse(listOf(CareersResponse.ErrorType.Generic()))
}
}
fun format(): CareersResponse {
val careers = topCareers.map {
Career(
id = it.id,
title = it.title,
)
}.toMutableList()
return CareersResponse(CareersResponse.SuccessData(careers = careers))
}
data class Career(
#SerializedName("id") val id: String,
#SerializedName("title") val title: String,
#SerializedName("math_skill") val mathSkill: Int
#SerializedName("other_skill") val mathSkill: Int
) {
var skills: List<Skill> = {
val mathSkill = Skill(name: "Math", rating: mathSkill, color: /**some color*/)
val otherSkill = Skill(name: "Other", rating: otherSkill, color: /**some color*/)
return listOf(mathSkill, otherSkill)
}
}
}
Yes, you can create a custom JsonDeserializer to modify how the JSON is parsed.
Here is a basic example of what that would look like.
class CareerDeserializer : JsonDeserializer<Career> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Career {
val obj = json.asJsonObject
// standard career data
val id = obj.get("id")?.asString
val name = obj.get("name").asString
// making a Skill object
val skill = Skill(
obj.get("mathematic_skill").asInt,
obj.get("critical_thinking_skill").asInt,
obj.get("swimming_skill").asInt
// etc
)
return Career(id, name, skill)
}
}
And make sure to register that within your GsonBuilder.
val gson = GsonBuilder()
.registerTypeAdapter(Career::class.java, CareerDeserializer())
.create()
Note, you'll also have to create a JsonSerializer if you want to go the other way too.
Edit:
However, if you're just looking to change the syntax of how you're accessing that data, you can do something like this.
data class Career(
// Other career data
val mathSkill: Int,
val thinkSkill: Int
// remaining skills
) {
val skills: List<Int>
get() = listOf(mathSkill, thinkSkill)
}
This would give you a skills list back whenever you needed it, and it would be created when you accessed it, so you won't have to worry about the data being out of sync. This would allow you to access your data as such.
career.skills[0] // get the math skill.
And you can take this another step further by adding a get operator to your Career class.
data class Career(
// Other career data
val mathSkill: Int,
val thinkSkill: Int
// remaining skills
) {
...
operator fun get(pos: Int) = skills[pos]
}
Now, you can simply do
career[0] // get the math skill.
Warning, this is dangerous because you're accessing an Array so you could get OutOfBoundsExceptions. Use constants to help you out.
Edit 2:
val skills = {
listOf(Skill("Math", mathSkill, /** some color */ ),
Skill("Other", otherSkill, /** some color */ ))
}

Categories

Resources