SpaceX REST API で試す Retrofit の coroutine 対応

Retrofit の coroutine 対応が進んでいるようです。

Jake Wharton's retrofit2-kotlin-coroutines-adapter has been the go-to solution for bridging the coroutine world with Retrofit for a little while. But now, a much-anticipated PR has finally been merged, officially bringing coroutine support to Retrofit 2.

Retrofit meets coroutines - zsmb.co

JakeWharton/retrofit2-kotlin-coroutines-adapter: A Retrofit 2 adapter for Kotlin coroutine's Deferred type.

SpaceX-API とか公開されていたのですね。

r-spacex/SpaceX-API: Open Source REST API for rocket, core, capsule, pad, and launch data

https://api.spacexdata.com/v3/rockets

対応する最小限のデータクラスを作って、試してみましょう。


data class Rocket(

    val id: Int,

    @field:Json(name = "rocket_name") 
    val name: String

)


val retrofit = Retrofit.Builder()
    .baseUrl("https://api.spacexdata.com/v3/")
    .addConverterFactory(MoshiConverterFactory.create())
    .build()

val api = retrofit.create<SpaceXApi>()

create() は reified type が使えるようになってます。

JSONのシリアライズはお好みのものを。

そして、interface を書いていきますが、

ここを戻り値に別に見てみます。

Call<List<Rocket>>

interface はこれまでと同じ記述。


interface SpaceXApi {

  @GET("rockets")
  fun getRockets(): Call<List<Rocket>>

}

受け側の3つの例。


runBlocking {
  val rockets: List<Rocket> = api.getRockets().await()
  rockets.forEach(::println)
}


runBlocking {
  val rockets: List<Rocket> = try {
    api.getRockets().await()
  } catch (e: Exception) {
    println("Network error :[")
    return@runBlocking
  }
  rockets.forEach(::println)
}


runBlocking {
  val response = api.getRockets().awaitResponse()
  if (response.code() == 200) {
    response.body()?.forEach(::println)
  }
}

結果はすべて同じ。


Rocket(id=1, name=Falcon 1)
Rocket(id=2, name=Falcon 9)
Rocket(id=3, name=Falcon Heavy)
Rocket(id=4, name=Big Falcon Rocket)

これまで非同期処理時に必要だった コールバック や enqueue の記述は必要ありません。

受け側の記述を変化させることで、Exception や レスポンスコードを拾うことができます。

suspend List<Rocket>

suspend を使うことでさらに直感的に書けます。


interface SpaceXApi {

  @GET("rockets")
  suspend fun getRockets(): List<Rocket>

}


runBlocking {
  val rockets = api.getRockets()
  rockets.forEach(::println)
}

結果。


Rocket(id=1, name=Falcon 1)
Rocket(id=2, name=Falcon 9)
Rocket(id=3, name=Falcon Heavy)
Rocket(id=4, name=Big Falcon Rocket)

List<Rocket> に対して Call や Deferred のようなラッパーは必要ありません。

suspend Response<List<Rocket>>

Response を拾います。


interface SpaceXApi {

  @GET("rockets")
  suspend fun getRockets(): Response<List<Rocket>>

}


runBlocking {
  val response = api.getRockets()
  if (response.code() == 200) {
    response.body()?.forEach(::println)
  }
}

結果。


Rocket(id=1, name=Falcon 1)
Rocket(id=2, name=Falcon 9)
Rocket(id=3, name=Falcon Heavy)
Rocket(id=4, name=Big Falcon Rocket)

最も実用的で簡潔な記述と思われます。

まとめ

Kotlin で記述するにしても、多くの記述が可能となりそうです。

RxJava他ライブラリを利用しての非同期実装を含めると様々となります。

ネット上で検索するにも自分の環境や好みに合った記述を探すのにも時間がかかることになり混乱もありそうで。

Jakeも今後はメンテしないと思われます。

Márton Braun on Twitter: "Retrofit is finally receiving the proper coroutine support it deserves! Read more about it in my latest blog post here: https://t.co/8k9EauzzUa #AndroidDev #Kotlin #Coroutines" / Twitter

ちなみに、これらの話は今現在はSNAPSHOTで進行中です。

以下でどうぞ。


repositories {
  maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
}

dependencies {
  implementation "com.squareup.retrofit2:retrofit:2.5.1-SNAPSHOT"
}


@ObsoleteCoroutinesApi とは「様子見」?

Coroutine で Channel 辺りをウロウロと。

あれ?

