実装技術の紹介(フラグメント)

Hello Kii では、アクティビティを次々と起動することによって画面遷移を実現していましたが、本格的な Android アプリを構築するにはフラグメントの利用が適しています。

フラグメントは、アクティビティでのユーザーインターフェイスを実現するための仕組みです。フラグメントを使うことでアクティビティ内部のビューの部品化や、ビューの遷移が容易に実装できるようになります。

Kii Balance では、フラグメントを利用して画面遷移とダイアログ表示を実現しています。また、画面またはダイアログが切り替わるときのデータの受け渡しも、フラグメントの枠組みを利用して行われます。

ここでは、まずフラグメントによる画面の切り替えについて説明します。次に、フラグメントのインスタンス化とデータの受け渡しについて説明します。ダイアログについては、ユーザーが入力したデータの取得方法についても解説します(画面遷移では画面に表示するデータの設定のみを行います)。

フラグメントの機能は、サポートライブラリー(com.android.support:appcompat-v7)と、Android OS の 2 つで同様の機能を提供しています。Android OS での実装は、OS のバージョンに依存するため、Kii Balance ではサポートライブラリーの実装を使用しています。Fragment クラスは android.app.Fragment ではなく、android.support.v4.app.Fragment のものを使用する点にご注意ください。

本トピックのスコープ

フラグメントがサポートする範囲は広いため、実装技術の習得は難しい面がありますが、ここでは実用的なモバイルアプリを構築するために必要な情報に限定して実装方法を説明します。なお、フラグメントを使った実装方法は、Android デベロッパー で詳細な解説があります。

フラグメントによる画面切り替え

以下は Kii Balance で使用しているフラグメント関連のクラスです。画面遷移とダイアログ表示の実現方法を順に説明します。

フラグメントによる画面遷移

Kii Balance では、タイトル画面とデータ一覧画面を切り替えるため、フラグメントのトランザクションを使用します。フラグメントのトランザクションは、アクティビティ内のフラグメントに対して、追加、削除、置き換えなどの操作を行うための仕組みです。

画面の切り替えは、アクティビティを実現するビューの内部にある R.id.main 要素を、Fragment のサブクラスによって置き換えることで実現します。

フラグメントの置き換えを行う処理は、ViewUtil クラスの toNextFragment() メソッドに実装されています。FragmentTransaction を使って、R.id.main を、指定されたフラグメント next で置き換えます。

public static void toNextFragment(FragmentManager manager, Fragment next, boolean addBackStack) {
  if (manager == null) { return; }
  FragmentTransaction transaction = manager.beginTransaction();
  if (addBackStack) {
    transaction.addToBackStack("");
  }
  transaction.replace(R.id.main, next);
  transaction.commit();
}

引数 addBackStack はフラグメントの遷移の過程で、現在のフラグメントをスタックに積むかどうかを指定します。

addBackStacktrue の場合は、addToBackStack() メソッドによって、現在のフラグメントをスタック上に保存してから、フラグメントの置き換えを行います。この場合、遷移先の画面で Android の戻るボタンがタップされると、スタック上に保存されているフラグメントが順に復元され、元の画面に戻ることができます。スタックが空の状態で、さらに戻るボタンをタップすると、アクティビティが終了します。

Kii Balance では、戻るボタンによる遷移を行わないため、常に addBackStackfalse で呼び出します。

フラグメントによるダイアログ表示

フラグメントの実装では、DialogFragment のサブクラスをダイアログとして画面表示する機能も用意されています。

初めのクラス図に示すように、ログイン、ユーザー登録、収支の編集、進捗表示の各機能が DialogFragment のサブクラスで実装されています。

ダイアログの表示方法はいずれのクラスでも同様ですが、収支の編集ダイアログでは、BalanceListFragment で以下のような処理を行っています。

ItemEditDialogFragment dialog = ItemEditDialogFragment.newInstance(this, REQUEST_ADD, null, "", Field.Type.INCOME, 0);
dialog.show(getFragmentManager(), "");

1 行目が DialogFragment のインスタンスを生成する処理です。詳細はこの次のセクションで説明します。

show() メソッドは、DialogFragment クラスが提供しています。このメソッドを呼び出すと、フラグメントをダイアログとして画面表示できます。第 2 引数は、フラグメントを後で参照するためのタグですが、ProgressDialogFragment クラス以外では未使用のため空文字列を指定しています。

