Generics problems encountered by Kotlin and GSON - android

I try to write a network request function,Its role is to analyze the general format and provide data content to callbacks.
This is my defined class:
data class Response<T>(
val code: Int = 0,
#JvmSuppressWildcards
var data: ArrayList<T>,
val message: String = "")
in this class 'code' and 'message' is fixed,'data' has different types
Select one of data:
data class TestData(
#SerializedName("create_by")
val createBy: String = "",
#SerializedName("create_time")
val createTime: String = "",
#SerializedName("name")
val name: String = "",
#SerializedName("id")
val id: String = "")
There is my network request function:
fun <T> post(callBack: (ArrayList<T>) -> Unit) {
...
override fun onSuccess(response:Response<String>) {
val result = gson.fromJson<Response<T>>(response.body().reader(),Response::class.java)
when{
result.code==200-> callBack.invoke(result.data)
}
}
...
}
Use it at Activity:
Request.addr(Constants.GET_TEST)
.post<TestData> {
tv.text = it[0].name
}
When i use Gson parse the server returns data,and want use JavaBean ,Logcat throw this Exception:
java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.example.network.response.TestData
i tried to use TypeToken to solve this problem ,but it also does not work.

It cause, because the parser can't fetch the real type T at runtime.
This extension works for me
inline fun Gson.fromJson(json: String) =
this.fromJson(json, object: TypeToken() {}.type)!!
Then you need modify as in you methods, as IDE recommend you.

Related

How to get Array type data in Firebase in Kotlin?

I have this type of array in firebase but how to fetch it and use in kotlin
I was able to get as String but how to get its as a data class Like this
data class Comment(
val uid: String,
val comment: String,
val stamp: Timestamp
)
and here's the code of getting string
var text by remember { mutableStateOf("loading...") }
FirebaseFirestore.getInstance().collection("MyApp")
.document("Something").get().addOnSuccessListener {
text = it.get("Comments").toString()
}
Firebase has a toObject method that can be used to turn your document into a custom object.
db.collection("Comments")
.get()
.addOnSuccessListener { documents ->
for (document in documents) {
val comment = document.toObject<Comment>()
}
}
The Comment data class should also define default values. So, it should be like...
data class Comment(
val uid: String = "",
val comment: String = "",
#ServerTimeStamp val stamp: Date? = null
)
I got ArrayLists with HashMaps represents my entities entities just using this:
val cleanAnswers: List<Answer> = (document.get(FIELD_ANSWERS)
as ArrayList<HashMap<String, Any>>).map {
Answer(
it[FIELD_VARIANT] as String,
it[FIELD_IS_CORRECT] as Boolean
)
}
My entity:
class Answer(val answer: String,
val isCorrect: Boolean) : Serializable

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
}
}

Is it possible to same key with differ class and data type in data class kotlin android?

I have one issue about code data class kotlin android.
How to implement server response? sometimes I get String value or sometime get Object class.
class CMSRespTemp {
data class CMSRespApi(
val status: Boolean = false,
val message: String = "",
val data: String as Data
)
data class Data(
val cms_id: String = "",
val cms_content: String = ""
)
}
When I implement only Data class it works, like this val data: Data or val data: String. But I need together Data and String with key only data.
Is it possible?
When having multiple type for same variable, we can use Any type which is equivalent to Object type in java. So solution is like below :
class CMSRespTemp {
data class CMSRespApi(
val status: Boolean = false,
val message: String = "",
var data: Any? = null // changed it to var from val, so that we can change it's type runtime if required
)
data class Data(
val cms_id: String = "",
val cms_content: String = ""
)
}
And when accessing that variable, one can simply cast like below :
val apiResponse : CMSRespApi //= some API response here from network call
when (apiResponse.data) {
is String -> {
// apiResponse.data will be smart-casted to String here
}
else -> {
val responseData = Gson().fromJson<CMSRespApi.Data>(
Gson().toJsonTree(apiResponse.data),
CMSRespApi.Data::class.java
)
}
}
After 12 Hrs spend and got the solution my self,
val getResultCon = getSerCont.result // response Any
val gson = Gson()
val jsonElement = gson.toJsonTree(getResultCon)
val resultData = gson.fromJson(jsonElement, SearchContactApi.Result::class.java)
Convert your data string to toJsonTree and fromJson with model class then got result.

how to parse data from json

