DroidKaigiのIssueに思い切って手を挙げてみたらすごく学びが深かった
はじめに
いきなり結論ですが、
Android開発初心者も思い切ってIssueを担当してみると圧倒的成長につながるよ
これがこの記事で1番言いたいことです。
具体的にはどういうことか、自分はどんなことをしてその結論に至ったのかを書いていきたいと思います。
対象の読者は、DroidKaigiをご存じない方、Androidにそれほど自信はないけどOSSへのContributeをしてみたい方です。 自分は初めてのOSS Contributorになれて嬉しさ満載なので、その気持ちをこの記事にぶつけます。
バックグラウンド
自分の経歴や開発経験は過去記事を読んでいただければ分かるのですが、ざっくりまとめると、
と言う感じです。
DroidKaigi
DroidKaigiはエンジニアが主役のAndroidカンファレンスです。
日本国内で最大級のAndroid技術カンファレンスです。
自分は1年前はDroidKaigiの存在をしらなかったため、参加するのは今回が初めてですが、各地の最強Androidエンジニアが長年培ってきた技術をとてもわかりやすく共有する場って、かなりワクワクしますね。
開催日は2020年2月20日(木)〜2020年2月21日(金)で、まだチケット販売もおこなっているそうなので、まだの方は是非申し込んでみることをお勧めします。
OSSなGithub Repository
こちらは、カンファレンス当日のタイムスケジュールやフロアマップをスマートフォンからみることができるアプリのソースコードです。
毎年、DroidKaigiのアプリを各地のAndroidエンジニアが有志で作っています。
例年、カンファレンスの約1ヶ月強前までにある程度実装されたこのRepositoryがpublicになります。
まだ一部機能が実装されていなかったり、バグが存在する状態なため、これまた有志のAndroidエンジニア達がContributorとして追加開発していく流れです。
2020年1月22日(水)現在では、合計100名ほどのContributorがいらっしゃるらしいです。
この、全体で知見を共有しつつ、最高のアプリを作っていく感じ、素晴らしいコミュニティ/カンファレンスだと思います。
そして1番何が良いって、
その年の最新のトレンドをふんだんに使用したコードなため、知見の宝庫なのです。
エンジニアという職業柄(まだ就職していませんが)技術のトレンドは毎年変わっていきます。流行り廃りが激しく、最近までのデファクトスタンダードが今はすでにレガシーになってしまったり。
毎年、この時期にこの 知見の宝庫 が共有されることで、キャッチアップがしやすくなる利点があります。
最強の教本ですね。
Issue
上述したように、DroidKaigiアプリは有志のエンジニアが募ってアプリを完成させていくため、好きなIssueを担当し、PRを出すことができます。
そのIssueは、運営?メンバーの方が立てたものはもちろん、自分がアプリを触っているうちに発見したバグや欲しい機能をIssueに追加することができます。
そのIssueの難易度にはかなり幅があり、
- Layoutのmerginを調節する
という簡単なIssueから、
- iOSアプリを対象にMPP(MultiPlatform Project)を進める
という力量が問われるIssueまで存在します。
冒頭でも行った、
Android開発初心者も思い切ってIssueを担当してみると圧倒的成長につながるよ
に繋がるのですが、
割と easy
Labelの貼られたIssueが多く、誰でもContributorになれるチャンスがあるのです。
担当したIssue
実際に担当したIssueはこの2つです。
- スタッフ一覧を表示する画面に関して、ViewModelにUiModelを導入する、
- 上記のViewModelTestの記述
これらのLabel、 easy
なんですよ。
UiModelってなに?テストなんてかけないよ って思われる方もいるかもしれません。なんなら最初見たときは自分もそうでしたが、
既にUiModelを導入したViewModelや、テストを書いた画面が存在するため、基本的にはその実装を模倣すればできるのです。
これが easy
である所以ですね。
ただまあ、コピペエンジニアは自分のポリシー的にもタブーなので、しっかりと理解して自分なりに実装してみつつ、あくまで他の画面は参考にするという方針でコードを書きました。
この記事は、同じAndroidエンジニアの友人へ向けた(自分なりの)コード解説を含んでいるので、
下記に UiModelとは、ViewModelTestに関して少し言及したいと思います。
※解釈に誤りがあれば教えてください🙏
これをみる前に、まずご自身でDoroidKaigiリポジトリのREADMEを一読してからのほうが理解が深まると思います。かなり丁寧に書いてくださっているので。
README見たらだいたいわかったという人は飛ばしてもらってOKです。
UiModel
StaffsViewModel
class StaffsViewModel @Inject constructor( private val staffRepository: StaffRepository ) : ViewModel() { data class UiModel( val isLoading: Boolean, val error: AppError?, val staffContents: StaffContents ) { companion object { val EMPTY = UiModel(false, null, StaffContents.EMPTY) } } // 略 }
自分が担当したIssueで追加したUiModelです。
UiModelというのはおそらく、UI(ActivityやFragment)側の状態を司るクラスだと思います。
実際にUiModeのコンストラクタでは、下記の3つの状態があります。
- isLoading
- ローディング中かどうか
- error
- AppErrorはseald classで
ApiException
等々のExceptionを複数定義しているクラス - errorではない可能性があるためNullable
- AppErrorはseald classで
- staffContents
- 表示するスタッフのリストを保持したModel
- repository等でデータ取得できればここで保持する
何も考えずにやるとすると、Loadingの状態をLiveDataとして普通にViewModelのメンバに持たせることになるかと思いますが、UiModelを挟むことで状態が散らばらずに一元管理できて便利なのでしょう。
StaffsViewModel
private val staffContentsLoadState: LiveData<LoadState<StaffContents>> = liveData { emitSource( staffRepository.staffs() .toLoadingState() .asLiveData() ) staffRepository.refresh() }
ViewModelのここでは、Repositoryを経由して StaffContents
を取得し、それをLoadingStateに変換し、さらにLiveDataとして扱うよ〜〜というコードですね。
Repositoryを経由しているのは、Entityの取得をDatabaseから取得するかApiから取得するかを、ViewModel以上で知らなくて良くなるためです。
責務の分離ですね。
内部的には StaffRepository#staffs
ではDatabaseから取得。 StaffRepository#refresh()
はAPIを叩いてからDatabaseに最新のスタッフ情報を格納する挙動になっています。
これらの挙動をViewModel等が意識しなくて良くなるため、Databaseの種類が変わろうとViewModel以上には影響がなくて済むのです。
StaffsViewModel
val uiModel: LiveData<UiModel> = combine( initialValue = UiModel.EMPTY, liveData1 = staffContentsLoadState ) { _, loadState -> val staffContents = when (loadState) { is LoadState.Loaded -> { loadState.value } else -> { StaffContents.EMPTY } } UiModel( isLoading = loadState.isLoading, error = loadState.getErrorIfExists().toAppError(), staffContents = staffContents ) }
これも、やっていることは簡単です。
combine
という、内部的には MediatorLiveData
と同様のことをしている拡張関数で、引数を数個とることができます。
combine
に渡したLiveDataの値に変更があれば、ラムダ式が実行されて、UiModelが更新されるという内容です。
今回はliveData1だけに止まっていますが、他にも hogeLoadState
なるLiveDataがあった場合には、
staffContentsLoadState
hogeLoadState
のどちらかに変更があった場合にラムダが実行されます。
そのため、UiModelのisLoadingは
isLoading = staffContentLoadState.isLoading || hogeLoadState.isLoading
のように変えてあげる感じです。
詳しくはリポジトリの SessionDetailViewModel
で複数バージョンのものが実装されているので見てみてください。
StaffsFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { //略 staffsViewModel.uiModel.observe(viewLifecycleOwner) { uiModel -> progressTimeLatch.loading = uiModel.isLoading groupAdapter.update(uiModel.staffContents.staffs.map { staffItemFactory.create(it) }) uiModel.error?.let { systemViewModel.onError(it) } } }
あとはFragmentのonViewCreatedでobserveして、UiModelの状態に応じてUIを切り替えてあげるだけです。
もしこれがUiModelとしてまとめておらず、ViewModelにLiveDataを乱立させていたら、observeが乱立し、UI更新処理が分散してしまい処理が追いづらくなるのです。
これですっきりまとめられましたね。
ちなみに、progressTimeLatchに関して、詳しくは内部実装を読んでいただきたいのですが、Fragmentでこのような実装がされていました。
StaffsFragment
progressTimeLatch = ProgressTimeLatch { showProgress -> binding.progressBar.isVisible = showProgress }.apply { loading = true }
ざっくりいうと、 ProgressTimeLatch
のメンバ変数である loading
の値を変更すると、上記のラムダ式が実行されるって言う感じですね。
上記はラムダ式の定義と同時に loading
をtrueにしているため、 binding.progressbar
がVisibleになる感じですね。
おわりに
思ったより長くなってしまった...
結論何が言いたかったかと言うと、繰り返しになりますが。
Android開発初心者も思い切ってIssueを担当してみると圧倒的成長につながるよ
です。
最初は何書いてるか全くわからん状態かもしれません。しかし、DroidKaigiは優しいため、大抵他の画面で参考になるコードがあったりするので、 easy
Labelであれば後先考えず
「🙋♂️」
とコメントしてしまうと良いかもしれません。
広く全体のコードを読もうとするから全くわからん状態になってしまうのかもしれません、 easy
なIssueを引き受け、一旦は狭い範囲を重点的にみて、他のコードを参考にしつつ理解を深めて全体に視線を広げていく方針はお勧めです。
おまけ
ちなみに、
easy
なIssueなんてすぐ埋まっちゃうよ!!
みたいな人は、GithubとSlackを連携させてIssueが立ったら自動的に通知してくれる仕組みを導入すると見逃しがなくなったり、素早く反応ができるためお勧めです。
これを見ればすぐに設定ができるのでやり得です。