なお、サポートライブラリー内部にある show() メソッドの実装を見ると、画面遷移の場合と同様に、FragmentTransaction を使って、フラグメントを画面表示していることが確認できるはずです。

public void show(FragmentManager manager, String tag) {
  ...
  FragmentTransaction ft = manager.beginTransaction();
  ft.add(this, tag);
  ft.commit();
}

フラグメントのインスタンス化と値の設定

画面遷移やダイアログ表示を行う際、対象のフラグメントのインスタンスは特別な方法で生成する必要があります。ここでは、呼び出し元からダイアログに値を渡す例を使って、インスタンスの生成方法について説明します。

ここで示すコードでは、フラグメントの枠組みを使って、引数をデータとして渡す方法のみを説明します。引数のアプリケーション上の役割は、後続のページで説明します。

誤ったインスタンス化

初めに誤った実装を示します。

先ほどの収支ダイアログの表示処理では、以下のように ItemEditDialogFragment インスタンスを生成したいところですが、このコードは正しく動作しません。ここでは、いくつかの引数値を伴って ItemEditDialogFragment インスタンスを生成しようとしています。

// This sample code does not work properly.
ItemEditDialogFragment dialog = new ItemEditDialogFragment(this, REQUEST_ADD, null, "", Field.Type.INCOME, 0);
dialog.show(getFragmentManager(), "");
// This sample code does not work properly.
public class ItemEditDialogFragment extends DialogFragment {
  private String mObjectId;
  private String mName;
  private int mType;
  private int mAmount;

  public ItemEditDialog(Fragment target, int requestCode,
          String objectId, String name, int type, int amount) {
    setTargetFragment(target, requestCode);
    mObjectId = objectId;
    mName = name;
    mType = type;
    mAmount = amount;
  }
}

Android OS では、OS の状況に応じて、任意のタイミングでアクティビティの破棄と再作成が行われます。この際、アクティビティ内部に設定されているフラグメントに対しても、同時に破棄と再作成が行われます。

フラグメントの再作成の際、フラグメントのコンストラクタは OS が呼び出すため、引数の値を OS から渡すことはできません。必ず、引数なしのデフォルトコンストラクタを使う必要があります。

正しいインスタンス化

正しい実装にするためには、デフォルトコンストラクタが必要です。

デフォルトコンストラクタを使ったフラグメントの再作成を実現するため、コンストラクタへの引数は Bundle クラスを経由して渡します。

  1. 呼び出し元から、Fragment のサブクラスが公開しているファクトリメソッドを呼び出します。
  2. デフォルトコンストラクタでインスタンスを生成します。
  3. Bundle インスタンスに引数を格納し、フラグメントに設定します。
  4. フラグメントの onCreate() などで Bundle の値を取得して利用します。

Bundle はサポートライブラリーと OS によって管理されているため、アクティビティが再生成されたときに引数の値を含めて復元することができます。

Kii Balance でのこの処理の実装を以下に示します。以下のコードは、先ほど示した BalanceListFragment でのダイアログの呼び出し元です。これは図中の :num1: に相当します。

ItemEditDialogFragment dialog = ItemEditDialogFragment.newInstance(this, REQUEST_ADD, null, "", Field.Type.INCOME, 0);
dialog.show(getFragmentManager(), "");

ファクトリメソッド newInstance() がある ItemEditDialogFragment では、以下のようにインスタンスを生成しています。

public class ItemEditDialogFragment extends DialogFragment {
  private static final String ARGS_OBJECT_ID = "objectId";
  private static final String ARGS_NAME = "name";
  private static final String ARGS_TYPE = "type";
  private static final String ARGS_AMOUNT = "amount";

  private String mObjectId;
  private String mName;
  private int mType;
  private int mAmount;

  public static ItemEditDialogFragment newInstance(Fragment target, int requestCode,
          String objectId, String name, int type, int amount) {
    ItemEditDialogFragment fragment = new ItemEditDialogFragment();
    fragment.setTargetFragment(target, requestCode);

    Bundle args = new Bundle();
    args.putString(ARGS_OBJECT_ID, objectId);
    args.putString(ARGS_NAME, name);
    args.putInt(ARGS_TYPE, type);
    args.putInt(ARGS_AMOUNT, amount);
    fragment.setArguments(args);
    return fragment;
  }

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    Bundle args = getArguments();
    mObjectId = args.getString(ARGS_OBJECT_ID);
    mName = args.getString(ARGS_NAME);
    mType = args.getInt(ARGS_TYPE);
    mAmount = args.getInt(ARGS_AMOUNT);
  }
}

