Android

制約を設けた上でのFragment.Setterは許す / Androidアプリを開発する際の俺的設計

More than 1 year has passed since last update.

@eaglesakura です。

Androidでは画面構築にFragmentを使うことも多いですが、多少クセが多いです。

例えば、画面遷移してバックグラウンドに移ったFragmentはメモリ使用状況に応じて勝手にDestroyされたり、再度Createされたりします。

Fragment登場時から言われている「基本」が幾つかありますが、時代の変化によっていくらか「これは制限を緩めても良いんじゃないか」と思うことがあります。


Fragment初期化の基本

Fragmentを利用する際、基本的には次のように初期化を行います。


  1. 引数なしのpublicコンストラクタを作る

  2. 外部から値を与えたい場合はFragment.setArgments(Bundle)を使用する

これらの制約は、前述の「勝手にDestroyされたりCreateされたりする」をシステム側が行なうためです。

Fragment.setArgments()で設定されたBundleは自動的に保存・復元されるため、Fragmentを扱う際の基本としてはこの方法を使うことになります。


Fragmentのステート保存の基本

Fragmentが一時的に廃棄される場合に備えて、Fragmentの情報を保持する仕組みがAndroidには備わっています。

廃棄はプログラマが明示的に行なう以外にも、システムが自動的に行なう場合があるため、オンメモリで保持しておきたい一時的なステートはここで保存・復元しなければなりません。

public class HogeFragment {

@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// ここでoutStateに現在の状態を保持する
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ここで必要に応じてsavedInstanceStateから復元する
}

}


ライブラリを組み合わせる

2016年現在、Bundleに対してチクチクとputHoge("Key", value)しているプロジェクトは少なく、大抵はIcepick等のライブラリで保存をさせてしまっているかと思います。

// 大昔のコード

public class HogeFragment {
int mHogeDefault;

int mHogeCount;

@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// ここでoutStateに現在の状態を保持する
outState.putInt("Hoge", mHogeCount);
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ここで必要に応じてArgmentsやsavedInstanceStateから復元する
mHogeDefault= getArgments().getInt("HogeDefault");
if (savedInstanceState != null) {
mHogeCount = savedInstanceState.getInt("Hoge");
}
}
}

// 2016年現在はIcepick等のライブラリで保存・レストアが一般的

public class HogeFragment {
@State
int mHogeDefault;

@State
int mHogeCount;

@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ここで必要に応じてArgmentsやsavedInstanceStateから復元する
mHogeDefault= getArgments().getInt("HogeDefault");
Icepick.restoreInstanceState(this, savedInstanceState);
}
}

ここまでが前提として、setArgments()も同じく、わざわざBundleにKey-Valueで詰め込まなくても、保持が保証されているのであれば次のようにしてしまっても実用上の問題は発生しません。

Factory/Builderのようなオブジェクトもこちらのほうが楽に作れます(Keyを考えたりする必要もない)。

// モノグサなコード

// RequireなパラメータならFactory/Builderを作るほうが安全
public class HogeFragment {
@State
int mHogeDefault;

@State
int mHogeCount;

public void setHogeDefault(int value) {
mHogeDefault = value;
}

@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Icepick.restoreInstanceState(this, savedInstanceState);
}
}

当然ながら、FragmentのライフサイクルやDestroy/Createが行われる可能性等の基本的な知識を持っているという前提があります。

わざわざArgmentsを解する(基本だからという理由でそれを強制する)より、こちらのほうが楽に作れるので、ケースバイケースではあるが「外部から与えられる初期値は必ずBundleを使用する」という超・強力な強制をするよりも設計の自由度が上がります。


最後に

あくまで基本を知っている上での変形なので、「とりあえず保存用のAnnotationつけといてね」という方法では痛い目を見ます。

特にAndroid慣れてない系開発者がBundleに突っ込めない系オブジェクト(シリアライズをサポートしていなかったり)を突っ込もうとして復元で死ぬというリスクもあるので、そういう人にはレビュアーが丁寧に殴れるような体制を敷きましょう。

「Argmentで設定しているコードは通す」

「Bundle保存に留意しているコードも通す」

「「何も考えてないコードは通さない」」