kgmyshin さんの「大きめのAndroidアプリでの設計を考えてみる~pocket~」に書かれているコラムで共感したし、安心した。 このコラムの題名は、「すべての画面遷移を管理するクラスを作るべきか」。
マストバイアイテムなので、今すぐBOOTHの購入ボタンを押そう。
簡単にまとめたい。
過去のアプリは、MVPで設計され、多くの画面にRecyclerViewが使われていた。 アイテムを選択すると、次の画面にジャンプして(=Fragmentを入れ替えて)詳細が見れるようなよくある遷移が多かった。
自分が経験した「すべての画面遷移を管理するクラス」は、だいたい以下のような機能を持っていた。
- ActivityがonCreate時に自身を渡してくるので、シングルトンで持つ
- Fragmentは、様々なActivityのR.id.contentに配置される
- 同一プロセスなら、どこからでも呼べる
- 要求されればstartActivityする
このクラスには、以下のメリットがある。
- どこに遷移するか遷移元で分かる
- ActivityとFragmentのペア事情を気にせずに遷移できる
- Activityは画面遷移でほとんど何もしないので実装をサボれる
画面遷移は好みの別れる所であるが、残念ながら、これは画面数が50を超えるようなアプリには向かない。
どのFragmentからどのFragmentに遷移するか把握はできるが、どのActivityで表示されているかをコードで把握するのは難しく、 表示が予想外な時にActivity終了のパターンに突き当たる。 戻るボタン押下時に複雑になることも想像に難しくない。
それに、タブレット用レイアウトで、左側に一覧、右側に詳細画面を作成しようと考えたら、真っ先に潰す対象になる。
その他のデメリットは、ツイートしたとおり。
どう解決したか
解決方法は簡単で、古典的な方法を採った。 画面遷移はActivityに全て任せ、Activityは自身に配置されるFragmentを全て知り管理し、 FragmentやRecyclerViewのアダプタは 最終的にActivityのメソッドを呼ぶ形である。 もちろん、「すべての画面遷移を管理するクラス」は潰した。
絵にするとこんな感じ。
処理の流れは以下のように変更した。
- RecyclerViewのアダプタのインターフェースをPresenterが実装し、呼ぶ
- PresenterはContractに書かれたインターフェース経由でFragmentに画面遷移を指示する
- Fragmentはインターフェース経由でActivityに遷移を要求する
Bundleに詰め込む値は、インターフェースの引数としてActivityまで渡していく形にした。 これで、「すべての画面遷移を管理するクラス」の「次画面に渡すパラメータを取得するためにRecyclerViewのアダプタがFragmentやPresenterを知っている」必要はなくなり、参照は簡潔になった。
正直な所、RecyclerViewのインターフェースをPresenterが実装するのには異論もあるはず。 RecyclerViewのアダプタはFragmentが知っているのだから、Fragmentが実装すべきではと思うが、 FragmentからPresenterに依頼し、またFragmentに戻ってくることが確実なので、ショートカットした。
副作用として、遷移先Fragmentと同一のものが既に表示中の場合、何もしないと行った制御も可能になった。
実装としては、Android StudioでListテンプレートを選んだ結果と近い。なので、古典的と称した。
TransactionTooLargeException
これでもTransactionTooLargeExceptionは解決できない。
Bundleに詰め込む値は必要なものなので、どうしても次画面に渡さなければならない。
ココの解決策は、デカイ値をシングルトンで持つことで解決した。
SharedPreferencesにJSONで保存する方法もあるが、アプリがクラッシュした後に値を持っていてほしくない事情もあり、シングルトンで持つ形とした。 不要になったら、消す必要はある。
Fragmentを切り出す
エラーの場合の表示だったり、戻るボタン押下で複雑な遷移になりそう、かつ切り出せそうな場合は、新しくActivityを作成して切り出した。
そうすることで、特定のAcitivityだけで管理され、局所化した。
インターフェース地獄
だが、この解決方法だとJavaのインターフェース地獄になるのは間違いない。
とくに、Fragmentが様々なActivityに表示される場合、ActivityとFragmentのペア事情が分からず、 改修初期はテンプレートで生成された通りのRuntime Exceptionのログを見ることになる。
一つ一つぶち当たって対処した。