【Kotlin】kotlinx.serialization で JSON を自在に変換する 🤔

 

🤔 Kotlin Serialization Guide を眺めてみる

JSON 処理なら、ここらですよね。

どんな扱いになっているのか見てみましょうか。


👉 kotlinx.serialization/serialization-guide.md at master · Kotlin/kotlinx.serialization · GitHub hatena-bookmark

基本的な使い方「Chapter 1.Basic Serialization」として、以下のようなデータクラスを例に以下のサンプルコード。


@Serializable
class Project(val name: String, val language: String)


val data = Project("kotlinx.serialization", "Kotlin")
println(Json.encodeToString(data))

// {"name":"kotlinx.serialization","language":"Kotlin"}


val data = Json.decodeFromString<Project>("""
  {"name":"kotlinx.serialization","language":"Kotlin"}
""")
println(data)

// Project(name=kotlinx.serialization, language=Kotlin)

以下のようなイメージのことを書いてる。


                +-----------------------------------+                 
                |                                   |                 
                | Project(                          |                 
                |   name = "kotlinx.serialization", |                 
                |   language = "Kotlin"             |                 
                | )                                 |                 
                |                                   |                 
                +-----------------------------------+                 
                              |      ^                                
                              |      |                                
                              |      |                                
Json.encodeToString(Project)  |      |  Json.decodeFromString(String) 
                              |      |                                
                              |      |                                
                              v      |                                
               +------------------------------------+                 
               |                                    |                 
               | {                                  |                 
               |   "name":"kotlinx.serialization",  |                 
               |   "language":"Kotlin"              |                 
               | }                                  |                 
               |                                    |                 
               +------------------------------------+

汎用的にしてこうですか。


            +-----------------------------------+                          
            |                 T                 |                          
            +-----------------------------------+                          
                            |  ^                                           
                            |  |                                           
                            |  |                                           
Json.encodeToString(T)      |  |     Json.decodeFromString<T>(String)      
                            |  |                                           
                            |  |                                           
                            v  |                                           
           +------------------------------------+                          
           |               String               |                          
           +------------------------------------+

メソッド名的には、


シリアル化  → encode
逆シリアル化 → decode

となってます。

 

🤔 JsonElement

というのがあります。

👉 JsonElement hatena-bookmark

少し試してみます。変換しながら型を見ていきます。


// T
val t = Project(name = "kotlinx.serialization", language = "Kotlin")
println("$t ${t::class.simpleName}")
// Project(name=kotlinx.serialization, language=Kotlin) Project


// T -> JsonElement
val e = Json.encodeToJsonElement(t)
println("$e ${e::class.simpleName}")
// {"name":"kotlinx.serialization","language":"Kotlin"} JsonObject


// JsonElement -> String
val s = Json.encodeToString(e)
println("$s ${s::class.simpleName}")
// {"name":"kotlinx.serialization","language":"Kotlin"} String


// String -> JsonElement
val e2 = Json.parseToJsonElement(s)
println("$e2 ${e2::class.simpleName}")
// {"name":"kotlinx.serialization","language":"Kotlin"} JsonObject


// JsonElement -> T
val t2 = Json.decodeFromJsonElement<Project>(e2)
println("$t2 ${t2::class.simpleName}")
// Project(name=kotlinx.serialization, language=Kotlin) Project

JsonElement の型は JsonObject です。

図にします。


                  +-----------------------------------+                             
                  |                 T                 |                             
                  +-----------------------------------+                             
                                  |  ^                                              
                                  |  |                                              
                                  |  |                                              
  Json.encodeToJsonElement(T)     |  |  Json.decodeFromJsonElement<T>(JsonElement)  
                                  |  |                                              
                                  |  |                                              
                                  v  |                                              
                 +------------------------------------+                             
                 |            JsonObject              |                             
                 +------------------------------------+                             
                                  |  ^                                              
                                  |  |                                              
                                  |  |                                              
Json.encodeToString(JsonElement)  |  |  Json.parseToJsonElement(String)      
                                  |  |                                              
                                  |  |                                              
                                  v  |                                              
                 +------------------------------------+                             
                 |               String               |                             
                 +------------------------------------+

しかし、考えてみると、JsonObject って最近ではあまり見かけなくなった気がします。

 

🤔 まとめ

以上を図にしておきます。


👉 Json.md GitHub Gist hatena-bookmark

String 方向が encode(エンコード)serialize(シリアル化)

T 方向が decode(デコード)deserialize(逆シリアル化)



【OpenAI】Kotlin OkHttp で Server-Sent Events (SSE)

