こんにちは。ピクトリンク事業部の古川です。
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も選択肢のひとつになっていくと思います。