KotlinX Serialization of Built-in Classes - json

Kotlinx docs don't seem to cover this, I am wondering if there is a way to write a custom Serializer for an existing class. In my case, I need to serialize PointF and Color. For PointF, I have done this:
object MyPointFAsStringSerializer : KSerializer<MyPointF>
{
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("Point") {
element<Float>("x")
element<Float>("y")
}
override fun serialize(encoder: Encoder, value: MyPointF) =
encoder.encodeStructure(descriptor) {
encodeFloatElement(descriptor, 0, (value.x))
encodeFloatElement(descriptor, 1, (value.y))
}
override fun deserialize(decoder: Decoder): MyPointF =
decoder.decodeStructure(descriptor) {
var x = -1f
var y = -1f
while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> x = decodeFloatElement(descriptor, 0)
1 -> y = decodeFloatElement(descriptor, 1)
CompositeDecoder.DECODE_DONE -> break
else -> error("Unexpected index: $index")
}
}
MyPointF(x, y)
}
}
#Serializable(with = MyPointFAsStringSerializer::class)
class MyPointF()
{
var x: Float = Float.MAX_VALUE
var y: Float = Float.MAX_VALUE
constructor(x: Float, y: Float) : this()
{
this.x = x
this.y = y
}
fun pointF () : PointF = PointF(this.x, this.y)
}
However, this approach is forcing me to go back and forth between PointF and MyPointF. I wondering how I could attach the same serializer to an existing class (i.e., PointF). In Swift, I simply do this by using encodeToString, which practically tells the compiler what to do with this kind of object when serializing. But I can't seem to find a way for Kotlin. Any suggestion would be appreciated.

It's definitely possible to serialize 3rd-party classes with kotlinx serialization. You can find the docs here
object PointFAsStringSerializer : KSerializer<PointF> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("Point") {
element<Float>("x")
element<Float>("y")
}
override fun serialize(encoder: Encoder, value: PointF) =
encoder.encodeStructure(descriptor) {
encodeFloatElement(descriptor, 0, (value.x))
encodeFloatElement(descriptor, 1, (value.y))
}
override fun deserialize(decoder: Decoder): PointF =
decoder.decodeStructure(descriptor) {
var x = -1f
var y = -1f
while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> x = decodeFloatElement(descriptor, 0)
1 -> y = decodeFloatElement(descriptor, 1)
CompositeDecoder.DECODE_DONE -> break
else -> error("Unexpected index: $index")
}
}
PointF(x, y)
}
}
fun main() {
println(Json.encodeToString(PointFAsStringSerializer, PointF(1.1f, 2.2f)))
}

Related

com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException

I am facing such an error in my application. I guess the problem is due to having char in note_title and note_desc. I couldn't find the solution. Is there anyone who can help?
navgraph
error
NoteDetailScreen
notes entity
? and other char cause error
I tried change note_title and note_desc types but didnt work.
I solved this way;
Let’s say you have a class like this:
#Parcalize
#Entity(tableName = "NOTES")
data class Notes(
#PrimaryKey(autoGenerate = true)
#ColumnInfo("note_id") #NotNull var note_id:Int,
#ColumnInfo("note_title") #NotNull var note_title: String,
#ColumnInfo("note_desc") #NotNull var note_desc: String,
#ColumnInfo("note_date") #Nullable var note_date: String?
): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readInt(),
parcel.readString().toString(),
parcel.readString().toString(),
parcel.readString()
) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(note_id)
parcel.writeString(note_title)
parcel.writeString(note_desc)
parcel.writeString(note_date)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Notes> {
override fun createFromParcel(parcel: Parcel): Notes {
return Notes(parcel)
}
override fun newArray(size: Int): Array<Notes?> {
return arrayOfNulls(size)
}
}
}
annotation class Parcalize
You can define the NavType like this:
class NavTypo : NavType<Notes>(isNullableAllowed = false) {
override fun get(bundle: Bundle, key: String): Notes? {
return bundle.getParcelable(key)
}
override fun parseValue(value: String): Notes {
return Gson().fromJson(value, Notes::class.java)
}
override fun put(bundle: Bundle, key: String, value: Notes) {
bundle.putParcelable(key, value)
}
}
And use it:
composable(
"note_details_page/{note_id}",
arguments = listOf(
navArgument("note_id"){
type = NavTypo()
}
)
){
val note = it.arguments?.getParcelable<Notes>("note_id")
if (note != null) {
NoteDetailScreen(note, navController)
}
}
Card(
backgroundColor = choosedColor,
modifier = Modifier
.padding(3.dp)
.sizeIn(maxHeight = 250.dp)
.combinedClickable(onClick={
val note = allNotes.value!![it]
val noteJson = Uri.encode(Gson().toJson(note))
navController.navigate("note_details_page/${noteJson}")
}

How do you serialize a list of BufferedImage in Kotlin?

I'm trying to implement a protocol where (part of it) is sending a list of small images over a socket. I'm using JSON and the images are base64 encoded.
Here's the data classes
#Serializable
sealed class CmdBase {
abstract val cmd: Command
}
#Serializable
#SerialName("CmdIdImgs")
class CmdIdImgs(
override val cmd: Command,
val id: String,
#Serializable(with = ImageListSerializer::class)
val thumbnails: List<BufferedImage>) : CmdBase()
So I added a serializer for BufferedImage
object ImageSerializer: KSerializer<BufferedImage> {
override val descriptor = PrimitiveSerialDescriptor("Image.image", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): BufferedImage {
val b64str = decoder.decodeString()
return ImageIO.read(ByteArrayInputStream(Base64.getDecoder().decode(b64str)))
}
override fun serialize(encoder: Encoder, value: BufferedImage) {
val buff = ByteArrayOutputStream()
ImageIO.write(value, "PNG", buff)
val b64str = Base64.getEncoder().encodeToString(buff.toByteArray())
encoder.encodeString(b64str)
}
}
But it's a list of BufferedImages, so I added a serializer for that
class ImageListSerializer: KSerializer<List<BufferedImage>> {
private val listSerializer = ListSerializer(ImageSerializer)
override val descriptor: SerialDescriptor = listSerializer.descriptor
override fun serialize(encoder: Encoder, value: List<BufferedImage>) {
listSerializer.serialize(encoder, value)
}
override fun deserialize(decoder: Decoder): List<BufferedImage> = with(decoder as JsonDecoder) {
decodeJsonElement().jsonArray.mapNotNull {
try {
json.decodeFromJsonElement(ImageSerializer, it)
} catch (e: SerializationException) {
e.printStackTrace()
null
}
}
}
}
And now a serializer for the whole class
object CmdIdImgsSerializer : SerializationStrategy<CmdIdImgs>, DeserializationStrategy<CmdIdImgs> {
override val descriptor = buildClassSerialDescriptor("CmdIdImgs") {
element("cmd", Command.serializer().descriptor)
element("id", String.serializer().descriptor)
element("thumbnails", ImageListSerializer().descriptor)
}
override fun serialize(encoder: Encoder, value: CmdIdImgs) {
encoder.encodeStructure(descriptor) {
encodeSerializableElement(descriptor, 0, Command.serializer(), value.cmd)
encodeSerializableElement(descriptor, 1, String.serializer(), value.id)
encodeSerializableElement(descriptor, 2, ImageListSerializer(), value.thumbnails)
}
}
override fun deserialize(decoder: Decoder): CmdIdImgs =
decoder.decodeStructure(descriptor) {
var cmd: Command = Command.FULL_TREE
var id: String = ""
var thumbnails: List<BufferedImage> = listOf()
loop# while (true) {
when (val i = decodeElementIndex(descriptor)) {
0 -> cmd = decodeSerializableElement(descriptor, i, Command.serializer())
1 -> id = decodeSerializableElement(descriptor, i, String.serializer())
2 -> thumbnails = decodeSerializableElement(descriptor, i, ImageListSerializer())
CompositeDecoder.DECODE_DONE -> break
else -> throw SerializationException("Unknown index $i")
}
}
CmdIdImgs(cmd, id, thumbnails)
}
}
But something is wrong, because I still get
Serializer has not been found for type 'BufferedImage'
on the 'val thumbnails: List<BufferedImage>' in the CmdIdImgs class
Any idea what I'm doing wrong?
Probably a lot since I'm a newbie with Kotlin :-)
Since you want to send JSON to your socket, I recommend you use de facto Jackson. If that's ok for you, then this is simpler - you only need to create one specialised serializer. Here's working code (deserializer TODO).
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.fasterxml.jackson.module.kotlin.KotlinModule
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.*
import javax.imageio.ImageIO
sealed class CmdBase {
abstract val cmd: String // Command
}
class CmdIdImgs(
override val cmd: String, // Command
val id: String,
val thumbnails: List<BufferedImage>,
) : CmdBase()
class BufferedImageSerializer : StdSerializer<BufferedImage>(BufferedImage::class.java) {
override fun serialize(value: BufferedImage?, jgen: JsonGenerator, provider: SerializerProvider?) {
value?.let {
val buff = ByteArrayOutputStream()
ImageIO.write(it, "PNG", buff)
val b64str = Base64.getEncoder().encodeToString(buff.toByteArray())
jgen.writeString(b64str)
}
}
}
//class BufferedImageDeserializer : StdDeserializer<BufferedImage>(BufferedImage::class.java) {
// override fun deserialize(jp: JsonParser, ctxt: DeserializationContext?): BufferedImage? {
// val node: JsonNode = jp.codec.readTree(jp)
// if (!node.isTextual) {
// node.asText()....
// }
// }
//}
val IMAGE_MODULE = SimpleModule().apply {
this.addSerializer(BufferedImage::class.java, BufferedImageSerializer())
//this.addDeserializer(BufferedImage::class.java, BufferedImageDeserializer())
}
val MAPPER = JsonMapper.builder()
.addModule(KotlinModule(strictNullChecks = true))
.addModule(IMAGE_MODULE)
.build()
fun main(args: Array<String>) {
val cmdIdImgs = CmdIdImgs("x", "1", listOf(ImageIO.read(File("/tmp/image.png"))))
println(MAPPER.writeValueAsString(cmdIdImgs))
}
Prints
{"cmd":"x","id":"1","thumbnails":["iVBORw0KGgoAAAAN....

How can I update seekBar by seconds on Media Player?(kotlin)

package com.example.ebookactivity
import android.media.MediaPlayer
import android.os.Bundle
import android.os.Handler
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.example.ebookactivity.databinding.FragmentDay1Binding
import android.widget.SeekBar
import android.os.SystemClock
import android.widget.TextView
import androidx.annotation.Dimension
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
/**
* A simple [Fragment] subclass.
* Use the [day_1.newInstance] factory method to
* create an instance of this fragment.
*/
class day_1 : Fragment() {
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
private var _binding: FragmentDay1Binding? = null
private val binding get() = _binding!!
private var mediaPlayer: MediaPlayer? = null
private val textsize_plus: Float = 10.0f
private lateinit var runnable: Runnable
private lateinit var handler: Handler
private lateinit var duration: TextView
private val j:Int = 0
var size:Float = 70.0f
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
if(mediaPlayer?.isPlaying == true) {
//seekbar()
binding.textView2.text = "tankyou"
binding.seekBar.setProgress(mediaPlayer!!.currentPosition)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentDay1Binding.inflate(inflater,container,false)
if (mediaPlayer?.isPlaying == true) {
binding.seekBar.progress = mediaPlayer!!.currentPosition
}
binding.button1.setOnClickListener {
size -= textsize_plus
binding.gangu.setTextSize(Dimension.DP,size)}
binding.buttonplus.setOnClickListener {
size += textsize_plus
binding.gangu.setTextSize(Dimension.DP,size)}
//seekbar()
binding.startbutton.setOnClickListener {
if(mediaPlayer == null) {
mediaPlayer = MediaPlayer.create(activity, R.raw.days_1)
binding.seekBar.max = mediaPlayer!!.duration
binding.seekBar.setProgress(mediaPlayer!!.currentPosition)
// seekbar()
}
mediaPlayer?.start()
}
binding.pausebutton.setOnClickListener {
if (mediaPlayer?.isPlaying == true) {
mediaPlayer?.pause()
binding.seekBar.setProgress(mediaPlayer!!.currentPosition)
} else {
mediaPlayer?.start()
}
}
binding.stopbutton.setOnClickListener {
mediaPlayer?.stop()
mediaPlayer=null
}
fun onStop() {
super.onStop()
mediaPlayer?.release()
}
binding.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) {
if (b) {
mediaPlayer?.seekTo(i/* * 1000*/)
// seekbar()
if (mediaPlayer?.isPlaying == true) {
binding.seekBar.setProgress(mediaPlayer!!.currentPosition)
}
}
binding.textView4.text = "ok"
if (mediaPlayer?.isPlaying == true) {
binding.seekBar.setProgress(mediaPlayer!!.currentPosition)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
}
}
)
return binding.root
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* #param param1 Parameter 1.
* #param param2 Parameter 2.
* #return A new instance of fragment day_1.
*/
// TODO: Rename and change types and number of parameters
#JvmStatic
fun newInstance(param1: String, param2: String) =
day_1().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
override fun onPause() {
super.onPause()
mediaPlayer?.stop()
}
fun seekbar() {
binding.seekBar.setProgress(mediaPlayer!!.currentPosition)
val min2: Int = mediaPlayer!!.duration / 60000
val sec2: Int = (mediaPlayer!!.duration - min2 * 60000) / 1000
val min: Int = mediaPlayer!!.currentPosition / 60000
val sec: Int = (mediaPlayer!!.currentPosition - min * 60000) / 1000
binding.textView2.text = "" + min + " : " + sec + ""
binding.textView4.text = "" + min2 + " : " + sec2 + ""
runnable = Runnable { seekbar() }
handler.postDelayed(runnable, 1000)
}
}
I want to update seekBar by seconds. but No matter where I put setProgress code, it isn't working. SetProgress is just working on ButtonClick in this code. I want to make seekbar update by seconds. so I make seekbar function. but it isn't work. If I put seekbar function any places, application would be broken. So I cannot resolve this problem. Please help me...

Make JsonDeserializer global effect

When deserialize json to Map<out Any, Any>, gson will use Double to fill the Map, even the field is int number, so I use a MapDeserializerDoubleAsIntFix to covert number to int if it is possible.
{
"person":{
"name":"jack",
"age":24,
"height":174.5
}
}
class MapDeserializerDoubleAsIntFix: JsonDeserializer<Map<out Any, Any>> {
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): Map<out Any, Any>? {
return deserialize(json) as Map<out Any, Any>
}
private fun deserialize(jsonElement: JsonElement): Any? {
when {
jsonElement.isJsonArray -> {
val list: MutableList<Any?> = ArrayList()
val arr = jsonElement.asJsonArray
for (anArr in arr) {
list.add(deserialize(anArr))
}
return list
}
jsonElement.isJsonObject -> {
val map: MutableMap<String, Any?> = LinkedTreeMap()
val obj = jsonElement.asJsonObject
val entitySet = obj.entrySet()
for ((key, value) in entitySet) {
map[key] = deserialize(value)
}
return map
}
jsonElement.isJsonPrimitive -> {
val prim = jsonElement.asJsonPrimitive
when {
prim.isBoolean -> {
return prim.asBoolean
}
prim.isString -> {
return prim.asString
}
prim.isNumber -> {
// Here is what i do
// use int or long if it is possible
val numStr = prim.asString
return if (numStr.contains(".")) {
prim.asDouble
} else {
val num = prim.asNumber
val numLong = num.toLong()
return if (numLong < Int.MAX_VALUE && numLong > Int.MIN_VALUE) {
numLong.toInt()
} else {
numLong
}
}
}
}
}
}
return null
}
}
object MapTypeToken: TypeToken<Map<out Any, Any>>()
private val GSON = GsonBuilder()
.registerTypeAdapter(MapTypeToken.type, MapDeserializerDoubleAsIntFix())
.create()
And when I use GSON deserialize json as map, it works.
val map: Map<out Any, Any> = GSON.fromJson(json, MapTypeToken.type)
But when the map is in a data class as a field, the MapDeserializerDoubleAsIntFix not work.
data class Test(
val person: Map<out Any, Any>
)
val test = GSON.fromJson(json, Test::class.java)
So is there any way to deserialize map or filed map ?
Now I use this to solve.
class TestJsonDeserializer: JsonDeserializer<Test> {
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext,
): Test {
val jsonObject = json.asJsonObject
val personElement = jsonObject.get("person")
val person = context.deserialize<Map<out Any, Any>>(personElement, MapTypeToken.type)
return Test(person)
}
}
private val GSON = GsonBuilder()
.registerTypeAdapter(MapTypeToken.type, MapDeserializerDoubleAsIntFix())
.registerTypeAdapter(Test::class.java, TestJsonDeserializer())
.create()
But if another data class has map, I have to write another JsonDeserializer and register it.
Hope there is a better way to make the MapDeserializerDoubleAsIntFix work as global.

