この文書は本書「Androidアプリ開発逆引き大全500の極意」(秀和システム刊、ISBN978-4-7980-3734-9)で紹介しきれなかった内容をまとめたものです。

目次へ
清水美樹の本 トップページへ

テンプレート「Master/DetailFlow」を実際に使ってみる

完成イメージ

惑星(Planet)を、複数(Planets)閲覧できるアプリ

Tips055で紹介したADTのテンプレート「Master/Detail Flow」とは、タブレットの広い画面用のページです。リストから見出しをクリック(Master)すると、その詳しい説明を見ることができるようなしくみです。
図1 Androidアプリケーション・プロジェクトの新規ウィザードで選べる「Master/Detail Flow」」
このページでは、テンプレートを元に、図1のように左側の惑星の名前をクリックするとその説明が右側に表示されるようなアプリを作ります。
図2 この文書で作るアプリの完成イメージ

タブレットAVDを作成する

タブレット用なら7インチ以上で1280x720など

「タブレット用」というとAndroid3.0や3.2が浮かびますが、Android4.0(API 15)からは、ターゲットにタブレットとスマートフォンの区別はありません。とすると、どうやってスマートフォンとタブレットを区別すればいいかというと、それは画面の形状と解像度です。
図3はAVDマネージャにおける新規仮想デバイスの作成画面(Tips021)ですが、「Nexus 7」の説明は「7.27", 800x1280」と書いてあります。これは7.27インチで解像度が800x1280ですから、小さめのタブレットとみなすことができます。一方、「Galaxy Nexus」は「4.64", 720x1280」ですから、解像度は高くてもスマートフォンです。
図3 「装置」では画面の大きさと解像度に注意する



「横向きアプリ」用に「横向きタブレット」を選ぶ

「Master/Detail Flow」は必ずしも「左側にリスト、右側に説明」の表示はなりません。「リストをクリックしたら、画面が切り替わって説明が表示される」方式にもなります。それは、画面の形状から自動で判断されることです。7インチのように微妙な大きさだと、「大きなスマートフォン」の扱いを受けてしまうこともあります。
図4 画面がもったいない
そこで、図2のような「横向きアプリ」を表示するには、AVDも「横向き」の画面指定があるものを選ぶのが簡単です。すなわち、「1024x600」のように、幅のほうが高さより大きい画面設定です。これなら、インチ数は小さくても「横向き」の設定がされます。
図5 横向き仮想デバイスの設定完了

Master/Detail FlowのAndroidアプリケーションを作る

アクティビティーのタイプに「Master/Detail Flow」を選ぶ

Androidアプリケーションプロジェクトの作成時にアクティビティとして「Master/Detail Flow」を選びます。図6「Object Kind」を「Planet」、「Object Kind Plural」を「Planets」に設定しています。ただし、それほど重要な設定ではありません。


最初のプロジェクト作成には時間がかかる

「Master/Detail Flow」は複雑なアプリケーションで、かつAndroidのバージョンが4以上なので、最初のプロジェクト作成には時間がかかります。
ライブラリをプロジェクトに完全に読み込み終わるまで、プロジェクトにはエラーの赤マークがついているかもしれません。
Eclipseワークベンチの右下に、「Androidのデータをロード中」と表示されていますので、これが100%になって、さらに消えるまで待ちます。
図7 「Androidのデータをロード中」が「100%」となっているが、消えるまでさらに数秒かかることがある


Master/Detail Flow テンプレートで作成されるプロジェクトの構造

作成されたプロジェクトについて、Javaのソースファイルの構造に注目すると、図8のようになっています。
図8 Master/Detailプロジェクトのうち、Javaソースファイルの構造

ナニナニDetailActivity, Fragment

図8で、「Planet」で始まるクラス名は、アクティビティ作成の際の設定によるものです。
より抽象的に考えると、「ナニナニDetailActivity」「ナニナニDetailFragment」というペア、「ナニナニListActivity」「ナニナニListFragment」というペアにわかれているのがわかります。
これは、「Master/Detail Flow」の2つに分かれた画面を記述するためのフラグメントとアクティビティのペアを表します。
図9 「リスト」および「詳細」のための、フラグメントとアクティビティのペア