ChatGPT API で stream 受信したいですよね。

stream boolean Optional Defaults to false

If set, partial message deltas will be sent, like in ChatGPT. Tokens will be sent as data-only server-sent events as they become available, with the stream terminated by a data: [DONE] message.

👉 API Reference - OpenAI API hatena-bookmark

簡単にできそうなのでやってみました。

 

📡 curl

curl で簡単に取得できる。


curl "https://api.openai.com/v1/chat/completions" \
  -H "accept: text/event-stream" \
  -H "authorization: Bearer $OPENAI_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "model": "gpt-3.5-turbo",
    "messages": [{"role": "user", "content": "Server-Sent Events とは"}],
    "stream": true
  }'


...

data: {"id":"chatcmpl-7OQ1xuBFfam8Ct","object":"chat.completion.chunk","created":1686054286,"model":"gpt-3.5-turbo","choices":[{"delta":{"content":"て"},"index":0,"finish_reason":null}]}

data: {"id":"chatcmpl-7OQ1xuBFfam8Ct","object":"chat.completion.chunk","created":1686054286,"model":"gpt-3.5-turbo","choices":[{"delta":{"content":"います"},"index":0,"finish_reason":null}]}

data: {"id":"chatcmpl-7OQ1xuBFfam8Ct","object":"chat.completion.chunk","created":1686054286,"model":"gpt-3.5-turbo","choices":[{"delta":{"content":"。"},"index":0,"finish_reason":null}]}

data: {"id":"chatcmpl-7OQ1xuBFfam8Ct","object":"chat.completion.chunk","created":1686054286,"model":"gpt-3.5-turbo","choices":[{"delta":{},"index":0,"finish_reason":"stop"}]}

data: [DONE]

パースはだるそうですが、取得はできてるようです!

では、いざ、Kotlin で。

 

📡 okhttp-sse

壊れてるのか何なのか微妙。


testImplementation("com.squareup.okhttp3:okhttp-sse:4.11.0")

👉 okhttp/okhttp-sse at master · square/okhttp · GitHub hatena-bookmark

現在の安定版 4.11.0 ではあやしい。

5.0.0-alpha.11 ではまあまあ動いてるのでそれで。



SSEは、日本語で表現するとサーバ送信イベントと表現されるもので、サーバからクライアントに対してリアルタイムでイベントを送信することができる機能です。 コネクションを張っておいて、サーバからイベントを好きなタイミングで送れるイメージです

 

📡 まとめ

カンファレンスでの公演資料とか、時間が経つと、まあ動かないこと多いですよね!


👉 Server-Sent Events in Android (with Node.js) | by Rahul Ray | Jun, 2023 | ProAndroidDev hatena-bookmark


【Kotlin】ChatGPT のようなメッセージングアプリのスクロールする吹き出し部分にローディングインジケーターを実装する

 

🎞️ ローディングインジケーターの作成

まずは、簡単にテキストで「読み込み中」を表示するViewを作ります。

JetpackCompose なら簡単に実装できます。




こんな無限ループでさえ、Jetpack Compose はメモリーリークを起こしません。きちんと捨ててます。

上の動画内コードから抜粋。


  val texts = listOf("○○○", "️●○○", "️●●○", "️●●●", "️●●○", "️●○○")
  var dots by remember { mutableStateOf("") }

  LaunchedEffect(Unit) {
    while (true) {
      texts.indices.forEach { index ->
        delay(500)
        dots = texts[index]
      }
    }
  }

これを LazyColumn の item の表示に使います。

 

🎞️ Flow<List<Item>> の一時的な付け替え

まず、ローディング表示のよくあるパターン。

リクエストの開始時に _loadingtrue に、レスポンス取得時に false にすることで List<Item> を更新します。


private val _loading = MutableStateFlow(false)
val loading = _listening.asStateFlow()

val items: Flow<List<Item>> = _loading.flatMapLatest { loading ->
  repository.items
}

これを少し変更します。

List<Item> は、レスポンスが帰ってくるまで変化がないですが、この間にダミーの「読み込み中を表す Item」を最後に追加しておきます。

もちろん読み込みが終われば元に戻す。


val items: Flow<List<Item>> = _loading.flatMapLatest { loading ->
  repository.items.map { list ->
    list.toMutableList().apply {
      if (loading) add(dummyLoadingItem)
    }.toList()
  }
}

あとは、各メッセージを表示する Composable で Item を確認して表示を切り替えれば良いです。

 

🎞️ 出来上がり



👉 【Kotlin】Flow flatMap* を ネストするか チェインするか【coroutine】 hatena-bookmark