LoginSignup
30
33

More than 5 years have passed since last update.

akitaika_ 的 Android コーディング論

Posted at

はじめに

新年明けましておめでとうございました!

えらく期間が空いてしまいましたが、いつの間にか Android 暦5年を突破したので、
ちょっと玄人ぶって
「自分が Android のコーディングをする時に考えていること」
をツラツラと書いていきます。

内容的に、
「仕事だろうがプライベートだろうが Android のコーディングを一度でもしたことがある人」
向けになるかもしれません。

すんごく長くなりました。申し訳ない…。

やりたいこと

  • 自分が Android のコーディングをする時に考えていることを書き連ねる
  • できれば、他の人の反応が見たい
  • 玄人ぶりたい

アクティビティは「画面を管理するクラス」である

人に「アクティビティとは何か?」と問われた時、
自分は「アクティビティは画面の管理クラスです!」と答えています。

まぁ、もちろんこれだけで伝わるとは思っていないですが、
アクティビティやらフラグメントやらの住み分けは以下のように考えています。

  • アクティビティでやるべきこと
    • フラグメントの管理
    • サービスの管理
    • 設定系の確認ダイアログの管理
    • 画面遷移系の管理(startActivityForResult の扱いも含む)
    • 通信系タスクの管理
  • フラグメントでやるべきこと
    • ビューの管理
    • ビューに依存するダイアログの管理
      • 例えば、項目のリストをダイアログ形式で表示し、
        その結果をビューに詰める必要があるもの

ということで、どんどん書き連ねていきます!

アクティビティでやるべき「フラグメントの管理」

なんだか仰々しいですが、
要は FragmentManager とかを利用して行う replace とか add とかの話です。
いきなり拍子抜け感がありますね!

ここで触れることは、

  • 正しい(と思っている)フラグメントの扱い方
  • フラグメントの切り替えはどこでやるべきか

の2本です。

正しい(と思っている)フラグメントの扱い方

説明しやすいので、とりあえず akitaika_ 的 replace の仕方をお見せします。


/**
 * フラグメントの管理
 *
 * @param fragmentManager       フラグメントマネージャー
 * @param containerId           フラグメントを設定するビューグループのID
 * @param fragmentClass         フラグメントのクラス
 * @param arguments             アーギュメント
 * @param transactionController 挿入する固有処理
 * @param checkOthers           他のフラグメントが設定済みかのチェックを行うか
 * @return アクティビティにアタッチされたフラグメント
 */
public static <T extends Fragment> T replace(
        final Context context,final FragmentManager fragmentManager,
        final int containerId, final Class<T> fragmentClass,
        final Bundle arguments, final boolean checkOthers) {
    // 指定のレイアウトに設定されているフラグメントを取得
    final T currFragment
            = findAlreadyFragmentById(fragmentManager, containerId, fragmentClass);
    if (currFragment != null) {
        // すでに設定されているフラグメントがあれば返却
        Log.i(sTag, String.format(
                "Fragment is setting already.(id=%s)", String.valueOf(containerId)));
        return currFragment;
    } else {
        if (checkOthers) {
            if (isSettingFragment(fragmentManager, containerId)) {
                // 違うフラグメントが設定されていたら、nullを返却
                Log.w(sTag, String.format(
                        "Other fragment is setting already.(id=%s)",
                        String.valueOf(containerId)));
                return null;
            }
        }

        // 設定されていない場合は、新しく作成する
        return createCommitFragment(context, fragmentManager, containerId, fragmentClass,
                arguments);
    }
}

/**
 * すでに設定されている指定したフラグメントを取得
 *
 * @param fragmentManager フラグメントマネージャー
 * @param containerId     フラグメントを設定するビューグループのID
 * @param fragmentClass   フラグメントのクラス
 * @return 指定したフラグメント
 */
public static <T extends Fragment> T findAlreadyFragmentById(
        final FragmentManager fragmentManager, final int containerId,
        final Class<T> fragmentClass) {
    Fragment currFragment = fragmentManager.findFragmentById(containerId);
    if (currFragment == null) {
        currFragment = fragmentManager.findFragmentByTag(fragmentClass.getName());
    }

    if (currFragment != null) {
        if (currFragment.getClass() == fragmentClass) {
            // すでに設定されているフラグメントを返却
            //noinspection unchecked
            return (T) currFragment;
        }
    }

    return null;
}

/**
 * フラグメントが指定のレイアウトに設定されているか
 *
 * @param fragmentManager フラグメントマネージャー
 * @param containerId     フラグメントを設定するビューグループのID
 * @return 設定されている/設定されていない
 */
