AndroidでKotlin Coroutinesを使ってみる


目次

こんにちは。ピクトリンク事業部の古川です。

Androidでは従来ReactiveXが主流ですが、最近話題になっているCoroutinesを使った非同期処理の実装について解説します。

Coroutinesとは?

よく誤解されますが、CoroutinesとReactiveXは全くの別物です。

ReactiveXは非同期処理をストリームで実施するためのものですが、

Coroutinesは、「特定のスレッドに束縛されない、中断可能な計算処理インスタンス」です。

「特例のスレッドに束縛されない」ため、Androidでは非同期処理に用いられることが多いです。

そのためReactiveXとの比較で語られることが多いですが、性質が異なることを意識しておいてください。

Kotlinの公式ドキュメントに

「Coroutines simplify asynchronous programming by putting the complications into libraries.」(コルーチンは非同期処理をシンプルに書けるようにする)

と書いているのも誤解を呼ぶ要因の一つかと思います…

 

Kotlin Coroutinesを導入する

利用するにはbuild.gradleに情報を追加します。

また、Experimentalですので、その情報も追加します。

androidExtensions {
    experimental = true
}
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.20"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.20"

使い方

今回はサンプルとして、GitHubのAPIをRetrofitでコールしたレスポンスを画面に表示するアプリを作ってみます。

事前準備

RetforitでAPIを叩くために、InterfaceとModelを準備します。

interface GithubApi {
    @GET("users/{user_name}")
    fun fetchUser(@Path("user_name") userName: String): Call<User>
}
data class User(var name: String? = "name",var id: String? = "id", var avatar_url: String? = "http://www.github.com")

実際に使ってみる

次に、このAPIを叩くメソッドを用意します。今回はViewModelに実装します。

class MainViewModel {
    companion object {
        private const val BASE_URL = "https://api.github.com/"
    }

    private val retrofit: Retrofit by lazy {
        Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).build()
    }

    private val githubApi: GithubApi by lazy {
        retrofit.create(GithubApi::class.java)
    }

    fun fetchUser(userName: String): Deferred<User> = async(CommonPool) {
        val userCall = githubApi.fetchUser(userName)
        val response = userCall.execute()
        response?.let {
            if (response.isSuccessful) {
                return@async response.body()!!
            }
        }
        return@async User()
    }
}

fetchUserメソッドの1行目が最も重要な部分です。

まず返却値の型としてUserをDeferredで返します。ReactiveXだとSingleになると思います。

async(CommonPool)はコルーチン内をスレッドプールで実行する、という理解でいいと思います(こちらを参考に)

後は通常通り、userを返却してあげているだけです。

それでは、このメソッドを呼び出してみます。

class MainActivity : AppCompatActivity() {
    val mainViewModel = MainViewModel()
    val binding: ActivityMainBinding by lazy {
        DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding.viewModel = mainViewModel

    val dialog = AlertDialog.Builder(this).setTitle("progress").setMessage("in Progress").create()
        findViewById<Button>(R.id.button).setOnClickListener {
            launch(UI) {
                dialog.show()
                mainViewModel.fetchUser("furusin").await()
                dialog.dismiss()
            }
        }
    }
}

単純に「処理開始時にダイアログを表示し、終わったら画面に反映して閉じるだけ」の処理をしています。

launch(UI)は「UIスレッドで実行してね」という意味になります。

mainViewModel.fetchUser("furusin").await()

ここで .await() を呼んでいますが、これは「この処理が終わるまで続きの処理は awaitする(待つ)」という意味です。

最後に、画面に表示するXMLと画面イメージです。

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewModel"
            type="jp.furyu.kotlincoroutinessample.viewmodel.MainViewModel" />

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".activity.MainActivity">

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="test Coroutines"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/textView_user_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.user.name}"
            app:layout_constraintBottom_toTopOf="@+id/textView_user_id"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/button"
            app:layout_constraintVertical_chainStyle="spread"
            tools:text="user name" />

        <TextView
            android:id="@+id/textView_user_id"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.user.id}"
            app:layout_constraintBottom_toTopOf="@+id/textView_user_avator_url"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView_user_name"
            tools:text="user id" />

        <TextView
            android:id="@+id/textView_user_avator_url"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.user.avatar_url}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView_user_id"
            tools:text="avator url" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

activity-MainActivity-08302018161310

まとめ

非常に簡単ではありますが、以上でCoroutinesを試してみることができました。

完全にReactiveXをCoroutinesに置き換えることはまだまだ難しいとは思いますが、ReactiveXよりも考えることが比較的少なく、実装も容易ですので

今後はCoroutinesも選択肢のひとつになっていくと思います。