i tried to parse json data, but it kind of weird because it not show the right data but if i tried to called json on browser it has the right data.
so this is how i parse the json data
doAsync {
val url = localhost.getMovie()
val request = okhttp3.Request.Builder().url(url).build()
val client = OkHttpClient()
uiThread {
client.newCall(request).enqueue(object : Callback, okhttp3.Callback {
override fun onResponse(call: okhttp3.Call?, response: okhttp3.Response?) {
val body = response?.body()?.string()
println(body)
uiThread {
val gson = GsonBuilder().create()
val movieFeed = gson.fromJson(body, Movie2Response::class.java)
Log.v("body", ""+body)
Log.v("feed", ""+movieFeed.data)
uiThread {
}
}
}
override fun onFailure(call: okhttp3.Call?, e: IOException) {
println("failed")
}
})
}
}
movie response
class Movie2Response (val data: MutableList<Movie2>)
movie
class Movie2 (
#SerializedName("id")
var movieId: String? = null,
#SerializedName("description")
var synopsis: String? = null,
#SerializedName("release_date")
var release: String? = null,
#SerializedName("poster")
var poster: String? = null,
#SerializedName("genre")
var genre: String? = null,
#SerializedName("title")
var title: String? = null
)
and this is what i got from the json data
V/body: {"data":[{"title":"Aquaman","description":""........
V/feed: [com.mqa.android.moviereview.model.Movie2#7509e04, com.mqa.android.moviereview.model.Movie2#890afed, com.mqa.android.moviereview.model.Movie2#9834e22, com.mqa.android.moviereview.model.Movie2#f02d0b3, com.mqa.android.moviereview.model.Movie2#d3b9670, com.mqa.android.moviereview.model.Movie2#4d55de9, com.mqa.android.moviereview.model.Movie2#cac2a6e, com.mqa.android.moviereview.model.Movie2#94fc50f, com.mqa.android.moviereview.model.Movie2#d9ba99c]
it shows right in body but in the array it show like that. please help what is wrong with it. because i want to show the title data to the spinner
Your code worked pretty well as the log results showed. The real problem is the log function Log.v("feed", ""+movieFeed.data). If you want to show pretty log, you should override the toString() method in Movie2 class by:
Open Movie2 and right click in the editor -> Generate -> then click toString() to override it.
For data class in Kotlin, you can just add data before class keyword.
Everything okey with you data. You just forgot adding default realisation to log this object.
class Movie2(/*your fields*/)
just add data before class. will be something like that
data class Movie2(/*your fields*/)
Kotlin doesn't know ho toString you Movie2. If you wanna default realisation use data class

Deserialize a Firebase Data-Snapshot to a Kotlin data class

Hi I have a Kotlin data class as follows
data class User (
#get:Exclude val gUser: Boolean,
#get:Exclude val uid: String,
#get:PropertyName("display_name") val displayName: String,
#get:PropertyName("email") val email: String,
#get:PropertyName("account_picture_url") val accountPicUrl: String,
#get:PropertyName("provider") val provider: String
)
I am able to serialize the object without an issues. But i'm having trouble deserializing the object when doing a firebase query. Currently this is what i'm doing to get the data
_firebaseReference.child(getString(R.string.firebase_users_key)).child(user.uid)
.setValue(user).addOnCompleteListener{
_firebaseReference.child("users").child(user.uid)
.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onCancelled(p0: DatabaseError) {
}
override fun onDataChange(p0: DataSnapshot) {
if (p0.exists()) {
val userHash = p0.value as HashMap<*, *>
var currentUser: User
if (userHash[getString(R.string.provider_key)]
!= getString(R.string.provider_google)) {
currentUser = User(false, p0.key!!,
userHash["display_name"].toString(),
userHash["email"].toString(),
userHash["account_picture_url"].toString(),
userHash["provider"].toString())
} else {
currentUser = User(true, p0.key!!,
userHash["display_name"].toString(),
userHash["email"].toString(),
userHash["account_picture_url"].toString(),
userHash["provider"].toString())
}
}
}
})
}
This is only a test project that i'm working on to practice my Kotlin, but this is something I would like to figure out.
If i'm doing it completely wrong please let me know, any advise would be greatly appreciated
Thanks
Firebase needs an empty constructor to be able to deserialize the objects:
data class User(
#Exclude val gUser: Boolean,
#Exclude val uid: String,
#PropertyName("display_name") val displayName: String,
#PropertyName("email") val email: String,
#PropertyName("account_picture_url") val accountPicUrl: String,
#PropertyName("provider") val provider: String
) {
constructor() : this(false, "", "", "", "", "")
}
You can either declare it like so and provide some default values to be able to call the primary constructor or you can declare default values for all your parameters:
data class User (
#Exclude val gUser: Boolean = false,
#Exclude val uid: String = "",
#PropertyName("display_name") val displayName: String = "",
#PropertyName("email") val email: String = "",
#PropertyName("account_picture_url") val accountPicUrl: String = "",
#PropertyName("provider") val provider: String = ""
)
Then various constructors will be created for you, including an empty constructor.
If there's a problem with serialization there might be because of the getters and setters generated by the ide, try reinforcing them with #get and #set annotations:
data class User (
#Exclude val gUser: Boolean = false,
#Exclude val uid: String = "",
#set:PropertyName("display_name")
#get:PropertyName("display_name")
var displayName: String = "",
#PropertyName("email") val email: String = "",
#set:PropertyName("account_picture_url")
#get:PropertyName("account_picture_url")
var accountPicUrl: String = "",
#PropertyName("provider") val provider: String = ""
)
What I actually wanted is a Kotlin data class which is derived from a domain model interface like so
data class Dto(#PropertyName("serialized_title") val override title: String) : DomainModel
In this case DomainModel is defined this way
interface DomainModel { val title: String }
My goal was to fetch data from Firestore and get deserialized Dto objects which are provided to clients which receive objects of type DomainModel. So this solution above unfortunately didn't work. I saw the workarounds using #get: and #set: Annotations but I wanted my data class properties to be immutable. Simply using vars is a bad design decision in my use case. And also this solution looks quite ugly...
After inspecting the decompiled Java-Code I came up with this solution
data class Dto(
#field:[JvmField PropertyName("serialized_title")]
override val title: String = "") : DomainModel
The decompiled Java-Code simply uses title as public final field having the PropertyName annotation.
I prefer this solution since it doesn't violate certain design decisions I made...
In Android Studio (kotlin)
use this (only var and getter and setter):
#set:PropertyName("email") #get:PropertyName("email") var emailPerson: String = ""
None of this works:
#PropertyName("email") var emailPerson: String = ""
#PropertyName("email") val emailPerson: String = ""
#get:PropertyName("email") val emailPerson: String = ""
Android Studio 4.1.2. Gradle: com.google.firebase:firebase-database:19.6.0

Categories

Resources