Fragments vs. CustomViews に一つの結論を出してみた

  • 242
    Like
  • 1
    Comment
More than 1 year has passed since last update.

前提知識

ざっくり言うと

  1. Fragment はライフサイクルが複雑で、フレームワークそのものが複雑なので、モジュラーな設計をするには大きすぎる
  2. View を継承して自分で CustomView を作るほうが、余分にライブラリを追加する必要がなく、古い端末も同時にサポートできる
  3. そもそも Activity も Fragment も Controller なのでロジックをそこに書いたらダメ

読んでおきたいリスト

  1. Fragment は本当に多様なデバイスへ対応する唯一の方法なのか
  2. Square Fragmentやめるってよ
  3. Advocating Against Android Fragment
  4. 【翻訳】Android Fragmentへの反対声明
  5. FragmentManager#executePendingTransactions() が怖くて使えないあなたへ

Fragments

知らないとハマる Fragment

Fragment には幾つかの知っておかないと困る、かつ毎回気にしないといけないハマりポイントが有ります。

  1. デフォルトコンストラクタを public にしないといけない
     Activity の再生成に伴って Fragment を再生成するとき、FragmentManager はリフレクションによってデフォルトコンストラクタを呼び出し、インスタンスを作って状態を戻そうとするので、デフォルトコンストラクタにアクセス出来ないものはインスタンスの再生成に失敗します。この制約から、匿名クラスを使った Fragment の取り扱いも不適切となります。
  2. FragmentTransaction#commit() が非同期に動く
     Handler を使ってメッセージキューに操作を積むのがお仕事なので、メッセージキューが詰まっていると、Fragment の状態が宙に浮いた期間ができます。また、実際に操作が実行されるタイミングですでに Activity のライフサイクルが終了(Activity#onSaveInstanceState()を通り過ぎた)している場合、Fragment の操作が許可されないため例外が飛びます。
     Fragment が持っている状態が消滅してしまっても構わない場合、FragmentTransaction#commitAllowingStateLoss()を使うことで、ライフサイクルが終了していても操作ができます。
     あるいは、同期的に操作するのならば、FragmentManager#executePendingTransactions()が利用できます。いずれにしても少し仰々しい操作をしている感じがしますが、意味合いはこの通りです。
  3. Activity との通信のためにインタフェースを使うときの方法を間違えやすい
     前述のとおり、リフレクションによるインスタンスの再生成が行われるので、コンストラクタは勿論、setter を経由した方法も取れません。唯一、Fragment が Activity にアタッチされた時のコールバックで Activity をインタフェースへキャストしメンバに保持する方法が正しく動作します。
  4. Loader との相性が地味によくない
     LoaderCallbacks のメソッド上で Fragment の操作は許可されていません。前述のとおり、コールバックが呼ばれる頃に Activity が生きている保証がないためです。状態の喪失を覚悟してでも Fragment を操作するか、Handler を経由して、操作を遅延させるかのいずれかの方法を取る必要があります。
  5. onActivityResult() がややこしい
     Activity にも、Fragment にも onActivityResult() がありますが、startActivityForResult() を呼び出したのが誰かによって、onActivityResult() が呼ばれる場所が変わります。Activity が呼び出したら Activity へ、Fragment が呼び出したら Fragment へ返ってきます。これは、requestCode の上位16ビットをいじってどこから呼び出したかを判別しているために起こります。ただし、Activity の実装で親の onActivityResult() を呼ばないと悲しい目に合います。

Fragment の複雑性

これだけのハマりポイントがあるのは、それだけ Fragment が複雑な仕組みで成り立っていることから来ています。

  1. Fragment のライフサイクルコールバックが多すぎる
     Activity のライフサイクルと連携するため以外にも様々なコールバックが呼ばれます。どれがどの順番で呼ばれ、再生成ではどこから呼ばれるのか。図を見ながらデバッグしていては時間がかかりますが、図を見ずにすべてを覚えておけるほどの量を超えているため、結果的に毎回図を見ることになります。
  2. FragmentManager の闇が深い
     FragmentManager 自体は抽象クラスで、その実装は FragmentManagerImpl にあるところから始まり、実際のライフサイクルを管理しているメソッドなどは巨大な switch 文があって追いかけるのに非常に苦労します。

Fragment を使いたくなる場面

それでも、Fragment の仕組みを用いることで簡単に実装できることもいくらかあります。

  1. 画面遷移のスタック管理を FragmentManager がしてくれる
     Fragment の切り替え等を一つのエントリとして、バックスタック管理ができます。これによって、パンくず UI が実現できます。擬似的に Activity の遷移をしているような感じにできますが、実際には Activity は 1 つで、Fragment がどんどん切り替わっていく形になります。バックスタックのデータ構造は FragmentManager に管理を任せることができ、アプリは、単純にスタックに積むのと取り出すのとを適切なタイミングで実行するだけで良いことになります。
  2. Fragment の遷移にアニメーションを適用する
     FragmentTransaction を用いて、トランザクションの実行時(Fragment の操作時)に適用するアニメーションを指定することができます。これによって、Fragment を切り替えた時に動きを付けることができます。
  3. ViewPager と共に、Fragment を添えて
     ViewPager のページコンテンツに Fragment を用いることができるよう設計されており、それぞれページごと独立してモジュールが動くようになると、Activity は ViewPager に FragmentPagerAdapter をセットするだけで物事が勝手に動き始めます。これはこれで使い勝手よく実装できる点です。

閑話休題:Fragment の良くない実装のサイン

  1. getActivity() の返り値を具体的な Activity にキャストしている
     Fragment は Activity に紐づくため、Fragment#getActivity()によって Activity のインスタンスが得られますが、これを具体的な Activity にキャストするということは、直接その Activity に依存することになり、せっかくの Fragment のモジュラーな設計が無意味になります。

CustomViews

古き良きフレームワーク

もともと View 自体にもライフサイクルがあり、また、Fragment にできるほとんどのことは View でもできるようになっています。Support Library を必要としないため、設計さえきちんとしていれば Fragment なしでも多様なデバイスへ対応する事ができます。

  1. リソースを分離する
     タブレット用、ハンドセット用、などデバイスごとのリソースの分離は、CustomView であろうと Fragment であろうと必要なことです。
  2. 状態管理も View で行う
     Activity や Fragment で状態管理するために必須のメソッドたち(onSaveInstanceState, onRestoreInstanceState) は View にもあります。扱い方は View のほうが特殊ですが、Parcelable の扱い方を知っていれば何も苦労することはありません。
  3. バックスタック管理ができる
     View の切り替えだけならば、同期処理で実現できます。これで、Fragment のような状態が不定になる状況もあり得ません。必要であれば、Square のライブラリも使うことができます。

View では物足りなくなる場面

もちろん、古きよきフレームワークは、時代の要求に合わなくなる面も存在します。

  1. Loader と共に使いにくい
     Activity や Fragment から LoaderManager を渡して、コールバックを View で受ければ…という設計にもできますが、それをするくらいなら Activity や Fragment で LoaderManager を管理して居る方が余計な複雑性を生まずに済むでしょう。

パターン化

ある程度 Fragment の良い所面倒なところ、View の良い所面倒なところを整理したところで、潰しの効きそうなパターンを見つけていきます。

コントローラ層

Activity も Fragment もコントローラとしてビジネスロジックを動かす(kickする)、View にデータを渡す、といった役割を持つことになります。2 つの違いは、Fragment はモジュラーに動作する Activity で、Activity はドメイン毎に分離された画面の単位、という感じになるでしょうか。

少なくとも、データを Activity 間だけでなくビジネスロジックや View へ渡すためには、データを表現するクラス(Entity)をきちんと設計しておく必要があります。これがないことには、ビジネスロジックも View のロジックも容易に複雑化していくことでしょう。

View のロジック

View 自身がデータを受け取って、自立してデータを View にアサインしていく流れが自然です。あるいは、コントローラに Helper オブジェクトを持たせて、View にアサインしていくロジックをその Helper に委譲していく方法(Presenter)もあり得ます。いずれにせよ、Activity や Fragment から委譲することが重要で、これをしない限りテストのコストは上がるばかりです。

ビジネスロジック

いわゆるモデルです。ロジックの開始とその結果についてデータ構造を決めておくことで、Activity、Fragment から分離しやすい状態になります。注意点として、Activity や Fragment のライフサイクルに合わせてコールバック等の面倒を見てやる必要があることでしょうか。死んだオブジェクトにコールバックを返すことを避ける仕組みが必要です。

インタラクション

ユーザがある操作をした時、モデルをどのような順序で操作し、結果を得るかを定義できれば、おそらく Activity や Fragment からはロジックが消え去っていることでしょう。DCI(Data/Context/Interaction) やロールオブジェクトのようなパターンを用いる方法が挙げられます。

まとめ

大事な結論

  1. コントローラにビジネスロジックを含まないようにする設計を最初に考える
  2. Fragment はモジュラーな Activity
  3. View の操作に関するロジックはコントローラの外に追い出す

あわせて読みたい

Android な話題は以下を。

  1. 今さら聞けない Activity と Fragment の使い分け
  2. やさしい設計 〜 Android 編
  3. EventBus はどこでつかうべきか
  4. コールバックと上手に付き合う

プログラミング全般の話題は以下を。

  1. MVCの流れを簡単にまとめてみる
  2. 新人プログラマに知っておいてもらいたい人類がオブジェクト指向を手に入れるまでの軌跡
  3. 「オブジェクト指向プログラミング」と「関数型プログラミング」のたった一つのシンプルな違い
  4. 何かのときにすっと出したい、プログラミングに関する法則・原則一覧