public static boolean isSettingFragment(
        final FragmentManager fragmentManager, final int containerId) {
    final Fragment fragment = fragmentManager.findFragmentById(containerId);
    return (fragment != null);
}

/**
 * フラグメントを作成してコミット
 *
 * @param context               コンテキスト
 * @param fragmentManager       フラグメントマネージャー
 * @param containerId           フラグメントを設定するビューグループのID
 * @param fragmentClass         フラグメントのクラス
 * @param arguments             アーギュメント
 * @return コミットしたフラグメント
 */
public static <T extends Fragment> T createCommitFragment(
        final Context context, final FragmentManager fragmentManager,
        final Class<T> fragmentClass, final Bundle arguments) {
    // フラグメントを生成
    //noinspection unchecked
    T newFragment = create(context, fragmentClass, arguments);

    // 固有処理を実行してコミット
    FragmentTransaction transaction = fragmentManager.beginTransaction();
    transaction.replace(containerId, newFragment, flagmentClass.getName());
    transaction.commitAllowingStateLoss();

    // スケジューリングされた処理を直ちに実行
    fragmentManager.executePendingTransactions();

    return newFragment;
}

実際には、「transaction.replace〜」のところをインターフェース化してたりとか、
人によっては「transaction.commitAllowingStateLoss();」に
「?」と思ったりするだろうとかいろいろありますが、
とりあえず、ここで重要なのは「一度設定したフラグメントを使い倒す!」ということです。

急にフラグメントの話になりますが、
フラグメントには setRetainInstance メソッドというものがあります。
これは、アクティビティを再生成することがあっても
フラグメントのインスタンスを保持し続けるというフラグ設定です。
基本的には、画面の回転対応とかで利用します。

で、察しの良い人ならもうわかったと思いますが、
いくらこの setRetainInstance に true を設定したとしても、
保持しているフラグメントをちゃんと認識できなければ、何にも活かせません!

よく、onCreate のタイミングで「null == savedInstanceState」をして、
フラグメントを設定しているかどうかを判定するコードを見かけますが、
一見何しているかよくわからない上に、なんというか
言葉にできないダサさみたいなものを感じませんか?

ということで、上記のような感じで
ちゃんと FragmentManager を駆使してチェックすると玄人感が出てくると思います。

余談ですが、そんな感じで setRetainInstance を使うと、
画面の回転対応に configChanges を使う必要は無くなると思います。

フラグメントの切り替えはどこでやるべきか

replace とか add とかはどこでやるべきでしょうか。
アクティビティです。

あっという間に結論が出ていますが、
要するに「フラグメント内で getActivity とかを駆使して操作することはやめよう
という話になります。

getActivity は null になることもあるから、null チェックしなきゃね!とか
アクティビティが終了中かの判定もしなきゃいけなくなるから面倒くさいね!とか
あるにはあるのですが、一番の問題点は、
「使いまわせないフラグメントになってしまう」ということです。

さぁ、フラグメントとは元々どういうものだったか思い出してみましょう!
素直に翻訳すると「断片」とかそういう意味になりますが、
フラグメントとは、画面を構成するパーツです。

タブレットなどの大きい画面では、
例えビューの単位を dp など可変にした場合でも、
スマートフォンなどの小さい画面に比べて表示できる情報量に差ができます。

ま、つまりこういう話です。

ということで、フラグメントは以下のようなことを満たしていないといけません。

  • どの画面(アクティビティ)でも利用できる(再利用性が高い)
  • 例え、アクティビティとフラグメントが「1:多」の関係になっても
    他のフラグメントの動きを阻害しない

さて、話を戻しましょう。
フラグメント内で他のフラグメントへの切り替えを行うフラグメントは、
上記を満たしているでしょうか?
答えは、NOOOOOOOOOOO!!!!!!!!! です。

この状態は、完全に切り替え先のフラグメントとズブズブの関係です。

例えば、別の画面では元々定義していた切り替え先のフラグメントとは
別のフラグメントにしたいかもしれません。
また、そもそも切り替えずに別の処理を行いたい場合もあるかもしれないですよね?
そういった場合、同じレイアウトのほぼ同じ処理の専用フラグメントが
どんどん増えていってしまいます。
それはもう強烈にダサい。

で、どうすればいいかというと、
フラグメントにアクティビティへのコールバックを用意して、
アクティビティで処理するようにしましょう。
こういう感じで。

アクティビティでやるべき「サービスの管理」

ここも要するに「getActivity はやめろ!!」という話です。

とはいえ、例えばサービスで処理した結果をビューに反映させたいとかあると思います。