Asynchronous call in Kotlin not working with RecycleView

I'm struggling with asynchronous calls - the app is just crashing. I want to load a JSON-file (containing 100 JSON-objects) from an URL and then send it to RecyclerView.
Here is the MainActivity-class:
class MainActivity : AppCompatActivity() {
lateinit var recyclerView: RecyclerView
lateinit var linearLayoutManager: LinearLayoutManager
private val url = [//some address here]
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = linearLayoutManager
AsyncTaskHandler().execute(url)
}
inner class AsyncTaskHandler : AsyncTask<String, String, String>() {
override fun onPreExecute() {
super.onPreExecute()
}
override fun doInBackground(vararg url: String?): String {
val text: String
val connection = URL(url[0]).openConnection() as HttpURLConnection
try {
connection.connect()
text = connection.inputStream.use { it.reader().use {reader -> reader.readText()} }
} finally {
connection.disconnect()
}
return text
}
override fun onPostExecute(result: String?) {
super.onPostExecute(result)
handleJson(result)
}
}
private fun handleJson(jsonString: String?) {
val jsonArray = JSONArray(jsonString)
var list = mutableListOf<DataSet>()
var i = 0
while (i < jsonArray.length()) {
val jsonObject = jsonArray.getJSONObject(i)
list.add(DataSet(
jsonObject.getString("title"),
jsonObject.getString("type")
))
i++
}
val adapter = Adapter(list)
recyclerView.adapter = adapter
}
}
...and ListAdapter-class:
class Adapter (private var targetData: MutableList<DataSet>): RecyclerView.Adapter<ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.element, parent, false)
return ViewHolder(v);
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = targetData[position]
holder.title?.text = item.title
holder.type?.text = item.type
}
override fun getItemCount(): Int {
return targetData.size
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var title = itemView.findViewById<TextView>(R.id.itemTitle)
var type = itemView.findViewById<TextView>(R.id.itemType)
}
What might be the problem here? Is there any better option to perform this?