Android Architecture Componentsを使ってリファクタリング

Yoshihisa氏:では、よろしくお願いします。今日はAndroid Architecture Componentsを使ってリファクタリングした話をします。

Takeda Yoshihisaと申します。

今、株式会社Diverseのマッチングアプリの「YYC」のAndroidアプリを開発していて、Androidデビューから1年経過しました。あと光の戦士をやっています。

今日は、マッチングアプリでよくある、ユーザー間のメッセージをやりとりする履歴を表示する画面のリファクタリングをした話をします。ユーザー履歴の画面なんですが、ユーザー間のメッセージのやりとりをチャット風に表示する画面です。

この画面はユーザーの状態、性別、18歳以上の確認が済んでいるかどうかとか、これまでやりとりをしたことがある人かどうかで、いろいろ表示が変わります。ちなみに、このスクリーンショットは女性、年齢確認済み、相手の男性にこれまで一度もメールを送ったことがないという人です。

さっきのこのスタンプのパレットが表示される前の構造はこんな感じになっています。

Activityの上にRecyclerViewがいて、そこに表示するかたちになっている。あと、メッセージの入力フォームは別のFragmentです。わりと単純で構造としてはこんな感じなんですけど、もうこの時点で、すでにやばい香りがぷんぷんします。

さらに、Ver2.9.23から、女性向けにさっき下のほうに出ていたスタンプの機能が追加されました。ただし、条件付きで、送信先は男性だけです。送信先の男性に今まで一度もメールを送ったことがないときに表示されます。

見た目としては、スタンプのパレットをフォーム上に吹き出す感じで出して、入力フォームの切り替えボタンを押すとパレットが開いたり閉じたりする。あと、パレットにも閉じるボタンがあって、それを押すと、それまた閉じたり開いたりして、フォーム上にある切り替えボタンが赤色になったりします。

構造として、さらに赤字で表示しているところが追加されました。

まぁ、図にするとこんな感じで……。

もう地獄ですよね(笑)。

(会場笑)

さらに、年齢確認の有無によって、本文が読めなくなったり、メッセージの送信に制限があります。

地獄が生まれました。これをどうにかした話をします。

“地獄”が誕生した原因

どうしてこんなことになったかという、そもそもの原因を話すと、典型的なFat Activityで、これを作った当時、ActivityとFragmentでロジックと状態を共有するベストな解決方法が思いつきませんでした。さらに、リファクタリングに取り組む時間が取れず、今の仕組みの上にどんどん機能を足していったという感じになっています。

このActivityとFragmentでロジックと状態をうまく共有する解決方法が出てきました。Android Architecture ComponentsのViewModelとLiveDataを組み合わせたらうまくいくんじゃないかということで、入れてみました。

軽く説明しておくと、ViewModelというのは、Activityが回転したときのデータ保持とか、複数のFragment間のデータ共有ができるやつです。LiveDataというのは、Lifecycleに従ってイイ感じにしてくれるデータホルダーです。LifecycleOwnerがSTARTEDまたはRESUMEDのときにアクティブになって通知してくれるやつですね。

LiveDataを使うとなにがいいかって、非同期処理でありがちな「処理が完了して、値を通知して、UIを操作しようとしたけど、肝心のUIは、例えば画面が閉じたりして非アクティブで、クラッシュしちゃう」みたいなことがないです。

「道具は揃った。さぁやるぞ」と思って、すぐに手をつけようってしたら、まぁだいたい失敗しますよね。だいたいリファクタリングが失敗する要因って、既存のコードを理解しないまま手を付けるとか、いきなり全部きれいにしようとするとか、カッとなって全部書き直すとかっていうのがあると思うんですけど、まずは手をつけるところをちゃんと決めましょう。

方針の策定

方針として、こんな感じで決めました。

まず、送信に関係する状態とロジックをViewModelに分割して分離する。プロフィールの取得とか、会話の履歴を取得するところともあったんですけど、これは送信の状態からはほぼ独立していたので、いったん後回しにしました。

送信状態で取りうる状態というのは画面全体の表示状態と関連が薄く、送信中は送信ボタンが消えてクルクルしたり、送信完了後スタンプパレットの表示状態が変わるぐらいでした。

ViewModelはステートマシンとして実装しました。これはなんでかというと、ほかの画面で実績があったからです。

そして、その状態を保持するホルダーはLiveDataを使いました。LiveDataを使った理由はとくになくて、正直なところ、ちょっと使ってみたかっただけです。ライフサイクルに正しくバインドできればRxを使ってもよいと思います。

ここがわりと重要なところで、新規のクラスのVMの周りはKotlinで、既存のクラスはJavaのまま我慢しました。さっきのリファクタリングが失敗する要因で、「いきなり全部書き直そうとする」というのは失敗する要因ですね。