フラグメントが未設定であれば、普通に setArguments を利用して渡せばいいのですが、
すでに設定済みのフラグメントに値を渡したい場合はどうすればいいでしょうか?

答えは、フラグメントの自分で用意したメソッドに値を渡すです。

「それって setter じゃねーか!!」と思われた方、正解です。
「フラグメントに setter とかwダメだろwww」と思われた方、不正解です。

確かに、普通のフラグメントにアクセッサを利用することは NG ですが、
setRetainInstance が true のフラグメントはアクセッサを利用することができます。
ということで、不正解な方は、ここを読みに行きましょう。

アクティビティでやるべき「設定系の確認ダイアログの管理」

設定系の確認ダイアログとは、
「Bluetooth を ON にしてくださいよ」とか
Marshmallow のパーミッション要求などを指します。

まぁ、パーミッションのやつは過去にこんなん書いたけど、何も見なかったということで…。

一見、フラグメントでやっても良さそうな感じですが、akitaika_ 的にこれは NG です。

フラグメントの満たすべき条件を
「フラグメントの切り替えはどこでやるべきか」に書いていますが、
それの2つ目「例え、アクティビティとフラグメントが「1:多」の関係になっても
他のフラグメントの動きを阻害しない」に違反しています。

例えば、カメラ でごにょごにょするフラグメントと
GPS でごにょごにょするフラグメントがあったとしましょう。
どちらも Dangerous なパーミッションが使われていますね!

これらがそれぞれ、onStart とかで機能を使おうとすると、
それぞれのフラグメントがパーミッション要求を行ってしまい、
ダイアログの多重起動というとても残念な事態を招いてしまいます。

なので、フラグメントには利用開始用のメソッドと利用停止用のメソッドを用意して、

  1. アクティビティで設定チェック
  2. 必要あれば、アクティビティで関連のダイアログを表示
  3. 使えるようになったら、フラグメントの利用開始用のメソッドを呼び出す

みたいな感じがいいんじゃないかなと思います。

そういう感じで考えているので、
実は Blutooth を扱うなどの端末の機能を利用するようなロジッククラスなんかも
アクティビティで管理するのが理想的だと思っています。

アクティビティでやるべき「画面遷移系の管理(startActivityForResult の扱いも含む)」

ここは正直、思想によるかなと思います。

主張したいことは
フラグメントの startActivity(startActivityForResult)を使わない
ということです。

自分は、大項目のタイトルの通り、
「アクティビティは画面を管理するクラスである」
と考えています。
なので、
画面遷移もアクティビティで管理するのが自然じゃないか?
と思っているわけです。

よって、自分がメインプログラマであるときは、
フラグメントに startActivity 用のコールバックを用意して、
実質的にはアクティビティの startActivity を利用するようにしています。

onActivityResult については、
リクエストコードをアプリで一意のものとなるように統一させているので、
アクティビティの onActivityResult のタイミングで、
フラグメントの onActivityResult を呼び出すようにしています。

アクティビティでやるべき「通信系タスクの管理」

これも基本的に
フラグメントの再利用性を高めるために、通信系タスクの処理を持たせないべき!
みたいな感じです。

とはいえ、完全に通信系タスクのインスタンスそのものをアクティビティに持たせると、
アクティビティが再生成されるたびに通信処理が走る!なーんて事態になってしまいます。

では、どうしているかというと、
「画面を持たない通信タスクを管理するだけのフラグメント」
を作成し、それをアクティビティで管理するようにしています。

このフラグメントも setRetainInstance を利用しています。
個人的に、setRetainInstance を使わないフラグメントってないんじゃないか?
という感じです。

このように実装していると、
端末を回転させてアクティビティが再生成されたとしても、通信が継続されます。

通信結果を他のフラグメントに反映させたい時は、通信結果をコールバックし、
アクティビティでその結果をフラグメントに振り分けるようにしています。

  1. onCreate でローディング表示用フラグメントと通信タスク管理用フラグメントを設定
  2. 通信タスク管理用フラグメントがライフサイクルに沿って通信を開始
    (画面上は、ローディング表示用フラグメントが表示されている)
  3. 通信タスク管理用フラグメントからコールバックされたアクティビティが
    結果を反映させたいフラグメントにアーギュメントで値を渡して、
    ローディング表示フラグメントの場所にそのフラグメントを置換

なんて流れをよく使っています。

フラグメントは「ビューを管理するクラス」である

もうなんかだいぶフラグメントの話をしてしまった感がありますが、
おさらい的にフラグメント関連の話をしていきます。

ということで、いきなり結論ですが、
フラグメントはビューに関連するもの以外は管理しないようにしましょう!

