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).
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
I need to save some data from my API in my Room database, so i've just created my model as the API response in my android project but when i try to run it the app require to set the Entity even to my object which is only set to give a definition of the object which will be saved in my database...
So which will be the right way to save an array of objects inside an object in my Room database?
Here is how my models looks like:
#Entity(tableName = "articoli_server_table")
data class ArticoliServer(
#PrimaryKey(autoGenerate = false)
var codart: String,
var desc: String,
var prezzo_acq: Float,
var prezzo_vend: Float,
var barcode: List<barcode>,
var qta: Float
)
data class barcode(
var barcode: String,
var qta: Float
)
There are a couple of approaches you can implement to save your list of barcode data in your database:
1. you can use custom TypeConverter for your list of barcode objects, for example you can persist your list of objects as a single string in the following form (comma seperated values): "barcode1,qta1,barcode2,qta2,..", the implementation could be like this:
#Entity(tableName = "articoli_server_table")
#TypeConverters(BarcodeListConverter::class)
data class ArticoliServer(
#PrimaryKey(autoGenerate = false)
var codart: String,
var desc: String,
var prezzo_acq: Float,
var prezzo_vend: Float,
var barcode: List<barcode>?,
var qta: Float
)
data class barcode(
var barcode: String,
var qta: Float
)
object BarcodeListConverter {
#TypeConverter
fun toString(barcodeList: List<barcode>?): String? {
if (barcodeList == null) return null
val stringList = mutableListOf<String>()
barcodeList.forEach {
stringList.add(it.barcode)
stringList.add(it.qta.toString())
}
return stringList.joinToString(",")
}
#TypeConverter
fun toBarcodeList(str: String?): List<barcode>? {
if (str == null) return null
val barcodeList = mutableListOf<barcode>()
val strList = str.split(",")
for (i in strList.indices step 2) {
barcodeList.add(barcode(strList[i], strList[i + 1].toFloat()))
}
return barcodeList
}
}
2. You can create a new entity (DB table) in your database for your barcode entities, and create a one to many relationship between your ArticoliServer and barcode entities. You can check this page for more info about defining relationship with Room persistence library.
P.S I recommend you to implement the second approach in case you have many barcode objects to persist in DB.
This is my model class and i have these types of json to convert in to this model. How i can do that with Moshi (Using Retrofit)
data class(var Id:Int, var image:List<String>)
{"Id":188, "image":"\/posts\/5fd9aa6961c6dd54129f51d1.jpeg"}
{"Id":188, "image":["\/posts\/5fd9aa6961c6dd54129f51d1.jpeg","\/posts\/5fd9aa6961c6dd54129f51d1.jpeg"]}
Your case is a bit unorthodox, in general I would avoid designing JSONs with matching field names but different signature. Anyway, the solution:
Define your model as follow:
data class MyModel(
#Json(name = "Id") val Id: Long,
#Json(name = "image") val images: List<String>
)
Then, you will have to create a custom adapter for it:
class MyModelAdapter {
#ToJson
fun toJson(model: MyModel): String {
// MyModel is data class so .toString() should convert it to correct Json format with
// image property as list of image path strings
return model.toString()
}
#FromJson
fun fromJson(reader: JsonReader): MyModel = with(reader) {
// We need to manually parse the json
var id: Long? = null
var singleImage: String? = null
val imageList = mutableListOf<String>()
beginObject()
while (hasNext()) {
// iterate through the JSON fields
when (nextName()) {
"Id" -> id = nextLong() // map the id field
"image" -> { // map the image field
when (peek()) {
JsonReader.Token.BEGIN_ARRAY -> {
// the case where image field is an array
beginArray()
while(hasNext()) {
val imageFromList = nextString()
imageList.add(imageFromList)
}
endArray()
}
JsonReader.Token.STRING -> {
// the case where image field is single string
singleImage = nextString()
}
else -> skipValue()
}
}
else -> skipValue()
}
}
endObject()
id ?: throw IllegalArgumentException("Id should not be null")
val images = if (singleImage != null) {
listOf(singleImage)
} else {
imageList
}
MyModel(Id = id, images = images)
}
}
Then, add the adapter to your moshi builder:
Moshi.Builder()
.add(MyModelAdapter())
.build()
That should do it. For complete code base, you can check my demo I just created that mirrors your case:
https://github.com/phamtdat/MoshiMultipleJsonDemo
I'm new to kotlin so this maybe a very easy issue to resolve.
What I'm trying to do is filter the json response that I receive using Retrofit2 before I display the images in a grid with a RecyclerView.
instagram.com/explore/tags/{hashtag}/?__a=1&max_id= Using Retrofit2 I'm able to get the data response fine and also display the given url images in a RecyclerView.
I have not been successful in using the filter, map, loops and conditions to remove elements from the Arraylist. I do not understand these to the fullest extent but I have searched looking for solutions and those are what I came apon.
Interface
interface InstagramDataFetcher
{
#GET("tags/{tag}/?__a=1&max_id=")
fun getInstagramData(#Path("tag") hashtag: String) : Call <InstagramResponse>
}
Where I get my response from and also get StringIndexOutOfBoundsException
class InstagramFeedFragment : Fragment()
{
private fun onResponse()
{
val service = RestAPI.retrofitInstance?.create(InstagramDataFetcher::class.java)
val call = service?.getInstagramData("hashtag")
call?.enqueue(object : Callback<InstagramResponse>
{
override fun onFailure(call: Call<InstagramResponse>, t: Throwable)
{
Log.d("FEED", " $t")
}
override fun onResponse(
call: Call<InstagramResponse>, response: Response<InstagramResponse>
)
{
//for ((index, value) in data.withIndex())
if (response.isSuccessful)
{
var data: ArrayList<InstagramResponse.InstagramEdgesResponse>? = null
val body = response.body()
data = body!!.graphql.hashtag.edge_hashtag_to_media.edges
for ((index, value) in data.withIndex())
{
if(value.node.accessibility_caption[index].toString().contains("text") ||
value.node.accessibility_caption[index].toString().contains("person"))
{
data.drop(index)
}
}
recyclerView.adapter = InstagramGridAdapter(data, parentFragment!!.context!!)
}
}
})
}
}
This is my model class
data class InstagramResponse(val graphql: InstagramGraphqlResponse)
{
data class InstagramGraphqlResponse(val hashtag: InstagramHashtagResponse)
data class InstagramHashtagResponse(val edge_hashtag_to_media: InstagramHashtagToMediaResponse)
data class InstagramHashtagToMediaResponse(
val page_info: InstagramPageInfo,
val edges: ArrayList<InstagramEdgesResponse>
)
data class InstagramPageInfo(
val has_next_page: Boolean,
val end_cursor: String
)
data class InstagramEdgesResponse(val node: InstagramNodeResponse)
data class InstagramNodeResponse(
val __typename: String,
val shortcode: String,
val display_url: String,
val thumbnail_src: String,
val thumbnail_resources: ArrayList<InstagramThumbnailResourceResponse>,
val is_video: Boolean,
val accessibility_caption: String
)
data class InstagramThumbnailResourceResponse(
val src: String,
val config_width: Int,
val config_height: Int
)
}
Simply again, I want to just remove elements from the arraylist that match certain things what I don't want. For instance. the "is_video" value that comes from the json. I want to go through the arraylist and remove all elements that have "is_video" as true.
Thanks
If you asking how to filter the list then below is the demo.
You just need to use filter on your data which is an ArrayList. I've tried keeping the same structure for the models so that you can get a better understanding.
fun main() {
val first = InstagramNodeResponse(
title = "first",
is_video = true
)
val second = InstagramNodeResponse(
title = "second",
is_video = false
)
val list: ArrayList<InstagramEdgesResponse> = arrayListOf(
InstagramEdgesResponse(node = first),
InstagramEdgesResponse(node = second)
)
val itemsWithVideo = list.filter { it.node.is_video == true }
val itemsWithoutVideo = list.filter { it.node.is_video == false }
println(itemsWithVideo.map { it.node.title }) // [first]
println(itemsWithoutVideo.map { it.node.title }) // [second]
}
// Models
data class InstagramEdgesResponse(val node: InstagramNodeResponse)
data class InstagramNodeResponse(
val title: String,
val is_video: Boolean
)