データを記述するクラスDummyContent

 上記のフラグメント/アクティビティのペアとは異なるパッケージに、「DummyContent.java」というソースファイルがあります。これは表示させるデータを記述するクラスです。「Dummy」という名前から推察されるように、一時的なものです。


プロジェクトを実行する

自動作成されたままの状態で、プロジェクトを実行してみます。リストをクリックすると詳細が表示されるというしくみは、すでにできていることがわかります。ただし、左側のリストで「Item1」をクリックすると、右側の説明欄にも「Item1」と表示されるという、きわめて簡単なものです。
図10 作成されたままを実行


このプロジェクトに最小限の変更を行って、図2のイメージを実現させましょう。その作業を通じて、Master/Detail Flowプロジェクトのプログラムがどのような構造になっているかを研究します。ただし、すべての仕組みを説明すると大変長くなると思われますので、最低限のカスタマイズに必要な内容にとどめます。

表示データはどこに記述されているのか

クラスDummyContentは全て「static」

データの「モデル」を記述しているのがDummyContent.javaです。ソースファイルの「全文」は長くなるので掲載しません。Eclipseのエディタ画面で実際に眺めてください。カラー表示もなされていて、ずっと見易いはずです。
図11 エディタでソースファイルを眺める
眺めると、やたらに「static」の記述が目立つと思います。いえ、すべてが「static」で定義されています。フィールドもメソッドも、内部クラス「DummyItemも全て「static」の修飾子を持ちます。

この目的は、「表示データ」をきわめて具体的な値に絞るためです。ダミー値なので、「オブジェクトを作ってそれに値を当てはめる」という作業を、可能な限り省略しようという考えです。「ためしにHelloと入れてみる」「ためしに5を入れてみる」というノリを、構造を持つデータに適用する方法です。
実際にこの形式のアプリを作成するときは、「普通のオブジェクト」を作成するようにクラスを定義します。


クラスの定義の中で表示文字列が設定してある

このクラスの定義の中で、すでに図10で表示されている文字列が設定されています。リスト1の部分です。
リスト1 アプリに表示させる文字列を設定している場所
static {
		
	addItem(new DummyItem("1", "Item 1"));
	addItem(new DummyItem("2", "Item 2"));
	addItem(new DummyItem("3", "Item 3"));
	}


この処理は、「普通のクラスの定義」では「コンストラクタ」の中で行うような初期化の処理です。

そこで、最低限ここで文字列、すなわち二重引用符の中に書かれている内容を書き換えれば、オリジナルな表示ができることになります。しかし、我々の目的である図2のような表示はできません。なぜでしょうか?以下に説明します。


MasterとDetailで共通の文字列が使われている(ひどい)

リスト1の中では、リスト2のように「クラスDummyItemのオブジェクト」が作成されています。
リスト2 クラスDummyItemのオブジェクトを作成
new DummyItem("1", "Item 1")


リスト2で最初の引数に渡された値 "1"は、「1番目の表示データ」という識別番号です。「Master」と「Detail」の関連づけに使いますます。
引数はもうひとつあって、"Item 1"という文字列が渡されています。
実は、この文字列一つが「Master」と「Detail」で共有されるしくみになっています。そのために図10では、リストで「Item2」を選ぶと右側に「Item2」と表示されるのです。これはいくらダミーと言っても手抜きしていると言わざるを得ません。Master用とDetail用と2種類ずつのデータを入れるしくみくらいは、サンプルとして作ってほしいものです。


ここを日本語の文字列で書き換えるのは気が引ける

MasterとDetailで同じ文字列しか使えないのを我慢するとしても、リスト1の文字列をそのまま日本語に書き換えるのは気がひけます。Androidアプリでは、固定文字列はなるべく「strings.xml」に書くことが推奨されるからです。さらに、日本語のような「英語でない文字」は、ソースコードに直接書くのは避けるべきです。

このように、自動記入された中身は、かなり深刻に実用的でありません。そこで、せめて「strings.xml」に記述した日本語を読みとって表示できるように、必要最小限のコードの変更をしてみましょう。そのためには、今見た「DummyContent.java」のみならず、他のソースコードにも変更を加える必要があります。

DummyContent.javaの修正

