「1apkでAndroidタブレット向けとスマートフォン向けアプリを実現する方法」で完成後のクラス図を記載しましたが、「Android デベロッパーラボ 東京 2011」のコードラボ向けソースコードをベースに今回の目的である1apkでタブレット向けとスマートフォン向けアプリを実現するために、大幅な変更が必要なことが判明しました。
設計方針の変更
そのため、今回はまず1apkでタブレット向けとスマートフォン向けアプリを実現するために、簡素化して実装を進めることにしました。
以下のものがそのクラス図です。
緑色のクラスが新規作成のクラスで、青色が変更を行う必要があるクラスです。
変更のステップは以下のような流れです。
- 起動Activityのタブレット、スマートフォンの両対応向けの準備
- スマートフォン向けレイアウト作成
- スマートフォン向けノート編集画面の作成
- Fragment クラスへの機能追加
起動Activityのタブレット、スマートフォンの両対応向けの準備
スマートフォン向けレイアウトを res/layout におくために、コードラボ完了時のタブレット向け画面レイアウトを res/layout-xlarge-v11 に名前変更を実施
起動Activityをタブレット向けとスマートフォン向けの両対応させるためには、Androidプラットフォームがどのようにレイアウトの切り替えを行っているかを知っておく必要があります。以下のアクティビティ図をもとに紹介します。Activity が起動されると onCreate() が呼び出されて、この中でレイアウトファイルを setContentView() するというのがAndroidのもっとも一般的な画面の作り方です。setContentView()を行うとAndroidの仕組みによって端末の固有情報をもとに読みだすレイアウト xmlファイルの読み出し先のディレクトリを変更してくれます。もっと詳しく知りたい場合は、Android SDK:Load the XML Resource(英語)を参照ください。
layout フォルダの使い方
今回の場合は setContentView(R.layout.notepad); と指定しているので、スマートフォンの場合、layout/notepad.xml が対象となり、タブレットの場合は、layout-xlarge-v11/notepad.xml が対象になり、対応したレイアウトファイルを読み込んでくれます。そのため、コードラボ完了時のレイアウトファイルのフォルダを以下のように変更してから、スマートフォン向けのnotepad.xml を新規作成します。
表:レイアウトフォルダの変更
変更前のフォルダ |
変更後のフォルダ |
/res/layout/ |
/res/layout-xlarge-v11/ |
/res/layout-port/ |
/res/layout-xlarge-port-v11/ |
(※)layoutより後ろに続く識別子(xlargeやportやv11など)の順番は正しい順序で指定しないと正しく動作しません。詳細な項目や並べる順番を知りたい場合は、Providing the Best Device Compatibility with Resources(英語)やHow Android Finds the Best-matching Resource(英語)を参照してください。
続いて、新規作成するスマートフォン向けのレイアウトファイルを紹介します。中身はタブレット向けのものとあまり変化はなく、以下のようになります。
タブレット向けの /res/layout-xlarge-v11/notepad.xml と比較してみるために以下の掲載します。
もともとの notepad.xml では、2ペイン構成で左側にノートリストがあって、右側に編集画面がでてくるものでした。スマートフォン向けの画面の場合は画面サイズが小さいため、2ペイン構成にはできないため1ペインで画面遷移を発生させるユーザインタフェースにします。そのため、タブレットのレイアウトファイルの <fragment …. NoteListFragment のみを抜き出したものがスマートフォンの起動画面のノートリストのレイアウトになります。
タブレット向けのレイアウトファイルのnotepad.xml の中の <LinearLayout android:id=”@+id/note_detail_container” という部分はまだ Fragment 指定になっていないため、あとから Fragment の指定に変更します。コートラボの課題では問題になりませんでしたが、せっかく NoteEdit を Fragment 化したので、ここも Fragment 指定にするように課題に入っていたほうがよかったですね。
スマフォでの実行
この状態でスマートフォンで実行してみましょう。ノートが1つもないため、真っ黒な画面になると思います。ここでノートを追加するためにメニューボタンを押すと Exception が発生して落ちてしまいます。
onCreateOptionsMenu() の中のadd.setShowAsAction() で落ちます。理由は、このメソッドは Honeycomb 3.0(API Level 11)からのAPIのため、API Levelが 10 以下のスマートフォンでは提供されておらず、実行時に Exception が発生してしまいます。
これを避けるために、実行時にAPIレベルの判断ロジックを呼び出して実行するかどうかを制御します。具体的な判断ロジックについては、前回のエントリ「1apkでAndroidタブレット向けとスマートフォン向けアプリを実現する方法 その2」を参照ください。今回やりたいことはAPIレベルの判断のため、UIUtils.isHoneycomb() を利用して、add.setShowAsAction() メソッドをコールするかどうかを判定します。
NotepadActivity.java の具体例は以下のようになります。
スマフォ向け対応
続いて、「スマートフォンの場合に画面遷移が必要なため、それ向け対応」を行います。
メニューボタンを押して「Add Note」を実行したときに、タブレットの場合は画面遷移を行わずに右側に新規ノート画面を表示していましたが、スマートフォンの場合はノート編集画面を新しく起動するようにします。「Add Note」を実行したときには、onMenuItemSelected() がコールされるため、その中でタブレットかスマートフォンかの判断ロジックを利用して、処理の振り分けを行います。具体的には以下のようなコードになります。
いまの状態では、スマートフォン向けのノート編集画面(NoteEditActivity)がないため、コンパイルエラーのままなので、これを新規作成します。
NoteEditActivity はスマートフォン向けのみのActivityのため、明確にわかるように所属するパッケージを変更します。
パッケージ名:com.example.android.honeypad.phone の中に以下のように NoteEditActivity.java を新規作成します。
現時点で作成される NoteEditActivity は以下のようになります。
public class NoteEditActivity extends FragmentActivity implements
NoteEditFragment.OnNoteSavedListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.note_edit_phone);
FragmentManager fm = getSupportFragmentManager();
NoteEditFragment edit = new NoteEditFragment();
// add the NoteEditFragment to the container
FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.note_detail_container, edit, "Edit");
ft.commit();
}
}
続いて、NoteEditActivity で参照しているレイアウトファイル(R.layout.note_edit_phone)を以下のように作成します。
上記の res/layout/note_edit_phone.xml の中身は、res/layout-xlarge-port-v11/notepad.xml の <LinearLayout android:id=”@+id/note_detail_container” の部分を Fragment 指定に変更したものです。そのためこの機会に res/layout-xlarge-port-v11/notepad.xml の <LinearLayout android:id=”@+id/note_detail_container” の部分をFragment 指定に変更しておきましょう。変更後の res/layout-xlarge-port-v11/notepad.xml は以下のようになります。
これでスマートフォンの起動画面(ノートリスト画面)(NotepadActivity)からノート編集画面に遷移することができました。ここでTitleとBodyに文章をいれて、Confirm ボタンを押すと保存されますが、ノートリスト画面に戻りません。これはNoteEdit を Fragment 対応したときにタブレット向けしか想定しておらず、Confirm ボタンを押したときの動作として、保存を行うことしか実装されていないためです。タブレットの場合は、左側にノートリスト画面があるため、保存すればすぐに新しいノートのタイトルが追加されて反映されるので問題ないですが、スマートフォン向けの場合は保存を行ったあとにノートリスト画面に戻ってほしいため、この機能を NoteEditFragment に追加します。
さて、この機能はどのように実現すればよいでしょうか?何も考えずに実現しようとすると、NoteEditFragment にタブレットかスマートフォンかの判断ロジックをいれて、スマートフォンの場合に Activity を finish() するという以下のような実装を行いそうです。ここで判断ロジックにisHoneycombTablet() を使っている理由は、、前回のエントリ「1apkでAndroidタブレット向けとスマートフォン向けアプリを実現する方法 その2」を参照ください。
private void saveNote() {
// save/update the note
ContentValues values = new ContentValues(2);
values.put(NotesProvider.KEY_TITLE, mTitleText.getText().toString());
values.put(NotesProvider.KEY_BODY, mBodyText.getText().toString());
if (mCurrentNote != null) {
getActivity().getContentResolver().update(mCurrentNote, values,
null, null);
} else {
getActivity().getContentResolver().insert(
NotesProvider.CONTENT_URI, values);
}
Toast.makeText(getActivity(), "Note Saved", Toast.LENGTH_SHORT).show();
if( UIUtils.isHoneycombTablet(getActivity()) ){
getActivity().finish();
}
}
FragmentとActivityの役割分担
この方法でも実現は可能ですが、Fragment の導入された目的に照らし合わせるとよろしくありません。
ここで今一度、ActivityとFragmentの関係について考えてみたいと思います。Fragmentが導入された経緯は、スマートフォンとタブレットという画面サイズが大幅に異なる2つの製品群に対して、いままでのActivityで画面を開発していくというスタイルのままではスマートフォン向けアプリとタブレット向けアプリに対してそれぞれの画面レイアウトをもったActivityを2つ開発していく必要があり、開発者にとっては大変な負担になっていくところでした。
そこで、honeycomb (Gingerbread 以下向けにも互換パッケージを提供)から Fragment という概念を導入し、画面を構成するActivityよりも概念的に小さく、ユーザに提供する機能単位で実装を行えるFragment の提供を開始しました。
Fragment はユーザに提供する機能を実装し、ActivityはFragmentの要素をいつ、どこに利用するかの管理や、Androidのお家芸であるIntentの処理を担当することによって、ソフトウェアのコンポーネント化を進めて、再利用性を向上し、ユーザに有用なアプリを提供していけるようになる仕組みです。
ということから考えると、上記のFragmentのなかでUtils.isHoneycombTablet()の判断ロジックをもって、Activityの画面遷移を操作していることは、Fragment が Activity の役割である画面遷移やIntentの処理を行っていることになり、本来の趣旨から外れてしまいます。
そのため、これらの処理は、Activity 側で実装を行い、Fragment 側はActivity の機能を呼び出す仕組みにします。
具体的なやり方としては、Fragment内でコールバックメソッドを定義し、Activityがそのコールバックメソッドを必ず実装するように定義することによって実現します。
この仕組みについては、Creating event callbacks to the activity(英語)かソフトウェア技術ドキュメントを勝手に翻訳:アクティビティとのやり取り:アクティビティへのイベントコールバックの作成に紹介があります。
NoteEditFragment.java に追加するコールバックメソッドは以下のようになります。
public class NoteEditFragment extends Fragment {
public interface OnNoteSavedListener {
public void onNoteSaved();
}
これを Activity で implements して実装します。注意点としては、Activityに対して実装を強制するため NoteEditFragment 内でこのコールバックメソッドが実装されていなければ、ClassCastException を発行して実行時例外が発生し、実行できないようにしています。
上記の実行時例外の発行は、以下のようなコードで実現しています。
public class NoteEditFragment extends Fragment {
...
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnNoteSavedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnNoteSavedListener");
}
}
NoteEditFragment にコールバックインタフェースに追加ができれば、以下のようにそのコールバックを呼び出すように変更します。
private void saveNote() {
// save/update the note
ContentValues values = new ContentValues(2);
values.put(NotesProvider.KEY_TITLE, mTitleText.getText().toString());
values.put(NotesProvider.KEY_BODY, mBodyText.getText().toString());
if (mCurrentNote != null) {
getActivity().getContentResolver().update(mCurrentNote, values,
null, null);
} else {
getActivity().getContentResolver().insert(
NotesProvider.CONTENT_URI, values);
}
Toast.makeText(getActivity(), "Note Saved", Toast.LENGTH_SHORT).show();
// イベントをホストのアクティビティに送信する
mListener.onNoteSaved();
}
NoteEditFragment でコールバックの実装を強制することによって、それを利用する2つのActivityで implements する必要がでてきました。
NoteEditActivity では、本来やりたかった保存時にActivityを終了して、ノートリスト画面に戻るという機能を実現したいため、以下のように onNoteSaved() 内で Activity を終了する finish() を実行します。
NotepadActivity では、現状通り特になにもすることはないので、以下のように空のまま実装します。
public class NotepadActivity extends FragmentActivity implements
NoteListEventsCallback, NoteEditFragment.OnNoteSavedListener {
@Override
public void onNoteSaved() {
// do nothing
}
これでスマートフォンとタブレットの両方でノート編集画面でノートを追加することができるようになりました。
次は、スマートフォンの起動画面のノートリスト画面ですでに作成済みのノートをクリックした場合に追加編集できる機能を実現します。
現状では、ノートリストを選択した場合、NoteListFragment の onListItemClick() メソッドが呼び出されます。
public class NoteListFragment extends ListFragment {
...
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
Uri noteUri = ContentUris.withAppendedId(NotesProvider.CONTENT_URI, id);
mContainerCallback.onNoteSelected(noteUri);
}
onListItemClick() メソッドの中でさきほどもあったような mContainerCallback.onNoteSelected(noteUri); のようにコールバックインタフェースを実行しています。このコールバックは以下のように定義されています。
public class NoteListFragment extends ListFragment {
public interface NoteListEventsCallback {
public void onNoteSelected(Uri noteUri);
public void onNoteDeleted();
}
このコールバックインターフェースは誰が実装しているかを調べてみると、NoteListFragment を利用している Activity で実装していることが以下からわかります。
public class NoteListFragment extends ListFragment {
...
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
// check that the containing activity implements our callback
mContainerCallback = (NoteListEventsCallback) activity;
} catch (ClassCastException e) {
activity.finish();
throw new ClassCastException(activity.toString()
+ " must implement NoteSelectedCallback");
}
}
NoteListFragment を利用しているActivityは、NotepadActivity1つのため、こちらを見てみます。
public class NotepadActivity extends FragmentActivity implements
NoteListEventsCallback, NoteEditFragment.OnNoteSavedListener {
...
@Override
public void onNoteSelected(Uri noteUri) {
showNote(noteUri);
}
private void showNote(final Uri noteUri) {
// check if the NoteEditFragment has been added
FragmentManager fm = getFragmentManager();
NoteEditFragment edit = (NoteEditFragment) fm.findFragmentByTag("Edit");
if (edit == null) {
// add the NoteEditFragment to the container
FragmentTransaction ft = fm.beginTransaction();
edit = new NoteEditFragment();
ft.add(R.id.note_detail_container, edit, "Edit");
ft.commit();
} else if (noteUri == null) {
edit.clear();
}
if (noteUri != null) {
edit.loadNote(noteUri);
}
}
NoteListFragment のリスト項目のクリックから onNoteSelected() コールバックインタフェースが実行されて、showNote(noteUri);が実行されます。
この中で FragmentManager を利用して fragment の管理を行っています。この処理は、2ペイン構成の場合の処理がモロに記載されているため、スマートフォン向けには不必要な処理です。
そのため、onNoteSelected() コールバックインタフェース内でスマートフォンかタブレット判断ロジックを利用して、処理を分岐させます。分岐させたスマートフォン側の処理では、ノート編集画面を起動するように実装します。
public class NotepadActivity extends FragmentActivity implements
NoteListEventsCallback, NoteEditFragment.OnNoteSavedListener {
...
@Override
public void onNoteSelected(Uri noteUri) {
if (UIUtils.isHoneycombTablet(this)) {
showNote(noteUri);
} else {
showNotePhone(noteUri);
}
}
// for phone api
private void showNotePhone(final Uri noteUri) {
Intent i = new Intent(this, NoteEditActivity.class);
i.setData(noteUri);
startActivity(i);
}
これでスマートフォンのノートリスト画面からノート編集画面に uri を渡して起動することができました。動作確認を行ってみると、どのノートリストを選択しても、TitleとBodyは表示されずに新規作成扱いになってしまいます。
これはまだNoteEditActivity 側で受信した Intent の処理を行っていないためです。
Activity と Fragment の役割分担の上記で話をしましたが、今回のようなバグを追うときにも Intent の処理の場合は、基本的に Activity が担当するため、呼び出す前のActivityが正しく送っていることが確認できていれば、デバッグを呼び出した後のActivityに注力することができます。
NoteEditActivity の onCreate() 内で Intent の受信を行っていないことが原因と判明したため、以下のようなロジックを onCreate() 内に追加します。
追加後のNoteEditActivity は以下のようになります。
public class NoteEditActivity extends FragmentActivity implements
NoteEditFragment.OnNoteSavedListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.note_edit_phone);
Intent intent = getIntent();
Uri uri = intent.getData();
FragmentManager fm = getSupportFragmentManager();
NoteEditFragment edit = new NoteEditFragment();
edit.loadNote(uri);
// add the NoteEditFragment to the container
FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.note_detail_container, edit, "Edit");
ft.commit();
}
@Override
public void onNoteSaved() {
finish();
}
}
おわりに
これで目的の1apkでタブレット向けとスマートフォン向けのアプリを作成することができました。今回のコードラボのアプリがActivityベースのスマートフォン向けアプリをスマートフォン向け構成を維持しないままにタブレット向けアプリにするというものでした。そして Fragment 実装に置き換えたうえで2ペイン対応のタブレット向けアプリにするというものでした。それをあとから、1apkでスマートフォンとタブレットの両対応アプリにするという一直線ではなく、横道にそれて一度喫茶店で休憩したあとに戻りながら目的地に歩いていくというやり方でした。
本来なら、Activity ベースのスマートフォン向けアプリがベースになるなら、スマートフォン向けアプリのままで Fragment 対応を実施して、スマートフォン向け動作を確保しながら、徐々にタブレット向けアプリを開発していくというスタイルがよいと思います。
今回作ったアプリは eclipse のプロジェクト形式で整理したうえ(まだぐちゃぐちゃなので)で公開したいと思います。もし急ぎでほしいという方は(そんな人いないと思いますが)ご連絡頂ければ先に展開させて頂きます。
「Android Layout Cookbook アプリの価値を高める開発テクニック」の作者の方もY.A.M の 雑記帳:Android Developer Lab Tokyo 2011 のノートパッドをカスタマイズしてみた。(こちらのほうが相当レベルは上ですが。。。大変勉強になります。)でカスタマイズされています。
Fragment についてはまだ日本でAndroidタブレットが普及していない事情もあり、利用されているアプリは少ないと思います。Fragmentは、画面を持たないものも開発可能ということでタブレットアプリとスマートフォンアプリの両方で利用できる汎用ライブラリ的なものを作るプラットフォームにもなれるのではないかと考えています。ADL2011で google の android developer Advocate の方が次期バージョンの IceCreamSandwich(ICS) でも Fragment は推進していくのでどんどん Fragment を利用すいてアプリを開発してくださいとおっしゃっていました。そういうことを踏まえると、今後Androidアプリを開発していくにあたって Fragment は避けて通れないものになっていく予感がしますし、使いこなせるようになれば大変強い武器になっていくのではないかと考えています。
今後の色々と調査結果を紹介していければと思っています。Fragment使ってAndroidアプリを作っていきましょう!!