今日12月16日は夜9:00からシン・ゴジラですね!楽しみです!
今年はオールアバウトを退職したので、もう社員ではありませんが、ありがたいことに一日分枠をいただきましたので、社外から参加させていただきます。
Ktorについて
先日Ktor1.0が正式リリースされましたね!
Ktorは非同期サーバ/クライアントシステムをKotlinで構築するためのフレームワークらしいです。
サーバサイドフレームワークは二年連続でアドベントカレンダーのネタにしちゃってるので、今回はクライアントサイドフレームワークをネタにしたいと思います。
Ktor is a framework for building asynchronous servers and clients in connected systems using the powerful Kotlin programming language
ちなみにKtorは「ことぅあー」じゃなくて、「くとぉあー」って読むっぽい?
実装してみる
試しにQiitaのAPIから投稿を取得するクライアントアプリを作ってみます。
準備
KtorをAndroidプロジェクトへインストールします。
buildscript {
repositories {
google()
jcenter()
maven { url "https://kotlin.bintray.com/kotlinx" } ←追加
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
maven { url "https://kotlin.bintray.com/kotlinx" } ←追加
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 28
defaultConfig {
applicationId "test.com.example"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
// ↓↓↓↓ここから
implementation "io.ktor:ktor-client-android:$ktor_version"
implementation "io.ktor:ktor-client-gson:$ktor_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
// ↑↑↑↑ここまで追加
}
kotlin_version = 1.3.10
ktor_version = 1.0.1
coroutines_version = 1.0.1
kotlin.coroutines = enable
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="test.com.example">
<uses-permission android:name="android.permission.INTERNET" /> ← 追加
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
UI部分の実装
UI部分を実装します。TextViewに通信した結果を突っ込んでるだけの超手抜きですが。
package test.com.example
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.util.Log
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
val url = "https://qiita.com/api/v2/items?page=1&per_page=20&query=qiita+user%3Ayaotti"
val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
Log.d(TAG, "Begin onCreate") // Async/Awaitの挙動に感動するためのログ
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch {
Log.d(TAG, "Begin Load Qiita")
val response = AsyncQiitaRequest().get<Array<QiitaItemEntity>>(url)
GlobalScope.apply {
launch(Dispatchers.Main) {
main_text_view.text = response[0].body
}
}
Log.d(TAG, "Finish Load Qiita")
}
Log.d(TAG, "Finish onCreate")
}
}
通信部分を実装する
Ktor ClientとCoroutineで非同期処理APIクライアントを実装します。
package test.com.example
import io.ktor.client.HttpClient
import io.ktor.client.engine.android.Android
import io.ktor.client.features.json.GsonSerializer
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.request.get
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
class AsyncRequest {
suspend inline fun<reified T> get(url: String, params: Map<String, Any> = mapOf()): T {
val client = HttpClient(Android) {
install(JsonFeature) {
serializer = GsonSerializer()
}
engine {
connectTimeout = 100_000
socketTimeout = 100_000
}
}
val request = GlobalScope.async {
Thread.sleep(2000) // 通信しててもUIに影響がないことを確認するためのテスト
val response = client.get<T>(url)
client.close()
return@async response
}
return request.await()
}
}
package test.com.example
data class QiitaItemEntity(
val rendered_body: String,
val body: String,
val coediting: Boolean,
val comments_count: Int,
val created_at: String,
// val group:
val id: String,
val likes_count: Int,
// tags:
val private: Boolean,
val reactions_count: Int,
val title: String,
val updated_at: String,
val url: String,
// user:
val page_views_count: Int?
)
処理流れまとめ
基本的な処理の流れは下記のような感じでしょうか。
- MainActivityのGlobalScope.launchで、UIスレッドとは別のスレッド(以下スレッドA)を作成
- スレッドA内でQiitaの記事データ読み込みを開始。(この時点ではUIスレッドとスレッドAが同時に実行される。体感したければ、activity_main.xmlにButtonを配置し、GlobalScope.async内で、Thread.sleepしてみると良いかも)
- データを読み込み終えたタイミングでUIスレッドを中断、スレッドA内のapplyで取得したQiitaの投稿データをTextViewへセットする処理をUIスレッドへ渡す。
- UIスレッドでTextViewにデータをセット
GlobalScope.launch {
Log.d(TAG, "Begin Load Qiita")
val response = AsyncQiitaRequest().get<Array<QiitaItemEntity>>(url)
GlobalScope.apply {
launch(Dispatchers.Main) {
main_text_view.text = response[0].body
}
}
Log.d(TAG, "Finish Load Qiita")
}
下記のclient.get<T>(url)
の部分がKtor Clientの機能ですね。
asyncで非同期でデータを取得し、データを取得し終えるタイミング(await)でデータを返しています。
val request = GlobalScope.async {
val response = client.get<T>(url)
client.close()
return@async response
}
return request.await()
おわりに
以上、Ktorを用いたasync/awaitのサンプルでした。
Ktorを使うことでクライアントサイドの通信周りを、すごくサクッと実装できた感があります。
特に、レスポンスのJsonマッピング部分が楽だった印象です。Retrofitなどを使うと、Gsonを利用するための設定をごちゃごちゃ書いた記憶がありますが、KtorはJsonシリアライザーをいくつかサポート(GsonとかJackson)しており、JsonシリアライザーにGsonを使うよーっていう宣言をするだけでGsonを使えたので、超楽でしたね。
val client = HttpClient(Android) {
install(JsonFeature) {
serializer = GsonSerializer()
}
}
あ、そういえばエラーハンドリングしてないや。
今後
僕がKtorの存在を知ったのは、1.0リリース後なので超にわかですが、Ktor使いやすかったので今後も何かで使ってみたいですね。
あと、マルチプラットフォームで利用するためのドキュメントなんてものもあるので、マルチプラットフォームでの実装も割と楽にできそうな予感...!
(実際Ktorのマルチプラットフォームのサンプルプロジェクトは結構凄かった。)
試してみたいですね〜
では、皆さま良いお年を!