こんにちは。ピクトリンク事業部の古川です。
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>

まとめ
非常に簡単ではありますが、以上でCoroutinesを試してみることができました。
完全にReactiveXをCoroutinesに置き換えることはまだまだ難しいとは思いますが、ReactiveXよりも考えることが比較的少なく、実装も容易ですので
今後はCoroutinesも選択肢のひとつになっていくと思います。