ここでは、ファクトリメソッド newInstance() を使ってフラグメントのインスタンスを生成します。

引数の受け渡しの観点から見ると、初めに示した引数付きのコンストラクタと同等の処理を実現できている点にご注意ください。

上のコードでは以下の処理が実行されます。

  • newInstance() の引数に、インスタンスへ渡したい値を指定します。これは、本来、コンストラクタの引数にしたい値です。

  • newInstance() の実装では、デフォルトコンストラクタを使ってフラグメントのインスタンスを作成します(図の :num2:)。次に、Bundle インスタンスを生成して、渡したい値を格納します。用意した Bundle は、setArguments() メソッドでフラグメントに格納します(図の :num3:)。setArguments()Fragment クラスが標準で提供しているメソッドです。

  • フラグメント側では、onCreate() メソッドや onCreateDialog() メソッドなどの初期化イベントから getArguments() メソッドを呼び出して、Bundle に格納した値を取得します(図の :num4:)。

この実装は ItemEditDialogFragment クラスのものですが、他のクラスも同様の方法で実装されています。また、Android Studio で新規のフラグメントを追加したときのスケルトンでも同様の実装が提示されます。

ダイアログからの戻り値の取得

ここでは、ダイアログから値を受け取る方法を示します。

ここで示すコードでは、フラグメントの枠組みを使って、戻り値をデータとして受け取る方法のみを説明します。戻り値のアプリケーション上の役割は、後続のページで説明します。

通常、ダイアログで入力した結果は、ダイアログの呼び出し元が受け取り、入力結果を使って何らかの処理を行います。この流れを実現するため、ここでは、リクエストコードと onActivityResult() メソッドを使う方法を示します。

Android Studio でフラグメントを新規作成した際のスケルトンでは、これと異なる方法で入力結果を受け取ります。スケルトンでは、フラグメントの親がアクティビティであることを想定しているため、OnFragmentInteractionListener インターフェイスを経由して結果を返します。今回示した、リクエストコードと onActivityResult() メソッドを使う方法は、親がフラグメントの場合に有効です。

収支の追加処理を行うダイアログを呼び出し、ダイアログからの戻り値を取得する流れを以下に示します。

  1. 呼び出し元から、ダイアログのファクトリメソッドを呼び出します。この際、結果を受け取るフラグメント(呼び出し元フラグメントの this)と、リクエストコード(REQUEST_ADD)を渡します。リクエストコードは、表示したダイアログの種類を後で確認するための識別子で、:num4: の処理で使用します。
  2. ファクトリメソッド newInstance() でフラグメントのインスタンスを生成します。この際、引数として渡されたフラグメントとリクエストコードを、ダイアログ内に設定しておきます。
  3. ダイアログでの入力を終えたら、onActivityResult() メソッドで結果を返します。通知先はフラグメントに設定された参照を使用します。onActivityResult() メソッドの引数には、リクエストコード(REQUEST_ADD)、リザルトコード(RESULT_OK)、ダイアログからの任意の戻り値を格納した Intent を渡します。
  4. 呼び出し元フラグメントの onActivityResult() メソッドでは、リクエストコードに応じて、結果の取得処理を行います。

これを実装している部分を以下に示します。

以下のコードは、先ほど示した BalanceListFragment での収支の編集ダイアログの呼び出し元です。図中の :num1: に相当します。

public class BalanceListFragment extends ListFragment {
  private static final int REQUEST_ADD = 1000;
  private static final int REQUEST_EDIT = 1001;

  @OnClick(R.id.button_add)
  void addClicked() {
    // Show the add dialog.
    ItemEditDialogFragment dialog = ItemEditDialogFragment.newInstance(this, REQUEST_ADD, null, "", Field.Type.INCOME, 0);
    dialog.show(getFragmentManager(), "");
  }
}

newInstance() メソッドの第 1 引数の this はダイアログの入力結果をこのクラスで受け取ることを意味し、第 2 引数の REQUEST_ADD は、結果を受け取った際に使用するリクエストコードを表します。ここでは、データ一覧画面の "+" ボタンのハンドラー addClicked() から ItemEditDialogFragment クラスを収支の追加の目的で使用しているため、REQUEST_ADD としています。編集処理では同じダイアログを、リクエストコード REQUEST_EDIT で使用します。これにより、:num4: で結果を受け取ったとき、リクエストコードからダイアログの使用目的を区別できます。