では、この方針に従って順番にやっていきます。まず状態を落とし込んでいきます。

失敗したときの例外を一緒に持たせたいので、ステートマシンに使う状態というのはenumじゃなくてsealed classで定義しました。SendMailStateという名前でクラスを定義して、「まだなにもしていない」、「送信中」、「送信成功した」、「失敗した」。

ViewModelの実装

ViewModelの実装というのはこんな感じです。

使うLiveDataは2つです。MutableLiveDataは、内部ではこいつにpostします。privateになっているのは、外から値を変更されたくないので公開はしないです。もう1つは公開用のLiveDataで、使う側はこいつをObserveするようにします。

これが正しいかどうかちょっとわかりませんが、初期値はinitでセットしています。

これが実際にメールを送るところです。

まずLiveDataが抱えている状態を一番上のif文でチェックして、送信中ならなにもしない。「getValue」メソッドで今抱えている値を取り出せるので、それを使っています。

送信中以外の場合はそのまま処理が進んで、今から送信するので「送信中」にセットします。そして、APIのレスポンスに応じてLiveDataに状態をセットしていきます。

このときに注意しないといけないのは、setValue、postValueというのがあるんですが、setValueのほうはUIスレッドだけです。IOスレッドで呼ぶと例外を起こします。なので、このコードではpostValueを使っています。

あとは、onCleared()というのは、これはViewModelが死ぬ時に呼び出されるやつです。

サブスクリプションをクリアするとか、そういう後始末に使います。

次に、ViewModel Factoryというのは、ViewModelのコンストラクタになにか引数を渡したいときに使うものです。ViewModelは直接newしてはいけません。なので、Factoryを定義してインスタンスを作ります。

処理の移動と状態遷移の追跡

では、実際にこのViewModelを使う側の話をしていきます。

まず、ActivityのonCreateでさっき定義したVMを生成します。そして、メッセージの入力フォームとスタンプパレットで、Activityの送信メソッドを呼んでいる箇所をVMの送信メソッドに置き換えます。VMが持っている送信状態をActivityと各FragmentでそれぞれObserveして、状態に応じてUIを変更する処理を実装します。

VMの初期化のコードはこんな感じです。

まず、ActivityのonCreateでFactoryを指定してVMを作ります。VMの状態を抱えているLiveDataがあるので、それでObserveしてハンドリングしていきます。このLiveDataはActivityがアクティブじゃないと通知してこないので、クラッシュしたりというのはないと思います。

これってメールの履歴画面はJavaで書かれているんですけど、if(~ instanceof ~)とかで気合でがんばっています。

あと、中で失敗したときは例外を取り出さないといけないんですが、それもいちいちキャストしてやったりします。なので、Javaにもwhen式とSmart Castほしいなってずっと思っています。

さらに、入力フォームとかスタンプパレット側でも同じことをします。Activityがオーナーになっている送信VMを取得します。Fragmentでも同じような感じでVMの状態をハンドリングします。

これは送信フォームの抜粋なんですが、送信フォームはJavaで書かれているので、when式とSmart Castを本当くれという気持ちでコードを書いていました。

リファクタリングの結果

2.9.23の構造はこんな感じになっていました。

リファクタリング後はこんな感じになりました。

すっきりした感があるんじゃないでしょうか?

さらに、実はこのあとちょっと作業を進めて、送信以外の部分についてもこれと同じような感じでリファクタリングを進めていきました。

プロフィールとメッセージの履歴取得の部分についても同じようにやりました。そうすると、Activityの行数は651から488に減りました。まだ多いですが、それでも多少すっきりしたと思います。

なんで減ったかというと、Activityが抱えていた入力フォームとかスタンプパレットの状態決定を、それぞれObserveして自分たちでやれという感じで移していったからです。まぁ減ったので、多少、機能の追加とか改修もやりやすくなったと思います。

これがリファクタリング後で、赤がメッセージ送信リクエストとその結果の通知です。

緑がプロフィールの取得と、あとやりとり履歴の取得の通知の結果です。

APIを通信をするようなところはほぼ全部ViewModel側に乗せることができました。あとはスタンプパレットの表示切り替えボタンを押したときとか、そういうのを追い出していけば、さらにActivityはダイエットできると思っていますが、まだやっていません。

では、まとめです。Android Architecture ComponentsのViewModelはいいぞ、と。Activityの回転とかFragmentのデータ共有に使える。なので、ぜひ積極的に使っていけば幸せになるかなと思います。

リファクタリングするときは、まず、いきなり手を付けずに、方針を決めましょう。ときには泥臭く地道にやっていくことも大事です。進めているうちにさらに良い解が見つかることがあるので、随時軌道修正しながらやっていくと、より良いものができると思います。

ちょっと早いですが、これで終わらせていただきます。ありがとうございました。

(会場拍手)