strings.xml

「DummyContent.java」の修正をする前に、「strings.xml」に、決め打ちの文字列を3つ登録しておきます。(Tips161)
たとえば、表1のようにします。
表1 「strings.xml」に記述する文字列の名前(name)と値(value)
namevalue
mercury水星
mercury_detail太陽系の中で最小の惑星。太陽にも近いし、非常に見えにくいのに、古代の人はよく見つけたものである。 運行が速いから俊足や翼のある妖精などにたとえられているが、よく運行しているとわかったものである。
venus金星
venus_detail明けの明星、宵の明星として親しまれている。大きさが地球に似ているので、ビーナスなだけに 美人ばかりが住んでいると想像されたこともあるが、実際の環境は苛酷なそうだ。
mars火星
mars_detailもっともよく探査が進んでいる惑星。「火」星というが赤いのは酸化鉄のせいで、実はとても寒い。タコのような生物が住んでいるという説がほぼ定着している。



クラスDummyItemの構造を変える

「データ・モデル(データの型)」は、DummyContentクラスの内部クラスとしてソースコード中に定義されている「DummyItem」です。
リスト3はDummyItemで定義されている「フィールド」です。IDと、Master/Detailで共有する「content」の2つしかありません。
リスト3 クラスDummyItemのもともとの定義
public static class DummyItem {
	public String id;
	public String content;

	public DummyItem(String id, String content) {
		this.id = id;
		this.content = content;
	}

	@Override
	public String toString() {
		return content;
	}
}



これにフィールド「name」を加えます。「name」を一覧に表示する値(「水星」「火星」など)、「content」をそれぞれの説明文と考えます。
リスト4 フィールド「name」の追加
	public String id;
	public String name;
	public String content;

	public DummyItem(String id, String name, String content) { 
		this.id = id;
		this.name = name;
		this.content = content;
	}



実は重要な「toString」メソッド

リスト3のメソッド「toString」には「@Override」のアノテーションがついています。何を「再定義」しているかというと、あの有名な「java.lang.Object」クラスのメソッドtoStringです。
リスト3では、再定義版のtoStringメソッドが返すものは「content」ですが、これはMaster/Detail Flowテンプレートの仕様で、「リストに表示する」ほうのフィールドを返すようにします。リスト5のように書きます。
リスト5 nameを返すように書き換える
	@Override
	public String toString() {
		return name;
	}




「ただのヘルパークラス」が「R」にアクセスするには?

さて、ここで問題が生じてきます。文字列として、「strings.xml」の値を読み込みたいのですが、そうするとたとえば、リスト6の形にしなければいけません。(Tips078)
リスト6 「strings.xml」に登録した値を読み込むには
getString(R.string.mercury)


しかし、getStringはアクティビティなど「Context」クラスのサブクラスのオブジェクトが呼び出せるメソッドです。一方、「DummyContents」というクラスは何のクラスも継承していない普通のクラスです。

こういうとき、よい方法があります。コンストラクタの定義で、Contextのサブクラスを引数にとるのです。
リスト7 普通のクラスのオブジェクトがリソースにアクセスするための方法

public DummyContent(Context c){		
....
	String mystring = c.getString(R.string.mercury), 

}


こうして、strings.xmlをコンストラクタ内で全部読みとってしまいます。


DummyItemは「static」を全部除去

クラスDummyContentがコンストラクタを使うとなると、他のクラスからはDummyContentのオブジェクトを呼び出すことになります。そこで、DummyObjectのクラスからstaticを除去し、オブジェクトに属するフィールドやオブジェクトが呼び出すメソッドからは「static」の修飾子をとります。


「DummyContent.java」の修正版

以上のことから、「DummyContent.java」は、リスト8のようになります。リスト1の意味を「コンストラクタで行うような処理」と説明しましたが、本当にコンストラクタにこれらの処理を入れています。
リスト8 「DummyContent.java」修正版の全文(パッケージやインポートの宣言は省略)

public class DummyContent {

	/**
	 * 静的フィールドITEMSを普通のフィールドに直す
	 */
	List dummyItems;

	/**
	 *静的フィールドITEMMAPを普通のフィールドに直す
	 */
    Map dummyItemMap;

