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
)
Related
I'm learning Compose by the article.
The article tell me:
All data types that are added to the Bundle are saved automatically. The simplest solution is to add the #Parcelize annotation to the object.
And it gives me the sample code.
#Parcelize
data class City(val name: String, val country: String) : Parcelable
#Composable
fun CityScreen() {
var selectedCity = rememberSaveable {
mutableStateOf(City("Madrid", "Spain"))
}
}
The following code is from the project.
I find the data class Post doesn't add #Parcelize, and val featured = remember { PostRepo.getFeaturedPost() } is OK, why?
#Composable
fun Home() {
val featured = remember { PostRepo.getFeaturedPost() }
val posts = remember { PostRepo.getPosts() }
...
}
object PostRepo {
fun getPosts(): List<Post> = posts
fun getFeaturedPost(): Post = posts.random()
}
#Immutable
data class Post(
val id: Long,
val title: String,
val subtitle: String? = null,
val url: String,
val metadata: Metadata,
#DrawableRes val imageId: Int,
#DrawableRes val imageThumbId: Int,
val tags: Set<String>
)
#Immutable
data class Metadata(
val author: PostAuthor,
val date: String,
val readTimeMinutes: Int
)
#Immutable
data class PostAuthor(
val name: String,
val url: String? = null
)
While remember helps you retain state across recompositions, the state is not retained across configuration changes. For this, you use rememberSaveable. RememberSaveable automatically saves any value that can be saved in a Bundle. For other values, you can pass in a custom saver object.
Since RememberSaveable automatically saves any value that can be saved in a Bundle , it requires the class to be Parcelable unlike remember .
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.
I am reading in the following JSON and using GSON to convert it to an object which I can then use to access the properties of said object to help display the images in my app.
However one of the field I want to use, imageURL, is returning null values. Looking at the json (link below) we can clearly see that it is not null.
https://api.letsbuildthatapp.com/youtube/home_feed
I have used the debugger to demonstrate the null value I am getting for each imageURL:
Debugger output
So the object is giving me null values for the imageURL but the link is not. What is going on?
Here is the function I wrote to fetch and parse the JSON object:
private fun fetchJson() {
println("Attempting to fetch JSON")
val url = "https://api.letsbuildthatapp.com/youtube/home_feed"
val request = Request.Builder().url(url).build()
val client = OkHttpClient()
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
val body = response.body?.string()
println(body)
val gson = GsonBuilder().create()
val homeFeed = gson.fromJson(body, HomeFeed::class.java)
runOnUiThread {
recyclerView_main.adapter = MainAdapter(homeFeed)
}
}
override fun onFailure(call: Call, e: IOException) {
println("failed to execute request")
}
}
)
}
My HomeFeed class is like this:
class HomeFeed(val videos: List<Video>)
class Video(val id: Int, val name: String, val link: String, val imageURL: String, numberOfViews: Int,
val channel: Channel)
class Channel(val name: String, val profileImageUrl: String)
I believe this should be detailed enough but if anymore info is needed please let me know.
Thank you.
Try with this object, you should use the same name for the vals, "imageUrl" instead of "imageURL":
data class HomeFeed(
val user: User,
val videos: List<Video>
)
data class User(
val id: Int,
val name: String,
val username: String
)
data class Video(
val channel: Channel,
val id: Int,
val imageUrl: String,
val link: String,
val name: String,
val numberOfViews: Int
)
data class Channel(
val name: String,
val numberOfSubscribers: Int,
val profileImageUrl: String
)
I have 4 different entities with the same type of data..
class {
val Int
val String
val String
}
I am using ViewModel to request the data and right now I have for Observables which updates the list adapter.
tagsViewModel.getAllText().observe(this,
Observer<List<Text>> { t -> adapter.setTags(t!!) })
My problem is that I am having some troubles when updating the ui so I wanted to just do one request to get the 4 different types of entities but I don't know how to get one only list with all different classes.
This is a class type
#Entity(tableName = "text")
data class Text(override var content: String, override var date: Long, override var type: String = AppConstants.TYPE_TEXT) : BaseTag() {
#PrimaryKey(autoGenerate = true)
override var id: Int = 0
}
and Base interface
abstract class BaseTag {
abstract val content: String?
abstract val date: Long?
abstract val id: Int?
abstract val type: String?
}
I would like to do this request:
{ texts : [Text,Text,Text],
emails : [Email,Email,Email]...
}
So... Is there any simple way to do this?
Thanks for any help.
I read topics like Android GSON deserialize delete empty arrays, https://medium.com/#int02h/custom-deserialization-with-gson-1bab538c0bfa, How do you get GSON to omit null or empty objects and empty arrays and lists?.
For instance, I have a class:
data class ViewProfileResponse(
val success: Int,
val wallets: List<Wallet>?,
val contact: Contact?,
val code: Int,
val errors: ErrorsResponse?
) {
data class Contact(
val countryId: Int,
val regionId: Int,
val cityId: Int
)
data class Wallet(
val id: Int,
val currency: String?,
val createTime: Int,
val balance: Float,
val bonusWallet: BonusWallet?
) {
data class BonusWallet(val id: Int, val balance: Float)
}
data class ErrorsResponse(val common: List<String>?)
class Deserializer : ResponseDeserializable<ViewProfileResponse> {
override fun deserialize(content: String): ViewProfileResponse? =
Gson().fromJson(content, ViewProfileResponse::class.java)
}
}
As you see, I have a complex class with subclasses, any of each can be null. But instead of sending null or {} in these fields, a server sends [] in JSON.
I mean, instead of "contact": null I get "contact": [].
How to write a custom deserializer for Gson? So that empty arrays could be removed, but other types retain. I have tens of those classes.
A temporary solution is based on https://stackoverflow.com/a/54709501/2914140.
In this case Deserializer will look like:
class Deserializer : ResponseDeserializable<ViewProfileResponse> {
private val gson = Gson()
private val gsonConverter = GsonConverter()
override fun deserialize(content: String): ViewProfileResponse? =
gson.fromJson(gsonConverter.cleanJson(content), ViewProfileResponse::class.java)
}