株式会社はてなに入社しました
今日はエイプリルフールです。 *1
*1:入社から5年と3ヶ月経ちました。
この記事は DroidKaigi 2020, day 1 17:00-17:40で発表される予定だった内容を書き起こしたものです。
他にも発表予定だった資料のAGENDAを1項目ごとに記事にして公開する予定です。
Kotlin Coroutinesが正式リリースされて2年ちょっとが過ぎ、もはや私たちのAndroidアプリ開発には欠かせないものとなりました。
コルーチンはRxJavaのようなストリームやコールバック方式と比べて非同期的な処理を直感的に記述でき、これまでRxJavaを使っていたようなユースケースをある程度置き換えられるようになっています。 私はこの2年と少しでKotlin Coroutinesを複数のアプリに導入し既存のパラダイムの置き換えを進めてきました。
Kotlin Coroutinesの導入を行ったことでAndroidアプリ開発が楽になりましたが、大変になったこともありました。
恐らくKotlin Coroutinesが難しく感じて導入をためらっている方もいるでしょう。
このシリーズでは、既存のAndroidアプリにKotlin Coroutinesを導入したことによってAndroidアプリ開発がどう変わったか、
Kotlin Coroutinesの導入を行ったことで困ったことをどう解決したかなどを紹介します。
皆様のKotlin Coroutinesの導入の参考になれば幸いです。
また、Kotlin Coroutinesに関してまだ何も知らないという方はこのシリーズを読む前に以下の記事か、Kotlin Coroutinesの公式ドキュメントを一読されることをおすすめします。
既存アプリにKotlin Coroutinesを導入するにあたっておそらく最初の壁となるのが既存のパラダイムへの影響でしょう。 Kotlin Coroutinesで書くと既存のRxJavaやイベントハンドラーのようなものをコルーチンに置き換えたくなるはずです。
既存のパラダイムを全部置き換えるのは工数がない、ライブラリーやフレームワークのコードは簡単には変えられない、といった問題で現実的ではなく実際は一部を置き換えるということになるでしょう。
どちらの方法で進めても途中で必ず既存のパラダイムとKotlin Coroutinesの仕組みとの変換が必要になってきます。 この記事ではRxJavaやコールバック、イベントリスナーといったような仕組みとKotlin Coroutinesを繋ぎ込む方法を紹介します。
RxJavaはおそらく現代のAndroidアプリ開発でJetpackの次くらいに一番よく使われているライブラリーではないでしょうか。
リアクティブプログラミングによってAndroidアプリ開発者は複雑な非同期処理を行う際のコールバック地獄から解放されました。
一方でコールバック地獄の次はOperator地獄、実行するスレッドを切り替えるための仕組みであるSchedulerが分かりにくい、Operatorをめちゃくちゃ組み合わせたら書いた本人以外何やってるのか分からないコードが完成する、などと人類には早すぎた感も否めないように思えます。
RxJavaとKotlin Coroutinesは kotlinx-coroutines-rx2
と kotlinx-coroutines-reactive
を使うことで相互運用が可能です。
RxJavaの各種ストリームをコルーチン内で使うための拡張関数が kotlinx-coroutines-rx2
と kotlinx-coroutines-reactive
(Reactive Streamsの Publisher
実装である Flowable
はこちら) には用意されています。
以下のようなRepositoryのコードがあるとします。
interface UserRepository { fun getCurrentUserId(): Maybe<UserId> fun getUser(userId: UserId): Single<User> fun userStatus(): Observable<UserStatus> fun userStatusFlowable(): Flowable<UserStatus> }
このRepositoryのメソッドはそれぞれコルーチン内で以下のように使えます。
val repository : UserRepository scope.launch { // get curent user id val userIdOrNull: UserId? = repository.getCurrentUserId().await() userIdOrNull?.let { // get user val user: User = repository.getUser(it).await() } // await first user status val userStatus: UserStatus = repository.userStatus().awaitFirst() // also ok val userStatus: UserStatus = repository.userStatusFlowable().awaitFirst() }
もちろんそれぞれのストリームがsubscribeした時に同期的に処理をする作られている場合はsubscribeしたその場で処理が同期的に行われてしまうので、通信やディスクI/Oを伴う処理が含まれている場合は以下のような対策が必要です。
withContext {}
を利用してメインスレッド上で実行されないようにするまた、これ以外にも Observable
や Flowable
を Flow
に変換する拡張関数も用意されています。
複数流れてくる値をコルーチンで処理したい場合にはこちらを使います。
interface UserRepository { fun userStatusFlowable(): Flowable<UserStatus> fun userStatusFlow(): Flow<UserStatus> = userStatusFlowable().asFlow() }
RepositoryをKotlin Coroutinesを使って書くようにしたはいいがViewModelがまだRxJavaで〜とかUseCaseで他のRxJavaのストリームと組み合わせないといけなくて、みたいなことがあった場合にはSuspending functionやFlowを使ってRxJavaのストリームを作ることもできます。
interface UserRepository { suspend fun getCurrentUserId(): UserId? suspend fun getUser(userId: UserId): User fun userStatus(): Flow<UserStatus> }
rxXXXX {}
という関数が用意されているので、この関数のラムダ式内でSuspending functionやFlowを使うと簡単にRxJavaのストリームを作ることができます。
val repository: UserRepository val currentUserId: Maybe<UserId> = rxMaybe { repository.getCurrentUserId() } val userId: UserId val user: Single<user> = rxSingle { repository.getUser(userId) } val userStatusFlowable: Flowable<UserStatus> = rxFlowable { producer -> repository.userStatus().collect { producer.send(it) } }
また、 Flow
を簡単に Observable
や Flowable
にも変換できます。
val userStatusFlowable: Flowable<UserStatus> = repository.userStatus().asFlowable() val userStatusObservable: Observable<UserStatus> = repository.userStatus().asObservable()
ここで紹介した以外にも kotlinx-coroutines-rx2
にはいくつかの関数が用意されてますので、詳しくは公式ドキュメントをチェックしてください。
ここでいうコールバックとは以下のようなものです。
コールバックをSuspending functionに変換するには以下の2つの関数が使えます。
suspendCoroutine
suspendCancelableCoroutine
2つの違いは名前通りキャンセル可能かどうかなので、必要に応じて選択してください。
これらの関数はKotlin Coroutinesの低レベルAPIで Continuation
を受け取ってこのインスタンスに値を渡すことで再開できる関数を作るといったものです。
例えば以下のようなHTTPクライアントがあるとします(実装は省きますが名前で雰囲気を掴んでいただく感じで)
interface HttpClient { fun myUser(callback: HttpClient.Callback<User>): Request interface Callback<T> { fun onSuccess(res: T) fun onFailure(e: HttpException) } interface Request { fun cancel() } }
これをSuspending functionに変換するとこうなります。
suspend fun HttpClient.myUser() : User = suspendCoroutine { cont -> myUser(object: HttpClient.Callback<User> { fun onSuccess(res: User) { cont.resume(res) } fun onFailure(e: HttpException) { cont.resumeWithException(e) } }) }
あとはコルーチン内で普通に他のSuspending functionと同じように使えます。
scope.launch { val httpClient: HttpClient val user: User = httpClient.myUser() }
ここでいうイベントリスナーとは以下のようなものです。
複数の値が流れてくるのでFlowが使えます。
ただしFlowを作るための一番簡単な関数である flow {}
は同じコルーチン内からしか値を送ることができません。こういったコルーチンのスコープ外から複数の値を送るFlowを作るのには callbackFlow {}
を使います。
似たような関数に channelFlow {}
がありますが、こちらはFlowが閉じられるのを待つ awaitClose {}
を呼び出したかどうかのチェックがありません。
val View.onClick: Flow<View> = callbackFlow { setOnClickListener { view -> offer(view) } awaitClose { setOnClickListener(null) } }
この関数で生成されたFlowをcollectすればクリックしたイベントがコルーチンに送られます。
scope.launch { val onClick = view.onClick onClick.collect { println("Collect") } }
ただしこの方法だと Flow
がコールドストリームであることに影響して、 collect {}
を実行するまでリスナーはセットされませんのでご注意ください。
この記事では既存のパラダイムとKotlin Coroutinesの共存方法について紹介しました。
Kotlin Coroutinesを使いたいが、既存のパラダイムとの組み合わせ方が分からない!という方の助けになれば幸いです。
次回はAndroidアプリでよく使われる既存のライブラリーとKotlin Coroutinesを組み合わせる方法について紹介したいと思います。
この記事は社内のエンジニアが集まるScrapboxへ書いたページの転記です。
なんとなくKotlin Coroutinesについて理解が深まると幸いです。
async
launch
Dispatchers.Main
がDispatcherとして設定されていれば作成されたCoroutineはメインスレッド上で実行されるSupervisorJob
やsupervisorScope
を使えば親から子への一方向にしか伝播しないようにできる
viewModelScope
やlifecycleScope
はSupervisorJob
が使われているので、子から親には伝播しないようになっているCoroutineScopeを作るには以下のような関数で行える
CoroutineScope
MainScope
SupervisorJob
と Dispatchers.Main
をもっているCoroutineScope
UIを操作する時に使ったりする
ActivityやFragmentなどで使うことがあったが、 lifecycle-runtime-ktx
に追加された LifecycleCoroutineScope
の登場で使わなくなっていきそうcoroutineScope
async {}
を並行実行したい時
よく使う既にあるものを使うと便利、よく使うCoroutineScopeは以下の通り
ViewModel.viewModelScope
SupervisorJob
+ Dispatchers.Main
Dispatchers.Main.immediate
になった模様ViewModel.onCleared
実行時にキャンセルされるLifecycle.coroutineScoope
/ LifecycleOwner.lifecycleScope
androidx.lifecycle
の Lifecycle
と連動しているCoroutineScopeandroidx.lifecycle.Lifecycle.Event#ON_DESTROY
のイベントを受け取った時にキャンセルされる
onDestroy
Fragment.viewLifecyclerOwner
だと Fragment.onDestroyView
launchWhenCreated
/ launchWhenResumed
/ launchWhenResumed
といった便利コルーチンビルダーが生えているGlobalScope
Dispatchers.Unconfined
を使って produce
などのoperatorを使う時、に使うがこれもレアケースだと思うsuspend
キーワードを使った関数を作ればよいcoroutineScope
と async
を使って Deferred
を立ち上げたりした場合は別withContext(defaultDispatcher) { /# do something #/ }
みたいな感じで囲ってやるとよいlaunch
async
await
はSuspending FunctionなのでSuspending Function内で実行する必要があるlaunch
-> coroutineScope
-> async
みたいな囲うのが一般的
coroutineScope
を使わないのもありwithContext
Dispatchers.Default
に切り替えて実行したい時に使われることが多いNonCancelable
を渡すとキャンセルされない
try/catch
できるSupervisorJob
を使っていると死なないcoroutineScope
で囲う死なないという話はあるが、それでもcatchしないと普通に例外が吐かれてアプリが死ぬdelay
yield
kotlinx.coroutines
にあるサスペンド関数は全てキャンセル可能になっている
All the suspending functions in
kotlinx.coroutines
are cancellable.
これは はてなエンジニア Advent Calendar 2019 13日目の記事です。
Kotlin Coroutinesのコルーチンをテストする時に必要なテクニックを紹介します。
コルーチンを使って非同期処理をしているが、テストをする時には同期的に実行してほしいことはよくありますよね。
TestCoroutineScope
と TestCoroutineDispatcher
を使えば簡単にできます。
また、 runBlockingTest
を使えばこれらのクラスが自動的に使われます。
普段使っているCoroutineDispatcherをテスト時に TestCoroutineDispatcher
に置き換えるには下記のような方法があります
kotlinx-coroutines-test
に含まれている Dispatchers.setMain
/ Dispatchers.resetMain
を使いましょう
@Before fun setUp() { Dispatchers.setMain(TestCoroutineDispatcher()) } @After fun tearDown() { DIspatchers.resetMain() }
直接置き換える手段はないので、DIしましょう。
Flow.collect
をコルーチン内で実行すると値を全部受け取るまで処理がそこで止まります。
例えばRoomが返すような終わりのないFlowはキャンセルしない限り collect
から先に進めないので、 launch
で新しいコルーチンを立ち上げるとよいです。
その時 TestCoroutineScope
や runBlockingTest
を一緒に使ってやると Flow.collect
が同期的に実行されます
@Test fun test() = runBlockingTest { val flow = // create flow var actual = null val flowCollectJob = launch { flow.collect { actual = it } } // ここでFlowに値が流れそうなことを何かする // yield()でこのコルーチンがDispatcherを一旦手放すことでflow.collectに渡したblockが実行され値が反映される assertThat(actual).isEqualTo(expected) // 最後にキャンセルしてやらないと完了していないコルーチンがあるので例外が起きる flowCollectJob.cancel() }
2020年2月に五反田で開催されるDroidKaigi 2020でKotlin Coroutinesについて発表します。Kotlin Coroutinesの使い方に困ってる方は是非見に来てください!
これから始めるKotlin Coroutinesの導入 Kotlin Coroutinesが正式リリースされて1年が過ぎ、もはや私たちのAndroidアプリ開発には欠かせないものとなりました。 コルーチンはRxJavaのようなストリームやコールバック方式と比べて非同期的な処理を直感的に記述でき、これまでRxJavaを使っていたようなユースケースをある程度置き換えられるようになっています。 私はこの1年と少しでKotlin Coroutinesを複数のアプリに導入し既存のパラダイムの置き換えを進めてきました。 Kotlin Coroutinesの導入を行ったことでAndroidアプリ開発が楽になりましたが、大変になったこともありました。 恐らくKotlin Coroutinesが難しく感じて導入をためらっている方もいるでしょう。 このセッションでは、既存のAndroidアプリにKotlin Coroutinesを導入したことによってAndroidアプリ開発がどう変わったか、 Kotlin Coroutinesの導入を行ったことで困ったことをどう解決したかなどを紹介します。 皆様のKotlin Coroutinesの導入の参考になれば幸いです。 - RxJavaを使って書かれたコードを置き換える - RxJavaを使った世界観にKotlin Coroutinesを混ぜる - Retrofitを用いたHTTP通信をKotlin Coroutinesに対応 - RoomをKotlin Coroutinesで利用する - Kotlin CoroutinesとView層との組み合わせ - Kotlin CoroutinesとLiveDataの使い分け - Kotlin CoroutinesをViewModelで活用する - Kotlin Coroutinesとテスト - 例外処理 - キャンセル可能に作る
明日の担当は id:polamjag です。
こんばんは、Kotlin大好きAndroidアプリエンジニアの id:takuji31 です。
※これは はてなエンジニア Advent Calendar 2018 21日目の記事です。
provideDelegate operatorはKotlin 1.1からあるオペレーターで、プロパティーの移譲オブジェクトを生成することができます。
Delegated Properties - Kotlin Programming Language
このオペレーターを挟むことで、Delegated property生成時にロジックを実行することができ、例えば以下のようなことができます。
まずはプロパティーになるclassを定義しましょう。
読み書きできるプロパティーを作るには kotlin.properties.ReadWriteProperty
を実装したclassが必要です。
ここではAndroidのSharedPreferencesを操作するStringのpropertyを定義します。
class SharedPreferencesProperty(private val key: String, private val defaultValue: String?) : ReadWriteProperty<PreferencesModel, String?> { override fun getValue(thisRef: PreferencesModel, property: KProperty<*>): String? { return thisRef.sharedPreferences.getString(key, defaultValue) } override fun setValue(thisRef: PreferencesModel, property: KProperty<*>, value: String?) { thisRef.sharedPreferences.edit().putString(key, value).apply() } }
次に provideDelegate
をもつclassを定義します。
SharedPreferencesに値を出し入れするにはキーを保持する必要がありますが、必ずしもキーがプロパティー名と同じではありません。
そこでプロパティーの移譲オブジェクトにキーを渡せるようにします。
もちろんキーは空文字だと困るので検証したいですね。
class SharedPreferencesPropertyProvider( private val key: String? = null, private val defaultValue: String? = null ) { operator fun provideDelegate( thisRef: PreferencesModel, prop: KProperty<*> ): ReadWriteProperty<PreferencesModel, String?> { val propertyName = prop.name val key = key ?: propertyName require(key.isNotEmpty()) { "Key cannot be empty" } return SharedPreferencesProperty(key, defaultValue) } }
最後にこのプロパティーを使うclassを定義します。
open class PreferencesModel(context: Context, name: String) { internal val sharedPreferences: SharedPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE) protected fun stringPreferences(key: String? = null, defaultValue: String? = null) = SharedPreferencesPropertyProvider(key, defaultValue) } class UserPreferences(context: Context) : PreferencesModel(context, "user") { var name: String? by stringPreferences() var profile: String? by stringPreferences("profile") }
あとはこの UserPreferences
のインスタンスのプロパティーをset/getするとSharedPreferencesを操作できます。
UserPreferences.profile
にはキーの値を渡していますが、例えばこの値を空文字にするとインスタンス生成時に例外が発生するようになります。
正しくインスタンス生成ができるかはテストでチェックしてやるとよいでしょう。
provideDelegate
メソッドを使うとDelegated propertyを更に柔軟にすることができるでしょう。
明日の担当は id:tkzwtks さんです。
※ これはtakuji31 Advent Calendar 2018、1日目(相当)の記事です。
ViewModelは我々Androidアプリエンジニアの生活になくてはならないコンポーネントの一つである。
AndroidのActivityのインスタンス単体のライフサイクルは特定の画面のライフサイクルより短く、画面回転やメモリー不足で簡単に消される。
だが通信などの非同期処理は画面を完全に抜け出すまでやっていてほしいし、データも保持されてほしく、そういう時にViewModelを使う。
使い方は公式ドキュメントを見てほしい。
AutoDisposeはUberが公開しているRxJava2のDisposableを自動的にdisposeするライブラリーである。
大半の処理は実行を指示したライフサイクルに対応するライフサイクルで処理をキャンセル(createならdestroy、resumeならpause)したいものである。
これをいい感じに自動的にやってくれるライブラリーがAutoDisposeだ。
1.0.0でandroidxにも対応していて安心。
使い方は簡単でRxJavaのObservableやSingleをsubscribeする前に
observable .autoDisposable(scope()) .subscribe()
などとしてやるだけである。
これだけで現在の状態に対応したライフサイクルイベントが起きた時にdisposeされる。
AutoDisposeには ScopeProvider
というinterfaceがあり、これを使うと今のスコープをAutoDisposeに知らせることができる。
ScopeProvider#requestScope
で CompletableSource
を返せばこれが完了した時に Disposable
がdisposeされる。
Fragment
や Activity
といった LifecycleOwner
は ScopeProvider
を継承した LifecycleScopeProvider
(を実装した AndroidLifecycleScopeProvider
) によってAndroidのActivityやFragmentのライフサイクルに応じたスコープを返すようになっている。
一方でViewModelはonCleared
という単一のライフサイクルメソッドを持っている。
AutoDisposeで管理されるViewModel内のDisposableは全て onCleared
が呼ばれた時に破棄されてほしい。
なので ScopeProvider
を実装して onCleared
が呼ばれた時に完了する CompletableSource
を返すようにしてやればよい。
abstract class BaseViewModel : ViewModel(), ScopeProvider { private val onClearedSubject = CompletableSubject.create(); override fun requestScope() : CompletableSource = onClearedSubject @CallSuper override fun onCleared() { onClearedSubject.onComplete() } } class HogeViewModel: BaseViewModel() { init { observable .autoDisposable(this) .subscribe() } }
明日も何か書きます。
Androiderならきっとみんな大好きなはずのThreeTen Backportに関しての雑メモ。
今から100年後のInstantが欲しくなって Instant.now().plus(100, ChronoUnit.YEARS)
ってやってみたけど UnsupportedTemporalTypeException
で死んだ。
コードを読んでみると ChronoUnit.YEARS
はサポートされていなくて以下の ChronoUnit
しか使えなかった。
NANOS
MICROS
MILLIS
SECONDS
HOURS
HALF_DAYS
DAYS
月と年の計算は曖昧な部分(うるう年とか)があるからサポートできないのだろうか。
とりあえず厳格に100年後が欲しかったわけじゃないので plus(36500, ChronoUnit.DAYS)
でなんとかすることにした。
問題のコードはこの辺り↓