フラグメントの満たすべき条件をもう一度書いておきます。

  • どの画面(アクティビティ)でも利用できる(再利用性が高い)
  • 例え、アクティビティとフラグメントが「1:多」の関係になっても
    他のフラグメントの動きを阻害しない

フラグメントでやるべき「ビューの管理」

例えば、以下のような仕様を含んでいるアプリを作る事になったとしましょう。

  • 位置情報を取得する
  • 取得した GPS 座標を1つの TextView に「緯度, 軽度」の形式で表示

この仕様に沿って、フラグメントに
LocationManger を利用して座標の取得から表示までを実装したとします。
さて、これは再利用性の高いフラグメントと言えるのでしょうか?
個人的に、これはまったく言えないと思っています。

ある画面では、別のフラグメントと併用して使おうとしているかもしれません。
そういった時に、LocationManager のパーミッションチェックが
他のフラグメントの邪魔をしないと言えるでしょうか?

また、ある画面では LocationManager ではなく、
Fused Location Provider を使いたいかもしれません。
(まぁ、まず普通の仕様ではありえませんが…)
そういった場合に、LocationManager のフラグメントをそのまま扱う事は出来ず、
専用の別のフラグメントを作成するほかなくなってしまいます。

では、どうすべきかというと
「フラグメントではビューないしビューに依存するロジック以外を管理しない」
という事を徹底すべきです。

上記の例に沿った場合、フラグメントには
「緯度軽度を設定する TextView」と
「緯度軽度を受け取って「緯度, 軽度」の形式の文字列に変換するロジック」
のみを持たせます。

こうすることによって、位置情報の取得方法を変えたとしても、
「取得した GPS 座標を1つの TextView に「緯度, 軽度」の形式で表示」部分の
再利用性を高めることができます。

少々強引な例しか出せませんでしたが、もちろん
「このアプリでは、そのような例外的な処理はありえないから実施しない!」
と切り捨てる事は大事です。

ですが、世の中絶対に仕様変更が起きない、などという事はありえません。

Android では、フラグメントの再利用性の高い物を作成していけば、
仕様変更に強いアプリになるんじゃないかなと漠然と思っています。

急にえらく恐ろしい感じの話になってしまいましたね;;

フラグメントでやるべき「ビューに依存するダイアログの管理」

そういうわけで、
「フラグメントではビューないしビューに依存するロジック以外を管理しない」
ことが望ましいのですが、
その関連で、フラグメント内で管理するべきダイアログというものも出てきます。

それが「ビューに依存するダイアログ」です。

例えば、ある TextView をクリックしたら、選択項目の一覧がダイアログで出てきて、
その選択したアイテム名を元の TextView に反映するなんて仕様のダイアログは、
アクティビティでなく、フラグメントで管理するべきです。

しかし、例えば、何かの送信ボタンをクリックして送信しても良いかを確認するダイアログは、
フラグメントではなく、アクティビティで管理したほうが良いでしょう。

さぁ、ややこしくなってきましたね!

この2つのダイアログの違いはどこかというと、
「フラグメント内で完結できる処理か否か」になります。

前者の場合は、ビューから始まりビューに終わるといったような
自身のフラグメント内で全ての処理が完結する形になっていますが、
後者の場合は、送信確認後に
他のフラグメント(通信タスクのみ管理するフラグメントなど)に処理を移す形になります。

もちろん、フラグメントに
送信確認ダイアログを表示するフラグを渡して管理することもできますが、
ダイアログのメッセージを変えたい場合は?とか、
ダイアログのポジティブな処理を変更したい場合は?とか出てくると、
その分のカスタムをドンドン追加せねばならず、結局再利用性が高いとは言えなくなります。

そうするよりかは、フラグメントは
「ビューないしビューに依存するロジック以外を管理しない」物であると割り切り、
そのダイアログ対象のフラグメント内だけで処理が完結できるものかどうかを考えた上で、
それぞれ適切なクラスで管理した方が良いでしょう。

最後に

あまりこういうことを議論できる人が周りにいないので、
思いの丈を書きなぐってしまいましたが、
あくまでも「自分はこう考えている」というだけで、この考えを強制するものではありません。

参考程度に眺めてくださいませ。

ちなみに、完全に力尽きてしまったので触れられなかったのですが、
自分はフラグメントの代わりにカスタムビューを使おうなどとおっしゃっている諸氏に物申したい勢です。

フラグメントはもっとできる子なんだよ!!!!!

まとめ

  • アクティビティは「画面を管理するクラス」
  • フラグメントは「ビューを管理するクラス」
  • とんでもない長文に反省している

参考

  • 今回は特にないです
30
33
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
33