13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

All About Group(株式会社オールアバウト)Advent Calendar 2018

Day 16

AndroidアプリでKtorとCoroutine使ってasync/awaitしてみるサンプル書いてみた

Last updated at Posted at 2018-12-15

今日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プロジェクトへインストールします。

app/build.gradle

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
}

build.gradle
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"
    // ↑↑↑↑ここまで追加
}
gradle.properties
kotlin_version = 1.3.10
ktor_version = 1.0.1
coroutines_version = 1.0.1

kotlin.coroutines = enable
app/src/main/AndroidManifest.xml
<?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に通信した結果を突っ込んでるだけの超手抜きですが。

MainActivity.kt
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クライアントを実装します。

AsyncRequest.kt
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()
    }
}
QiitaItemEntity.kt
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?
)

処理流れまとめ

基本的な処理の流れは下記のような感じでしょうか。

  1. MainActivityのGlobalScope.launchで、UIスレッドとは別のスレッド(以下スレッドA)を作成
  2. スレッドA内でQiitaの記事データ読み込みを開始。(この時点ではUIスレッドとスレッドAが同時に実行される。体感したければ、activity_main.xmlにButtonを配置し、GlobalScope.async内で、Thread.sleepしてみると良いかも)
  3. データを読み込み終えたタイミングでUIスレッドを中断、スレッドA内のapplyで取得したQiitaの投稿データをTextViewへセットする処理をUIスレッドへ渡す。
  4. 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のマルチプラットフォームのサンプルプロジェクトは結構凄かった。)
試してみたいですね〜
では、皆さま良いお年を!

13
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?