Android Design のパターンを実装してみた(Patterns - Multi-pane Layouts)
Android Advent Calendar 2012 12/18(裏) 担当の @daichan4649 です。
よろしくお願いします。
経緯
Android Design のサイト、ありますよね。Android公式から提示されてるガイドライン。アプリはこう作ってねとか、画面遷移はこういう風に、とか、アイコンはこういうサイズで、とか。しかし、コード例へのリンクがない。理想のパターンを定義するなら一緒にコードも公開してくれたらいいのに。
なんて思いから 「自分ならこう実装する」 というのをやってみました。実際にはどう実装するのがベストプラクティスなのかわからないので、コード晒すのでみなさまぼこぼこにしてください。 #ドM
Multi-pane Layouts
Android Design にはいろんなパターンが書いてありますが、まずは Patterns の中から Multi-pane Layouts をチョイスしてみました。
Combining Multiple Views Into One 1. 同一apk で 小さい端末では 1pane 、画面大きめな端末では 2pane にする 2. 親(リスト) を選択すると 子(詳細) を表示する
よくある 2pane なアプリのパターンについて、ですね。とりあえず、このパターンのサンプルアプリ作ってみました。サイトではタブ表示になってるけども、今回そこは関係ないのであえて無視してます。
スクリーンショットです。
大きめの端末(Nexus7)
初期表示
リスト選択
それではアプリの実装について説明してみます。
1. クラス構成
- Activity 1つ
- Fragment(リスト)
- Fragment(詳細)
ここはシンプルに。
2. レイアウト
本当はもっと細かくサイズ指定すべきなんだろうけど、めんどくさいのでこんなかんじで2つ用意。
- 画面小さめな端末向け(/layout)
- 画面大きめな端末向け(/layout-large)
3. Fragment の表示/入れ替えロジック
画面小さめな端末(1pane)
画面大きめな端末(2pane)
- 初期表示
- 左側に Fragment(リスト) を表示
- 右側に コンテナ を表示(空)
- リスト選択時
- 右側のコンテナ を Fragment(詳細) で replace
xml ごとに構成を変えます。
で、この手の Activity1つ、Fragment複数、なパターンを使ったときにいつも悩むのが データの管理。コンフィグ変更(縦横切替等) で Activity が再生成された際の挙動を考慮して、こんな感じで考えてみました。
4. データ管理
CRUD
Fragment から Activity を参照(Fragment#getActivity)し、「必ず」 Activity 経由でデータをいじる。
保持
Activity 内でデータ保持
今回は 「コンフィグ変更(Activity#onDestroy)時にデータ破棄」 するような作りにしているため、Activity で単純にデータ保持しています。「コンフィグ変更時にデータ破棄」 させたくない場合は Fragment#setRetainInstance(true) な Fragment で管理すべき、だと考えてます。このあたりについては前に以下のエントリでまとめてみました。
Activity再生成時のデータの保存・復元(Fragment#setRetainInstance)
これを基にすると、各 Fragment でデータを表示するまでの流れはこのようになります。
5. Fragment でデータ表示するまでの流れ
- Fragment(リスト)
- Fragment のインスタンスは Activity 表示時 にレイアウトから生成
- onActivityCreated で Activityからデータ取得
- adapter に取得したデータを設定
- Fragment(詳細)
- Fragment のインスタンスは リスト選択時 にコードから生成(引数: 選択位置)
- 生成時に引数で渡された 選択位置 を内部保持(Fragment#setArguments)
- onCreateView で ↑の 選択位置 を基にして Activity から 表示用データ を取得(Fragment#getArguments)
アプリの構造、設計はざっくりこんなかんじです。コード本体は ここ(github) に置いてます。今回記載した以外のパターンについても実装したら(需要はおいといて)公開していこうと思っています。
最後に
去年 に引き続き、今年もこのようなすばらしい企画に参加させていただき、主催の @youten_redoさん、参加者の皆さま、本当にありがとうございます。来年も参加できたらいいな!明日 12/19 の担当は @androidsolaさん(表)、@furusin_oriverさん(裏) です!
追記
事件です。最新のADT (21.0.1 現在)では、マルチペインな雛形を自動生成できることが発覚しました!!m9(^Д^)
あれ、目から水が。
LruCache 使ってみた(android.util.LruCache)
GoogleI/O 2012 のセッションで LruCache の話が出てたので使ってみた。
Doing More With Less: Being a Good Android Citizen
pdf(右側の 「Session presentation」 リンク) の p.8 くらいから。
LruCache(android.util.LruCache)
- Least Recently Used アルゴリズム(wiki) を用いたキャッシュ管理クラス
- 中身は LinkedHashMap
- インスタンス生成時に保持キャッシュサイズの最大値を指定
- 要素がputされたときに↑を超えていたら既存要素を自動的に破棄
- API Level 12
- support library(v4) にもあるから APILevel が ↑以下でも使える
使い方は基本的にこんなかんじ。
実装方法
- LruCache の拡張クラスを作成
- コンストラクタに 保持キャッシュサイズ最大値 を定義(↓のsizeOfの合計値上限)
- LruCache#sizeOf を override して 各要素のサイズ計算方法 を定義
- LruCache#entryRemoved を override して 各要素の破棄処理 を定義
サンプルコード。
private void testLruCache() { // cache要素最大数 10個 LruCache lruCache = new BitmapLruCache(10); // cache要素追加 String key = ~; Bitmap val = ~; lruCache.put(key, val); // cache要素取得 Bitmap cache = lruCache.get(key); // cache解放処理(全要素) lruCache.evictAll(); } private static class BitmapLruCache extends LruCache<String, Bitmap> { public BitmapLruCache(int maxSize) { super(maxSize); } @Override protected int sizeOf(String key, Bitmap value) { // default では 1 を返却 return super.sizeOf(key, value); } @Override protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) { // cache解放処理 if (!oldValue.isRecycled()) { oldValue.recycle(); oldValue = null; } } }
この実装の場合、こんな感じになる。
- 保持キャッシュ(Bitmap)最大数は 10個
- 保持キャッシュ数が 10個 を超えると要素が解放されていく(Bitmap#recycle)
ビルド時にエラー発生(Error generating final archive)
また出た。
毎年恒例のこのエラー。
対応策は 「debug.keystore」 の削除。
去年の自分のエントリにも書いてた。
Error generating final archive(Debug Certificate expired on~) が発生する
このエラーが出たということはー
自分の場合は 「Androidアプリ開発始めてから丸2年経過」 ということか。
毎年このエラーが出るたびにイロイロ考えさせられそうです。
自分の1年を振り返るいいタイミング?
画面回転してもFragment再生成しないでレイアウト変更したい+取得したデータ使い回したい
お題
- 画面回転させて、縦/横で別のレイアウトを表示する
- 縦画面のときは ListView を表示
- 横画面のときは GridView を表示(1行2列)
- ただし、一度取得したデータは使いまわしたい
縦画面
横画面
実装のポイント
- Activity
- コンフィグ変更のたびに毎回 再生成
- Fragment
- Fragment#setRetainInstance(true) を設定し、Activity 再生成時に Fragment は再生成されないようにする(Fragment#onCreate/onDestroy を抑止)
- Activity 再生成のたびに走る Fragment#onCreateView でレイアウト再読込させる
- データ取得
- 一度だけ(今回の例では一度だけ呼び出される Fragment#onCreate 内で)
- レイアウト
メイン部分のコード。(コード全体はこちら)
Activity再生成時のデータの保存・復元(Fragment#setRetainInstance)
- ネットワークからデータ取得(画像とか)
- データ取得完了したら画面にデータ反映
こういうパターンってよくありますよね。このときに画面回転等のコンフィグ変更(縦横切替とか)が行われると 「Activity のインスタンスが破棄->再生成」 されるので、何も考えてないと保存データも一緒に破棄されてしまいます。なので、内部に保存しているデータを 「保存/復元」 する必要があります。
自分、コンフィグ変更で Activity再生成 とかされると正直めんどくさいので、基本的に "android:configChanges" の属性指定で Activity再生成抑止 して データの保存/復元 とか考えない方向でいいんじゃね。とか思ってたんです(すみません)。
でもよくよく調べてたら Activity再生成 を回避できないケース がありまして。例えば、「フォント切替」。
こいつは configChanges のパラメータにないので、厳密にいうとやっぱり Activity再生成 は避けて通れない。悲しいです。というわけで、Activity再生成時の内部データの保存/復元方法 はどうやるのが正解なのだろう、と調べてみると 2.x、3.x〜系 でだいぶ変わっていました。自分への備忘録の意味も込めてまとめてみます。
Activity再生成時の保存/復元処理
2.x系(API Level 10まで)
- サイズ小さめのデータの場合(serializeされる)
- Activity#onSaveInstanceState で保存処理
- Activity#onCreate、onRestoreInstanceState で復元処理
- サイズ大きめのデータの場合(serializeされない)
- Activity#onRetainNonConfigurationInstance で保存処理
- 再生成後に任意のタイミングで Activity#getLastNonConfigurationInstance 呼び出して復元処理
3.x、4.x系(API Level 11〜)
- サイズ小さめのデータの場合(serializeされる) ※Activity単位(2.x系と一緒)
- Activity#onSaveInstanceState で保存処理
- Activity#onCreate、onRestoreInstanceState で復元処理
- サイズ小さめのデータの場合(serializeされる) ※Fragment単位
- Fragment#onSaveInstanceState で保存処理
- Fragment#onCreate で復元処理
- サイズ大きめのデータの場合
- Fragment#setRetainInstance(true) を指定して Fragment が破棄されないようにし、そこへデータ保存
- 任意のタイミングで↑の Fragment からデータ取得して復元処理
サイズ小さめデータの 保存/復元 を行う際、Activity#onSaveInstanceState〜 については 2.x系 と特に変わらないのですが、Fragment単位でも同様のことができるようになってます。
サイズ大きめデータの 保存/復元 を行う際、3.x系〜は Activity#onRetainNonConfigurationInstance が非推奨になってます。代わりに Fragment#setRetainInstance(true) を使用する方法が推奨されています。
Fragment#setRetainInstance(true) を呼び出すことでどうなるかというと、
- Activity再生成時に Fragment#onDestroy、onCreate が呼ばれなくなる。
- Activity再生成時には Fragment#onDetach、onAttach が呼び出されるだけ。
- Fragmentインスタンスが破棄されないので Fragmentインスタンス内にデータをそのまま保持しておくことが可能。
ということみたいです。つまり、でかいデータについては、
Fragmentインスタンス自体を破棄されないようにしとく。 で、Fragmentインスタンス内部でそのまま保持っておく。
ってことですかね!
参考
- Activity#onRetainNonConfigurationInstance
- Android UI Cookbook for 4.0
- 「2.9 ビューを持たないフラグメントで定期処理をする」