株式会社はてなに入社しました
出遅れた。気がつけば7年。
2022年10月に約1年半運営してきたウマ娘のサークルを解散した。
サークルを解散するまでの1年半で学んだことを2022年も終わるのでここに供養したいと思う。
いわゆるオンゲーのギルドも会社組織みたいなもんだと思うので、仕事にも役立つかもしれない。
細かく言及するようなことでもないので軽く羅列
古くはIRC、今はSlackやDiscordで仕事でもプライベートでもコミュニケーションを取る人間なのでテキストチャットでのコミュニケーションに関する知見はそれなりにあると思うがそれでも学ぶことは多かったと思う。
これスマートフォンネイティブの人がやりがちだと思っているんだけど、スクショは下記のような点で圧倒的にURL+埋め込みの情報表示に劣るので本当にやめた方がいい。
特に上の2つは軽視してる人が多いんだけど、デマが広がったり情報が誤解されたまま解釈されるとかあるので本当に良くない。
ウマ娘愛好会では基本的にWebページのスクショを貼るのを禁止していて、必ずソースのURLを貼るようにお願いしていた。
これはTwitterから拾った画像です、とかYouTubeのスクショとかも同様。どれも該当するパーマリンクが存在するんだからそれを貼ってくれ〜〜〜〜〜〜〜
積極的に導入せよ、いなくて困ることはあってもいて困ることはそんなにない。
あなたがNode.jsの書けるソフトウェアエンジニアなら自分でBot書くのもあり。
ウマ娘愛好会は最終的にサークル加入からメンバー管理、通話の読み上げ、ファン数の管理まで全てBotで行われるようになった。
最低限運営メンバーとそれ以外、くらいのロールはあった方がいい。
運営メンバー以外のロールはeveryoneでもよさそう。
everyoneの権限をデフォルトから削るのは、メンバーにかなりの不便を強いるのでやめるのが吉。
ちなみにウマ娘愛好会ではeveryoneにはデフォルトに+でイベント管理や絵文字管理の権限をつけていた(メンバーでも使いやすくなるように)
最多の頃で120人以上メンバーがいたけど、悪用されたことはない。
話す人間が多くなってくるとテキストチャンネルの分割は必須になってくる。アクティブで喋る人が4-5人くらいでもう分割を考えた方がいいと思っている。
ゲームのDiscordサーバーなんかだと分け方は分かりやすくて、コンテンツとか目的で分けると吉。
その他だと以下のようなチャンネルが作られると良いと思う。
ボイスチャンネルの有無はサーバーの方向性によりそう。個人的にはあってほしい。
聞き専の人の参加ハードル下げるために読み上げ系botは必須。音楽再生Botとかはお好みで。
あらかじめ想定されるチャンネルを作っておくのもいいけど、それ以外に雑多に使えるように一時的なボイスチャンネルを作れるBotを導入したりすると便利。
解散して3ヶ月が経とうとしているウマ娘愛好会のDiscordサーバーだが、未だに毎日誰かが喋っている状態。ここから一緒に他のゲームをやったり、相変わらずウマ娘の話や競馬の話をしたりで盛り上がっている。
跡地としてはまあ成功したんじゃないか。
まとまらなかった。今後もうこんな規模のDiscordサーバーを運営する機会はたぶん来ないが、いい勉強になった。
ウマ娘にはオンゲによくあるギルドみたいな「サークル」という機能があって、サークルごとに1ヶ月で育成したウマ娘のファン数合計で競うランキング機能があったり、キャラ強化用のアイテムをサークルメンバーに寄付してショップでアイテム交換できるポイントをもらったり、みたいなことができる。
自分が所属しているサークルグループ「ウマ娘愛好会グループ」では創設サークルの「シン・ウマ娘愛好会」や昨年12月からグループに加わったランキング上位サークルの「西京ファーム」を始め3つのサークル最大90人が一つのグループとして活動していて、Discordサーバーを共有している。
3つあるサークルは枠としては別モノだが、毎月サークル間で移籍することができたり、運営は3サークル合同で行ってたりしていて加入手続きやメンバー管理、募集枠の確定などあらゆるプロセスが複雑になりがち。 それを解決するために自前でDiscordのbotとWebUIを開発して運用している。
この記事ではそのシステムの概要について書き連ねることにする。
それぞれの細かい内容は別途記事にする。
続きを読むこの記事は 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.