2012/07/12

Android:ADT Rev.20 雛形MasterDetailFlow


図1


ADT Rev.20でAndroidプロジェクトを新規作成する際の"Create Activity"(図1)で
"MasterDetailFlow"を選択した場合に作成される雛形の設計・実装を調査しました。

MasterDetailFlowはよくある2パネルのアプリを作成します。
ただし、画面領域を十分に確保できない場合は1ペインで表示します。
イメージはこんな感じです。
http://developer.android.com/images/fundamentals/fragments.png

MasterDetailFlowなプロジェクトを作成すると下記の構成で雛形が作成されます。


パッと見でこの辺がパネル数を制御してそうですね。
  • layout/activity_item_twopane.xml
  • values-large/refs.xml
  • values-sw600dp/refs.xml

activity_item_twopane.xmlは2パネル用のレイアウトファイル。
<LinearLayout
    ...一部省略...
    android:orientation="horizontal"
    android:showDividers="middle">

    <fragment android:name="com.example.fragment.ItemListFragment"
        ...一部省略...
        android:layout_weight="1" />

    <FrameLayout android:id="@+id/item_detail_container"
        ...一部省略...
        android:layout_weight="3" />

</LinearLayout>
リストと詳細の画面割当は1:3。それぞれの間はdividerで区切ってます。
リストパネルはItemListFragmentで管理。
詳細パネルはitem_detail_containerのIDを持つ空コンテナで、ItemDetailFragment
ではないようです。

次に2パネルレイアウトのactivity_item_twopane.xmlを読み込んでいる人を探します。
順当にレイアウト参照元を辿っていくと...

はじめに目をつけた通り、↓のようです。
  • values-large/refs.xml
  • values-sw600dp/refs.xml

どちらも内容は全く同じ。
layoutリソースactivity_item_twopaneをactivity_item_listとして定義してる。
<resources>
    <item type="layout" name="activity_item_list">@layout/activity_item_twopane</item>
</resources>
と、ここでもう一度layoutフォルダの中を見るとactivity_item_list.xmlが既に存在し
ている。
そのため、先ほどのrefs.xmlはlayoutリソースの再定義(上書き)となる。

まとめると、refs.xmlによって下記のようにリソースが定義されている。
  • デフォルト=activity_item_listはそのままactivity_item_list
  • 大画面端末=activity_item_listはactivity_item_twopane.xmlで上書き
  • 画面最短幅が600dp以上=activity_item_listはactivity_item_twopane.xmlで上書き
ということで、レイアウトactivity_item_listを読み込めば適切なリソースが選択されます。
refs.xmlでのリソース再定義は結構スマートですね。


activity_item_listを読み込んでいるのはItemListActivity。
単にactivity_item_listを読み込むだけで、1パネルor2パネルのレイアウトが適切に
選択されます。
Activity自身はonCreate内で詳細表示領域の空コンテナ(item_detail_container)がレイ
アウト内に存在しているかをチェックして、2パネルモードかどうかを判定しています。

また、ActivityがFragmentの状態を管理(ItemListFragment.setActivateOnItemClick)
していることもわかります。
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_item_list);

    if (findViewById(R.id.item_detail_container) != null) {
        mTwoPane = true;
        ((ItemListFragment) getSupportFragmentManager()
                .findFragmentById(R.id.item_list))
                .setActivateOnItemClick(true);
    }
}

ItemListActivityはonItemSelectedメソッドをオーバーライドしています。
これはItemListFragment.Callbacksインタフェースで定義されたコールバックメソッド。
ItemListFragmentでリストが選択されるとここにコールバックされます。
Fragment⇒Activityの連携はコールバックで実現しているようです。
この辺りはDeveloperサイトでも紹介されている方法ですね。
http://developer.android.com/training/basics/fragments/communicating.html
public class ItemListActivity extends FragmentActivity
        implements ItemListFragment.Callbacks {

    //...省略...

    @Override
    public void onItemSelected(String id) {
        if (mTwoPane) {
            Bundle arguments = new Bundle();
            arguments.putString(ItemDetailFragment.ARG_ITEM_ID, id);
            ItemDetailFragment fragment = new ItemDetailFragment();
            fragment.setArguments(arguments);
            getSupportFragmentManager().beginTransaction()
                    .replace(R.id.item_detail_container, fragment)
                    .commit();
        } else {
            Intent detailIntent = new Intent(this, ItemDetailActivity.class);
            detailIntent.putExtra(ItemDetailFragment.ARG_ITEM_ID, id);
            startActivity(detailIntent);
        }
    }
}

詳細画面の表示に関して。
2パネルの場合、空コンテナitem_detail_containerをFragmentのreplaceトランザクショ
ンで置き換えるようにしています。
1パネルの場合は、Activity自身でstartActivityを実行しているのがわかります。

詳細画面への情報連携(どのリストアイテムを選んだか)はIntentのextra値を使用。
リストアイテム選択時の処理はActivityまかせですね。


コールバック部分をもう少し追ってみます。

ItemListFragmentはコールバックリスナーであるmCallbacksを管理します。
初期値はsDummyCallbacks。こいつはNullオブジェクトの役目をします。
private Callbacks mCallbacks = sDummyCallbacks;

public interface Callbacks {
    public void onItemSelected(String id);
}

private static Callbacks sDummyCallbacks = new Callbacks() {
    @Override
    public void onItemSelected(String id) {
    }
};

mCallbacksはonAttachで登録。onDetachでNullオブジェクト化されます。
登録時はactivityインスタンスをCallbacksにキャストするため、事前にキャスト可能か
チェックをしてます。
このことから、ItemListFragmentを使うActivityはCallbacksインタフェースを実装する
必要があります。

Developerサイトではtry-catchでcallback登録していましたが、instanceOfで例外判定す
るこちらのほうが良い方法といえそうですね。
http://developer.android.com/training/basics/fragments/communicating.html#DefineInterface
@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    if (!(activity instanceof Callbacks)) {
        throw new IllegalStateException("Activity must implement fragment's callbacks.");
    }

    mCallbacks = (Callbacks) activity;
}

@Override
public void onDetach() {
    super.onDetach();
    mCallbacks = sDummyCallbacks;
}

@Override
public void onListItemClick(ListView listView, View view, int position, long id) {
    super.onListItemClick(listView, view, position, id);
    mCallbacks.onItemSelected(DummyContent.ITEMS.get(position).id);
}

●おわりに...

MasterDetailFlowの雛形からは、Fragment⇒Activityへのメッセージ通信方法と、
画面サイズに依存したパネル数の変更方法についてのヒントがありました。

もし、手元にスマートフォンしかない場合、この雛形では縦横切り替えしてパネル数が
変化する動作を確認できません(sw600dp or largeを満たさない為)
values-sw600dp ⇒ values-land に変更すればスマートフォンでも手軽にこれを確認
できます。

以上です。