リクエストコードはクラスの先頭にある定数宣言で適当な数値を割り当てています。数値は BalanceListFragment で一意の値であれば、どのような値でも問題ありません。

第 3 引数以降は、フラグメントのインスタンス化 で説明したダイアログへの引数です。

次に、呼び出し先の ItemEditDialogFragment では、以下のような処理を行っています。

public class ItemEditDialogFragment extends DialogFragment {
  static final String RESULT_OBJECT_ID = "objectId";
  static final String RESULT_NAME = "name";
  static final String RESULT_TYPE = "type";
  static final String RESULT_AMOUNT = "amount";

  public static ItemEditDialogFragment newInstance(Fragment target, int requestCode,
          String objectId, String name, int type, int amount) {
    ItemEditDialogFragment fragment = new ItemEditDialogFragment();
    fragment.setTargetFragment(target, requestCode);
    ......
    return fragment;
  }

  private void submit(String action) {
    Fragment target = getTargetFragment();
    if (target == null) { return; }

    String name = mNameEdit.getText().toString();
    int type = toType(mRadioGroup.getCheckedRadioButtonId());
    int amount = toInt(mAmountEdit.getText().toString());
    int subAmount = toInt(mSubAmountEdit.getText().toString());

    Intent data = new Intent(action);
    data.putExtra(RESULT_OBJECT_ID, mObjectId);
    data.putExtra(RESULT_NAME, name);
    data.putExtra(RESULT_TYPE, type);
    data.putExtra(RESULT_AMOUNT, amount * 100 + subAmount);

    target.onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, data);
  }
}

newInstance() メソッドと setTargetFragment() メソッドは図中の :num2: に相当します。

newInstance() メソッドでは フラグメントのインスタンス化 で説明した処理を行います。この際、setTargetFragment() メソッドを呼び出して、戻り値を返すフラグメントとリクエストコードを、ダイアログに設定します。setTargetFragment() メソッドは、Fragment クラスが提供しているフラグメントの機能です。

ダイアログで処理が完了すると、Kii Balance では、submit() メソッドでダイアログの結果を戻します。この処理は :num3: に相当します。

submit() メソッドでは以下の処理が実行されます。

  • onActivityResult() メソッドの対象となるインスタンスを getTargetFragment() で取得します。これは、ファクトリメソッドの setTargetFragment() で設定したフラグメントです。

  • ユーザーインターフェイスから値を取り出して、編集結果を Intent に格納します。この際、Intentaction には、タップされたボタンに応じた createupdatedelete のいずれかの文字列が渡されます。

  • ファクトリメソッドで設定したリクエストコードを getTargetRequestCode() で取得します。

  • ダイアログの処理結果を返すコードを RESULT_OK とします。

最後に、BalanceListFragment クラスで結果を受け取ります。以下は onActivityResult() メソッドの実装です。この処理は :num4: に相当します。

public class BalanceListFragment extends ListFragment {
  @Override
  public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (resultCode != Activity.RESULT_OK) { return; }

    switch (requestCode) {
    case REQUEST_ADD: {
      String name = data.getStringExtra(ItemEditDialogFragment.RESULT_NAME);
      int type = data.getIntExtra(ItemEditDialogFragment.RESULT_TYPE, Field.Type.EXPENSE);
      int amount = data.getIntExtra(ItemEditDialogFragment.RESULT_AMOUNT, 0);

      createObject(name, type, amount);
      break;
    }
    case REQUEST_EDIT: {
      ......
    }
    }
    super.onActivityResult(requestCode, resultCode, data);
  }
}

ここでは、ダイアログ側で用意した値を引数から取得して、アプリケーションに依存した処理を実行します。

実際のモバイルアプリの開発にもこの流れを応用できます。onActivityResult() メソッドでリクエストコードごとに処理の種類を振り分け、ダイアログの入力値を Intent から取得するという流れは、多くのモバイルアプリでも共通して利用できるはずです。


次は...

技術の解説は以上です。次のページからは、これらの技術を使った Kii Balance の実装について説明します。

アクティビティの実装 に移動してください。