	/**
	 * コンストラクタ。コンテキストを引数にとることで、リソースにアクセスできる。
	 */
	public DummyContent(Context c){
		
		dummyItems = new ArrayList();
		dummyItemMap = new HashMap();
		
		addItem(new DummyItem( "1", 
				c.getString(R.string.mercury), 
				c.getString(R.string.mercury_detail)));
		
		addItem(new DummyItem("2",
				c.getString(R.string.venus), 
				c.getString(R.string.venus_detail)));
		
		addItem(new DummyItem("3",
				c.getString(R.string.mars), 
				c.getString(R.string.mars_detail)));
		
	}
	
	//フィールドdummyItemsのgetメソッド
	public List getItems(){
		return dummyItems;
	}
	
	//フィールドdummyItemMapのgetメソッド
	public Map getItemMap(){
		return dummyItemMap;
	}
	

	private void addItem(DummyItem item) {
		dummyItems.add(item);
		dummyItemMap.put(item.id, item);
	}

	/**
	 * 内部クラスDummyItem。もはや静的クラスではない
	 */
	public class DummyItem {
		public String id;
		public String name;
		public String content;

		public DummyItem(String id, String name, String content) {
			this.id = id;
			this.name=name;
			this.content = content;
		}
		
		@Override
		public String toString(){
			return name;
		}
	}
}



PlanetListFragment.javaの修正

DummyContentクラスのオブジェクトを作成

DummyContentクラスが静的クラスでなくなったので、コンストラクタによりこのクラスのオブジェクトを作成します。
DummyContentオブジェクトはは複数個所で用いますので、メンバー変数を作ります。
リスト9 DummyContentクラスのオブジェクトを保持するメンバー変数mDumCon
DummyContent mDumCon;


mDumConにDummyContentのオブジェクトを割り当てる作業は、onCreateメソッドで行います。「フラグメントなので、「コンテキスト」としてはgetActivity()の戻り値を渡すことになります。
リスト10 onCreateメソッド中でmDumConを初期化
@Override
public void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	mDumCon=new DummyContent(getActivity());
	.....
	



オブジェクトmDumConを用いた処理に書き換え

さらに、onCreateメソッドのリスト11の部分を、リスト12のように直します。
リスト11 静的フィールドを呼び出している
	setListAdapter(new ArrayAdapter(
			getActivity(),
			android.R.layout.simple_list_item_activated_1,
			android.R.id.text1, DummyContent.ITEMS));
}//onCreateメソッド終わり
リスト12 mDumConとgetItemsメソッドで内容を得る
	setListAdapter(new ArrayAdapter(
			getActivity(),
			android.R.layout.simple_list_item_activated_1,
			android.R.id.text1, mDumCon.getItems()));
}


同様に、onListItemClickメソッド中のリスト13の部分を、リスト14のように直します。
リスト13 静的フィールドを呼び出している
	mCallbacks.onItemSelected(DummyContent.ITEMS.get(position).id);
リスト14 mDumConとgetItemsメソッドで内容を得る
	mCallbacks.onItemSelected(mDumCon.getItems().get(position).id);



PlanetDetailFragment.javaの修正

メンバー変数mItemの初期化にDummyContentオブジェクトを用いる

PlanetDetailFragmentでは、DummyContentオブジェクトを直接メンバー変数として用いることはなく、メンバー変数mItemに、DummyContentオブジェクトの中からDummyItemクラスのオブジェクトを取り出して与えます。初期設定では静的フィールドを用いてリスト15のように処理しています。onCreateメソッドです。
リスト15 メンバー変数mItemに静的フィールドの値を与える
	mItem = DummyContent.ITEM_MAP.get(getArguments().getString(
			ARG_ITEM_ID));



これをリスト16のように「DummyContentオブジェクトのgetItemMapメソッドを用いた方法に書き出します。
リスト16 DummyContentオブジェクトのgetItemMapメソッドを用いる
	mItem = new DummyContent(getActivity()).getItemMap().get(getArguments().getString(
			ARG_ITEM_ID));



これで、図2の完成イメージが得られるでしょう。Master/Detail Flowアプリを動かすための他の記述についての説明は、この記事では省略します。
目次へ
清水美樹の本 トップページへ