This declaration is experimental and its usage should be marked with '@kotlinx.coroutines.ObsoleteCoroutinesApi' or '@UseExperimental(kotlinx.coroutines.ObsoleteCoroutinesApi::class)'

調べてみる。

From the point of usage `ObsoleteCoroutinesApi` is just like experimental. The different is in intent. When API is experimental it might change or might graduate as is. For obsolete API we already know that there are problems with designs that will force us to deprecated this API in the future when we have a better replacement.

Right now there is no replacement for channel operations, so you have no choice but continue using them. However, we plan to replace them with mechanism based on lazy/cold streams (#254) in the future, which will get you much better performance.

What is the recommended approach for @ObsoleteCoroutinesApi #632

Google翻訳で。

使用の観点からObsoleteCoroutinesApiは実験的なようです。 違いは意図しています。 APIが実験的なものである場合、それは変更されるか、またはそのまま卒業するかもしれません。 時代遅れのAPIについては、より良い代替品があるときに将来的にこのAPIを非推奨にするような設計上の問題があることはすでにわかっています。

今のところチャンネル操作に代わるものはないので、あなたには選択肢がないがそれらを使い続けることができる。 ただし、将来的にはレイジー/コールドストリーム(#254)に基づくメカニズムに置き換える予定です。これにより、パフォーマンスが大幅に向上します。

まとめ


@ExperimentalCoroutinesApi

→ 将来的には、変更される or そのまま。


@ObsoleteCoroutinesApi

→ 設計上の問題がある。

→ 今現在、代替えはない。

→ 将来的には非推奨。

アンダースコアがついてるのはそゆこと?


_events.consumeEach {

チャンネル周りは、今は「様子見」がいいのでしょうね。


SQLDelight の データベースバージョン

「分かれば簡単だけど、分かるまで難しい」

そんなこと多いですよね。

なにを悩んでいたのか。というやつ。

SQLDelight は、1.0 となり、今現在、ドキュメントやリファレンスが少なくてはまります。

おおまかに「しくみ」を捉えてからやってみること大事です。

 

1.テーブル作成

テーブルを作成したい場合。SQLで、


CREATE TABLE player (
  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
  number INTEGER NOT NULL,
  name TEXT NOT NULL,
  time TEXT DEFAULT (strftime('%s', 'now')),
  UNIQUE (number, name)
);

のようなものを書きますよね。

これは、Player.sq というファイルに書いて、所定の位置に置きます。

この位置はデフォルトでは、パッケージ名 com.example.testdelight の場合、


/app/src/main/sqldelight/com/example/testdelight/Player.sq

となります。

 

2.クエリー作成

プログラムコード上で利用したい「メソッド名」と、それに対するSQLを箇条書きにします。


selectAll:
SELECT *
FROM player;

insert:
INSERT INTO player(number, name)
VALUES (?, ?);

changes:
SELECT changes();

count:
SELECT COUNT(id)
FROM player;

これも、前述の Player.sq ファイルに追記します。

これで、テーブル周りの設定は終わりです。

 

3.スキーマのバージョン

ここが少し分かりづらかったのですが、

「201901281」を新バージョンにしたい場合、

「1を引いたもの」をファイル名として、

「201901280.sqm」

として置きます。

今回は、テーブル定義の変更はないので、中身なしの空ファイルです。

少し不思議な感じがしますが、書き出してみると分かってきます。

なお、このファイルを設定しなければ、適用されるバージョンは「1」となります。

 

4.ビルドして書き出す

ここでビルドすると、以下のようなファイルが書き出されます。

それぞれ以下のコードとなっています。

これらを使って、コードを書いていきます。

 

まとめ

既存の .db ファイルに対して、バージョン更新を行いたい場合のキモとなるのは、書き出される Database ファイル。


object Schema : SqlDriver.Schema {

     override val version: Int
         get() = 201901281 // 201801280 + 1

     // ...

     override fun migrate(
         driver: SqlDriver,
         oldVersion: Int,
         newVersion: Int
     ) {
         if (oldVersion <= 201901280 && newVersion > 201901280) { // same .sqm file name

           // from 201801280.sqm contents
           // ...

         }
     }
 }

.sqm のファイル名の数字が、

「新バージョンの数字」
「適用される既存.db のバージョンの数字」

を決める。

SQLDelight 1.0 使い方 #1
SQLDelight 1.0 使い方 #2
SQLiteのユーザバージョンを利用する - Basic
Pragma statements supported by SQLite