I have following class hierarchy Github Sample
interface OptionV2 {
val id: String
}
#JsonClass(generateAdapter = true)
class ImageSelectionOption(
override val id: String,
value: String,
#Json(name = "active_image")
val image: String?,
): OptionV2
#JsonClass(generateAdapter = true)
class QuestionResponse<T> (
override val id: String,
val answer: T?,
): OptionV2
And following test
val childOptions = listOf(ImageSelectionOption(value = "dummy", id = "dummy", image = "dummy"))
val childResponse = QuestionResponse<List<OptionV2>>(answer = childOptions, id = "child_qid")
val parentOptions = listOf(childResponse)
val parentResponse = QuestionResponse<Any>(answer = parentOptions, id = "parent_qid")
val moshi = Moshi.Builder().add(OptionV2MoshiAdapter.OptionAdapterFactory).build()
val type = Types.newParameterizedType(QuestionResponse::class.java, Any::class.java)
moshi.adapter<QuestionResponse<Any>>(type).toJson(parentResponse)
I am essentially attempting to deserialize QuestionResponse<List<QuestionResponse<List<Option>>>> type. This fails with following error
Failed to find the generated JsonAdapter constructor for 'class dev.abhishekbansal.moshilistinheritance.QuestionResponse'. Suspiciously, the type was not parameterized but the target class 'dev.abhishekbansal.moshilistinheritance.QuestionResponseJsonAdapter' is generic. Consider using Types#newParameterizedType() to define these missing type variables.
I wish to be able to write a custom adapter for this if needed. As I need to be able to deserialize this in the Retrofit scenario.
Here is more complete Github Sample
Update
Finally got it working by using this
// List<Option>
val listType = Types.newParameterizedType(List::class.java, OptionV2::class.java)
// QuestionResponse<List<Option>>
val qr1 = Types.newParameterizedType(QuestionResponse::class.java, listType)
// List<QuestionResponse<List<Option>>>
val listType2 = Types.newParameterizedType(List::class.java, qr1)
// QuestionResponse<List<QuestionResponse<List<Option>>>>
val finalType = Types.newParameterizedType(QuestionResponse::class.java, listType2)
println(moshi.adapter<QuestionResponse<Any>>(finalType).toJson(parentResponse))
I am still confused about how can I write a custom adapter for this which can be supplied to Moshi instance which is supplied to Retrofit. So that it can be serialized on the fly.
Here is the Custom Adapter that worked for me. I have a couple of doubts in this but it works.
class QuestionResponseAdapter<T>(val elementAdapter: JsonAdapter<T>) : JsonAdapter<T>() {
override fun fromJson(reader: JsonReader): T? {
return elementAdapter.fromJson(reader)
}
override fun toJson(writer: JsonWriter, value: T?) {
elementAdapter.toJson(writer, value)
}
object QuestionResponseAdapterFactory : Factory {
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
if (!annotations.isEmpty()) {
return null // Annotations? This factory doesn't apply.
}
if (type !== QuestionResponse::class.java) {
return null // Not a QuestionResponse This factory doesn't apply.
}
// Handle Type erasure at runtime, this class does not need adapter with single level of generic though
val parameterizedType = Types.newParameterizedType(type, Any::class.java)
val elementAdapter: JsonAdapter<Any> = moshi.adapter(parameterizedType)
return QuestionResponseAdapter(elementAdapter).nullSafe()
}
}
}
In my case I added a similar custom method.
protected inline fun <reified T> convert(value: SomeGenericClass<T>): String {
val parameterizedType = Types.newParameterizedType(SomeGenericClass::class.java, T::class.java)
val adapter = moshi.adapter<SomeGenericClass<T>>(parameterizedType)
return adapter.toJson(value)
}
For instance you want to convert an object of SomeGenericClass<*> to JSON.
#JsonClass(generateAdapter = true)
class SomeGenericClass<T>(
#Json(name = "pages")
val pages: Int = 0,
)
#JsonClass(generateAdapter = true)
data class Book(
#Json(name = "id")
val id: String,
)
val book = SomeGenericClass<Book>(
pages = 100,
author = "Aaa",
...)
Then you should create a Moshi object and call convert(book).
See also 1 and 2.
Related
I have a parameterized base class
#JsonClass(generateAdapter = true)
data class BaseResponse<T>(
#Json(name = "message")
val message: String?,
#Json(name = "data")
val data: T? = null
)
I want to get parse a JSON string and get the message value
private inline fun <reified T> getMessage(): String? {
return try {
val jsonStr = "{\"message\":\"Email or password not provided\"}"
val types = Types.newParameterizedType(
BaseResponse::class.java,
T::class.java
)
val moshiAdapter = Moshi.Builder().build().adapter(types)
val baseResponse = moshiAdapter.fromJson(jsonStr)
baseResponse?.message
} catch (exception: Exception) {
null
}
}
Got compile error at the adapter function
How I call this function
val str = getMessage<Any>()
You're not specifying that you're parsing a BaseResponse, just replace your adapter creation by this
val moshiAdapter = Moshi.Builder().build().adapter<BaseResponse<T>>(types)
Hi i have a normal data class
data class Message(
val code: Int,
val message: String?
)
and the #Parcelize version of it
#Parcelize
data class MessagePresentation(
val code: Int,
val message: String?
): Parcelable
Now how to write a converter so i can map the class 1 to 1?
You can achieve this in multiple ways:
via extension functions -
private fun Message.toMessagePresentation(): MessagePresentation {
return MessagePresentation(code, message)`
}
via instance method -
data class Message(
val code: Int,
val message: String?
) {
fun toMessagePresentation(): MessagePresentation {
return MessagePresentation(code, message)
}
}
via constructor -
#Parcelize
data class MessagePresentation(
val code: Int,
val message: String?
) : Parcelable {
constructor(message: Message) : this(message.code, message.message)
}
Output -
val message = Message(0, "hello")
val messageRepresentation1 = message.toMessagePresentation()
val messageRepresentation2 = message.toMessagePresentation()
val messageRepresentation3 = MessagePresentation(message)
You don't need to create some special logic for converting. The only thing that you should do is create your MessagePresentation object with your Message object's fields.
...
val message = Message(code, message)
val messagePresentation = MessagePresentation(message.code, message.message)
...
For example you can specify additional constructor in MessagePresentation class with Message arg.
Also you can create extension function like this
fun Message.toPresentation(): MessagePresentation =
MessagePresentation(
code = code
message = message
)
I have a Sticker class and its wrapper:
#JsonClass(generateAdapter = true)
class StickerDto(
#Json (name = "totalAnimatedStickers") val total: Int,
#Json(name = "pages") val pages: Int,
#Json(name = "data") val stickers: List<Sticker>
)
#JsonClass(generateAdapter = true)
class Sticker(
#Json(name = "name") val name: String,
#Json(name = "id") val id: String,
#Json(name = "stickerData") val stickerData: JsonObject,
var isSelected:Boolean = false
)
The stickerData attribute comes from the api with a dynamic json object with unknown attributes
"stickerData": {}
How do I deserialize an object like that using Moshi?
My current retrofit client:
private fun createNewFriendsClient(authRefreshClient: AuthRefreshClient,
preferencesInteractor: PreferencesInteractor): FriendsApiClient {
val logger = run {
val httpLoggingInterceptor = HttpLoggingInterceptor()
httpLoggingInterceptor.apply {
httpLoggingInterceptor.level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
}
}
val okHttp = OkHttpClient.Builder().addInterceptor(logger).authenticator(RefreshUserAuthenticator(authRefreshClient, preferencesInteractor,
UnauthorizedNavigator(SDKInternal.appContext, Interactors.preferences))).build()
return Retrofit.Builder()
.client(okHttp)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(MoshiConverterFactory.create())
.baseUrl(Interactors.apiEndpoint)
.build()
.create(FriendsApiClient::class.java)
}
Gives me an
"Unable to create converter for class StickerDto"
Caused by NoJsonAdapter for java.util.Comparator<? super java.lang.String>
error. What converter do I need to use if not that Moshi one? Trying to pull it down as a string also gives an error as it is expecting and object. I just need that string.
Edit, the Json string is very long but it begins like this:
{"tileId":"1264373a-24d8-4c10-ae90-d6e8f671410c","friendId":"2c50f187-039a-4f85-b12b-0c802396a611","name":"David Carey","message":"Joined WeAre8","animatedSticker":{"v":"5.5.7","fr":24,"ip":0,"op":48,"w":1024,"h":1024,"nm":"party_popper","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"C | Position","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[176,892,0],"to":[-6.667,6.667,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":7,"s":[136,932,0],"to":[0,0,0],"ti":[-6.667,6.667,0]},{"t":11,"s":[176,892,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":7,"s":[115,75,100]},{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":11,"s":[95,105,100]},{"t":20,"s":[100,100,100]}],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Controller","np":13,"mn":"Pseudo/DUIK controller","ix":1,"en":1,"ef":[{"ty":6,"nm":"Icon","mn":"Pseudo/DUIK controller-0001","ix":1,"v":0},{"ty":2,"nm":"Color","mn":"Pseudo/DUIK controller-0002","ix":2,"v":{"a":0,"k":[0.92549020052,0.0941176489,0.0941176489,1],"ix":2}},{"ty":3,"nm":"Position","mn":"Pseudo/DUIK controller-0003","ix":3,"v":{"a":0,"k":[0,0],"ix":3}},{"ty":0,"nm":"Size","mn":"Pseudo/DUIK controller-0004","ix":4,"v":{"a":0,"k":100,"ix":4}},{"ty":0,"nm":"Orientation","mn":"Pseudo/DUIK controller-0005
Note that JsonObject is a class from the gson package, so if you want to use Moshi you will need to switch to JSONObject which is the default class supported by Android.
To do this you will need to write your own JSONObject adapter.
First, write your adapter class:
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson
import okio.Buffer
import org.json.JSONException
import org.json.JSONObject
class JSONObjectAdapter {
#FromJson
fun fromJson(reader: JsonReader): JSONObject? {
// Here we're expecting the JSON object, it is processed as Map<String, Any> by Moshi
return (reader.readJsonValue() as? Map<String, Any>)?.let { data ->
try {
JSONObject(data)
} catch (e: JSONException) {
// Handle exception
return null
}
}
}
#ToJson
fun toJson(writer: JsonWriter, value: JSONObject?) {
if (value != null) {
writer.value(Buffer().writeUtf8(value.toString()))
} else {
writer.value(null as String?)
}
}
}
Adjust your retrofit build to provide custom Moshi object when creating the MoshiConverterFactory:
.addConverterFactory(MoshiConverterFactory.create(Moshi.Builder().add(JSONObjectAdapter()).build()))
and then you are good to go and use JSONObject
#Json(name = "stickerData") val stickerData: JSONObject
Good luck and I hope this helps!
Built-in Type Adapters for Moshi include Arrays, Collections, Lists, Sets, and Maps. A JsonObject type is not provided with Moshi itself, but it would be an enhanced Map<String, Any> anyhow, so just use the Map instead of an object.
#JsonClass(generateAdapter = true)
class Sticker(
#Json(name = "name") val name: String,
#Json(name = "id") val id: String,
#Json(name = "stickerData") val stickerData: Map<String, Any>,
var isSelected: Boolean = false
)
The values are automatically converted as well. Thus you'll find strings, lists or numbers there.
I made stickerData into a Map and used a GsonConverterFactory instead of Moshi.
I have made a TypeConverter but I get an error
Unable to create converter for class
.models.lastanime.EpisodesEntityfor method EpisodesApi.getEpisodes
I can't finish understanding how to make the TypeConverter, I have done this, I know that the implementation is placed correctly since I have not had problems in the compilation, but the data does not load since I get an error, and it seems that it is not saved in the room database
TYPE CONVERTER
class ListStringConverter {
#TypeConverter
fun fromString(value: String?): List<ServerEntity> {
val listType = object :
TypeToken<List<ServerEntity?>?>() {}.type
return Gson()
.fromJson<List<ServerEntity>>(value, listType)
}
#TypeConverter
fun listToString(list: List<ServerEntity?>?): String {
val gson = Gson()
return gson.toJson(list)
}
}
MODEL EPISODES ENTITY
data class EpisodesEntity(
#SerializedName("episodes")
val episodes: List<EpisodeEntity>
)
MODEL EPISODE ENTITY
#Entity
data class EpisodeEntity(
#PrimaryKey(autoGenerate = true)
val id: Int,
#SerializedName("poster")
#ColumnInfo(name = "episode")
val episode: Int?,
#SerializedName("poster")
#ColumnInfo(name = "poster")
val poster: String?,
#SerializedName("servers")
#ColumnInfo(name = "servers")
val servers: List<ServerEntity>?,
#SerializedName("title")
#ColumnInfo(name = "title")
val title: String?
)
In addition to all the model, the list of Servers is what gives me trouble inserting it in room
#SerializedName("servers")
#ColumnInfo(name = "servers")
val servers: List<ServerEntity>?,
API REPOSITORY
interface LastEpisodesRepository {
fun lastEpisodes(): Flow<Either<Failure, List<Episode>>>
class Network(
private val networkHandler: NetworkHandler,
private val service: LastEpisodesService,
private val local: EpisodeLocal
) : LastEpisodesRepository {
val preferences by lazy { SharedPrefsHelpers() }
override fun lastEpisodes(): Flow<Either<Failure, List<Episode>>> =
flow {
val days = local.getEpisodes()
val time = preferences.getLong(LocalShared.LastAnimes.lastepisodes, 0L)
if (days.isNullOrEmpty() || time == 0L || isFetchCurrentNeeded(time)) {
emit(getRemoteDay())
} else {
emit(Either.Right(local.getEpisodes().map { it.toEpisode() }))
}
}.catch {
emit(Either.Left(Failure.CustomError(ServiceKOs.DATABASE_ACCESS_ERROR, "DB Error")))
}.flowOn(Dispatchers.IO)
private fun getRemoteEpisode(): Either<Failure, List<Episode>> =
when (networkHandler.isConnected) {
true -> request(
service.getEpisodes(),
{ episodeEntity ->
val episodeList: List<EpisodeEntity> = episodeEntity.episodes
preferences.saveLong(LocalShared.LastAnimes.lastepisodes, Date().time)
addAllEpisodes(episodeList)
episodeList.map { it.toEpisode() }
},
EpisodesEntity(emptyList())
)
false, null -> Either.Left(Failure.NetworkConnection())
}
private fun addAllEpisodes(episodes: List<EpisodeEntity>) {
for (episode in episodes) {
local.addEpisodes(episode)
}
}
}
Room are the calls that are made from the local variable, the application checks if there is downloaded data and if there is not, it calls the service, returns the data and at the same time saves it in the Room database.
After several days carefully studying more about the advanced inserts of Room, I have discovered how to make the TypeConverter for a specific custom object, In my case ServersEntity
#TypeConverter
fun stringToListServer(data: String?): List<ServerEntity?>? {
if (data == null) {
return Collections.emptyList()
}
val listType: Type = object :
TypeToken<List<ServerEntity?>?>() {}.type
return gson.fromJson<List<ServerEntity?>>(data, listType)
}
#TypeConverter
fun listServerToString(someObjects: List<ServerEntity?>?): String? {
return gson.toJson(someObjects)
}
On the other hand to convert the String lists, it would simply be done as follows
#TypeConverter
fun fromString(value: String?): List<String> {
val listType = object :
TypeToken<ArrayList<String?>?>() {}.type
return Gson().fromJson(value, listType)
}
#TypeConverter
fun fromList(list: List<String?>?): String {
val gson = Gson()
return gson.toJson(list)
}
You cannot have an entity holding a List of another entity. You need to define a one-to-many relation between them.
I want to save in my Room database an object where one of the variables can either be of on type or another. I thought a sealed class would make sense, so I took this approach:
sealed class BluetoothMessageType() {
data class Dbm(
val data: String
) : BluetoothMessageType()
data class Pwm(
val data: String
) : BluetoothMessageType()
}
Or even this, but it is not necessary. I found that this one gave me even more errors as it did not know how to handle the open val, so if I find a solution for the first version I would be happy anyway.
sealed class BluetoothMessageType(
open val data: String
) {
data class Dbm(
override val data: String
) : BluetoothMessageType()
data class Pwm(
override val data: String
) : BluetoothMessageType()
}
Then the Entity class
#Entity(tableName = MESSAGES_TABLE_NAME)
data class DatabaseBluetoothMessage(
#PrimaryKey(autoGenerate = true)
val id: Long = 0L,
val time: Long = Instant().millis,
val data: BluetoothMessageType
)
I have created a TypeConverter to convert it to and from a String as well, so I assume that it is not a problem.
First, is this possible? I assume this should function in a similar way that it would with an abstract class, but I have not managed to find a working solution with that either. If it is not possible, what sort of approach should I take when I want to save some data that may be either of one or another type if not with sealed classes?
We faced such problem when we tried using Polymorphism in our domain, and we solved it this way:
Domain:
We have a Photo model that looks like this:
sealed interface Photo {
val id: Long
data class Empty(
override val id: Long
) : Photo
data class Simple(
override val id: Long,
val hasStickers: Boolean,
val accessHash: Long,
val fileReferenceBase64: String,
val date: Int,
val sizes: List<PhotoSize>,
val dcId: Int
) : Photo
}
Photo has PhotoSize inside, it looks like this:
sealed interface PhotoSize {
val type: String
data class Empty(
override val type: String
) : PhotoSize
data class Simple(
override val type: String,
val location: FileLocation,
val width: Int,
val height: Int,
val size: Int,
) : PhotoSize
data class Cached(
override val type: String,
val location: FileLocation,
val width: Int,
val height: Int,
val bytesBase64: String,
) : PhotoSize
data class Stripped(
override val type: String,
val bytesBase64: String,
) : PhotoSize
}
Data:
There is much work to do in our data module to make this happen. I will decompose the process to three parts to make it look easier:
1. Entity:
So, using Room and SQL in general, it is hard to save such objects, so we had to come up with this idea. Our PhotoEntity (Which is the Local version of Photo from our domain looks like this:
#Entity
data class PhotoEntity(
// Shared columns
#PrimaryKey
val id: Long,
val type: Type,
// Simple Columns
val hasStickers: Boolean? = null,
val accessHash: Long? = null,
val fileReferenceBase64: String? = null,
val date: Int? = null,
val dcId: Int? = null
) {
enum class Type {
EMPTY,
SIMPLE,
}
}
And our PhotoSizeEntity looks like this:
#Entity
data class PhotoSizeEntity(
// Shared columns
#PrimaryKey
#Embedded
val identity: Identity,
val type: Type,
// Simple columns
#Embedded
val locationLocal: LocalFileLocation? = null,
val width: Int? = null,
val height: Int? = null,
val size: Int? = null,
// Cached and Stripped columns
val bytesBase64: String? = null,
) {
data class Identity(
val photoId: Long,
val sizeType: String
)
enum class Type {
EMPTY,
SIMPLE,
CACHED,
STRIPPED
}
}
Then we have this compound class to unite PhotoEntity and PhotoSizeEntity together, so we can retrieve all data required by our domain's model:
data class PhotoCompound(
#Embedded
val photo: PhotoEntity,
#Relation(entity = PhotoSizeEntity::class, parentColumn = "id", entityColumn = "photoId")
val sizes: List<PhotoSizeEntity>? = null,
)
2. Dao
So our dao should be able to store and retrieve this data. You can have two daos for PhotoEntity and PhotoSizeEntity instead of one, for the sake of flexibility, but in this example we will use a shared one, it looks like this:
#Dao
interface IPhotoDao {
#Transaction
#Query("SELECT * FROM PhotoEntity WHERE id = :id")
suspend fun getPhotoCompound(id: Long): PhotoCompound
#Transaction
suspend fun insertOrUpdateCompound(compound: PhotoCompound) {
compound.sizes?.let { sizes ->
insertOrUpdate(sizes)
}
insertOrUpdate(compound.photo)
}
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrUpdate(entity: PhotoEntity)
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrUpdate(entities: List<PhotoSizeEntity>)
}
3. Adapter:
After solving the problem of saving data to SQL database, we now need to solve the problem of converting between domain and local entities. Our Photo's converter aka adapter looks like this:
fun Photo.toCompound() = when(this) {
is Photo.Empty -> this.toCompound()
is Photo.Simple -> this.toCompound()
}
fun PhotoCompound.toModel() = when (photo.type) {
PhotoEntity.Type.EMPTY -> Photo.Empty(photo.id)
PhotoEntity.Type.SIMPLE -> this.toSimpleModel()
}
private fun PhotoCompound.toSimpleModel() = photo.run {
Photo.Simple(
id,
hasStickers!!,
accessHash!!,
fileReferenceBase64!!,
date!!,
sizes?.toModels()!!,
dcId!!
)
}
private fun Photo.Empty.toCompound(): PhotoCompound {
val photo = PhotoEntity(
id,
PhotoEntity.Type.EMPTY
)
return PhotoCompound(photo)
}
private fun Photo.Simple.toCompound(): PhotoCompound {
val photo = PhotoEntity(
id,
PhotoEntity.Type.SIMPLE,
hasStickers = hasStickers,
accessHash = accessHash,
fileReferenceBase64 = fileReferenceBase64,
date = date,
dcId = dcId,
)
val sizeEntities = sizes.toEntities(id)
return PhotoCompound(photo, sizeEntities)
}
And for the PhotoSize, it looks like this:
fun List<PhotoSize>.toEntities(photoId: Long) = map { photoSize ->
photoSize.toEntity(photoId)
}
fun PhotoSize.toEntity(photoId: Long) = when(this) {
is PhotoSize.Cached -> this.toEntity(photoId)
is PhotoSize.Empty -> this.toEntity(photoId)
is PhotoSize.Simple -> this.toEntity(photoId)
is PhotoSize.Stripped -> this.toEntity(photoId)
}
fun List<PhotoSizeEntity>.toModels() = map { photoSizeEntity ->
photoSizeEntity.toModel()
}
fun PhotoSizeEntity.toModel() = when(type) {
PhotoSizeEntity.Type.EMPTY -> this.toEmptyModel()
PhotoSizeEntity.Type.SIMPLE -> this.toSimpleModel()
PhotoSizeEntity.Type.CACHED -> this.toCachedModel()
PhotoSizeEntity.Type.STRIPPED -> this.toStrippedModel()
}
private fun PhotoSizeEntity.toEmptyModel() = PhotoSize.Empty(identity.sizeType)
private fun PhotoSizeEntity.toCachedModel() = PhotoSize.Cached(
identity.sizeType,
locationLocal?.toModel()!!,
width!!,
height!!,
bytesBase64!!
)
private fun PhotoSizeEntity.toSimpleModel() = PhotoSize.Simple(
identity.sizeType,
locationLocal?.toModel()!!,
width!!,
height!!,
size!!
)
private fun PhotoSizeEntity.toStrippedModel() = PhotoSize.Stripped(
identity.sizeType,
bytesBase64!!
)
private fun PhotoSize.Cached.toEntity(photoId: Long) = PhotoSizeEntity(
PhotoSizeEntity.Identity(photoId, type),
PhotoSizeEntity.Type.CACHED,
locationLocal = location.toEntity(),
width = width,
height = height,
bytesBase64 = bytesBase64
)
private fun PhotoSize.Simple.toEntity(photoId: Long) = PhotoSizeEntity(
PhotoSizeEntity.Identity(photoId, type),
PhotoSizeEntity.Type.SIMPLE,
locationLocal = location.toEntity(),
width = width,
height = height,
size = size
)
private fun PhotoSize.Stripped.toEntity(photoId: Long) = PhotoSizeEntity(
PhotoSizeEntity.Identity(photoId, type),
PhotoSizeEntity.Type.STRIPPED,
bytesBase64 = bytesBase64
)
private fun PhotoSize.Empty.toEntity(photoId: Long) = PhotoSizeEntity(
PhotoSizeEntity.Identity(photoId, type),
PhotoSizeEntity.Type.EMPTY
)
That's it!
Conclusion:
To save a sealed class to Room or SQL, whether as an Entity, or as an Embedded object, you need to have one big data class with all the properties, from all the sealed variants, and use an Enum type to indicate variant type to use later for conversion between domain and data, or for indication in your code if you don't use Clean Architecture. Hard, but solid and flexible. I hope Room will have some annotations that can generate such code to get rid of the boilerplate code.
PS: This class is taken from Telegram's scheme, they also solve the problem of polymorphism when it comes to communication with a server. Checkout their TL Language here: https://core.telegram.org/mtproto/TL
PS2: If you like Telegram's TL language, you can use this generator to generate Kotlin classes from scheme.tl files: https://github.com/tamimattafi/mtproto
EDIT: You can use this code generating library to automatically generate Dao for compound classes, to make it easier to insert, which removes a lot of boilerplate to map things correctly.
Link: https://github.com/tamimattafi/android-room-compound
Happy Coding!
In my case I did the following:
sealed class Status2() {
object Online : Status2()
object Offline : Status2()
override fun toString(): String {
return when (this) {
is Online ->"Online"
is Offline -> "Offline"
}
}
}
class StatusConverter{
#TypeConverter
fun toHealth(value: Boolean): Status2 {
return if (value){
Status2.Online
} else{
Status2.Offline
}
}
#TypeConverter
fun fromHealth(value: Status2):Boolean {
return when(value){
is Status2.Offline -> false
is Status2.Online -> true
}
}
}
#Dao
interface CourierDao2 {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertStatus(courier: CourierCurrentStatus)
#Query("SELECT * FROM CourierCurrentStatus")
fun getCourierStatus(): Flow<CourierCurrentStatus>
}
#Entity
data class CourierCurrentStatus(
#PrimaryKey
val id: Int = 0,
var status: Status2 = Status2.Offline
)
and it works like a charm