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

このページでは、Android SDK のフラグメントを使って画面遷移を行う実装を説明します。

フラグメントは Kii Cloud の範囲外の機能です。このページではフラグメントの Kii Cloud への応用を説明していますが、実装方法はフラグメントの基本に基づいています。詳細は、専門書籍をご覧になるか、Web 上の一般的な技術情報などを検索してください。

画面遷移

Hello Thing-IF はログイン画面とコマンド画面の 2 つの画面を持っています。ログイン画面で初期登録(onboard)が完了すると、ThingIFAPI のインスタンスを生成し、コマンド画面に遷移します。

ThingIFAPI は Thing-IF SDK の API を実行するためのクラスです。メソッドには、コマンドの実行や、ステートの取得など、Thing Interaction Framework に対する API が用意されています。

モバイルアプリの画面遷移は、ThingIFAPI インスタンスの管理と密接な関係があります。Hello Thing-IF では画面遷移を以下の図のように実現します。LoginFragment の初期登録で ThingIFAPI インスタンスを生成し、得られた ThingIFAPI を CommandFragment まで中継します。同時に、プロセス再起動後も保持し続けるようにシリアライズなどの管理を行います。

MainActivity のユーザーインターフェイスでは、R.id.main として、フラグメントを使った画面切り替え用の領域を確保しています。モバイルアプリの起動時、MainActivity の onCreate() では、以下のように LoginFragment を設定します(条件による画面遷移の実装方法は後述の 手順 2 で示します)。

public class MainActivity extends AppCompatActivity implements LoginFragment.OnFragmentInteractionListener {
  protected void onCreate(Bundle savedInstanceState) {
    ......
    FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
    transaction.replace(R.id.main, LoginFragment.newInstance());
    transaction.commit();
    ......
  }
}

この後、図の 1~4 に従って、以下のような処理を行います。

  1. 初期登録完了時の中継処理
  2. MainActivity での保持
  3. コマンド画面への中継
  4. CommandFragment での利用

1. 初期登録完了時の中継処理

これは、図の「1. 初期登録完了時 ThingIFAPI を渡す」の処理です。

LoginFragment ではログイン画面の処理を実装します。ONBOARD ボタンがクリックされると、初期登録の処理を行い、結果として ThingIFAPI のインスタンスが生成されます。これを MainActivity 経由で中継します。

中継処理を実現するため、LoginFragment では OnFragmentIntentListener インターフェイスを定義します。ここでは初期登録完了時に ThingIFAPI インスタンスを渡すためのメソッド onThingIFInitialized() を定義しています。このインターフェイスは MainActivity が実装します。

public class LoginFragment extends Fragment {
  ......
  public interface OnFragmentInteractionListener {
    void onThingIFInitialized(ThingIFAPI api);
  }
}

public class MainActivity extends AppCompatActivity implements LoginFragment.OnFragmentInteractionListener {
  private ThingIFAPI mApi;

  @Override
  public void onThingIFInitialized(ThingIFAPI api) {
    mApi = api;
    ......
  }
}

LoginFragment では OnFragmentInteractionListener を以下のように利用します。これは、Android Studio で Fragment クラスを新規作成すると作成されるテンプレートのとおりです。

public class LoginFragment extends Fragment {
  private OnFragmentInteractionListener mListener;

  @Override
  public void onAttach(Context context) {
    super.onAttach(context);
    if (context instanceof OnFragmentInteractionListener) {
      mListener = (OnFragmentInteractionListener) context;
    } else {
      throw new RuntimeException(context.toString()
              + " must implement OnFragmentInteractionListener.");
    }
  }

  @OnClick(R.id.buttonOnboard)
  void onOnboardClicked() {
    ......
    mAdm.when(api.initializeThingIFApi(getContext(), username, userPassword, vendorThingID, thingPassword)
    ).then(new DoneCallback<ThingIFAPI>() {
      @Override
      public void onDone(ThingIFAPI api) {
        if (mListener != null) {
          mListener.onThingIFInitialized(api);
        }
        ......
      }
    });
    ......
  }
}

LoginFragment の onAttach() は MainActivity と LoginFragment が紐付いた際のイベントです。ここで、mListener に MainActivity の OnFragmentInteractionListener インターフェイスを保持しておきます。これによって、LoinFragment から MainActivity に ThingIFAPI を引き渡す経路が形成されます。

ONBOARD ボタンがクリックされると、Butter Knife によって onOnboardClicked() メソッドが呼び出されます。ここでは、api.initializeThingIFAPI() メソッドによって初期登録を実行します。成功時には Promise によって onDone() が実行されます。最終的に、初期登録で得られた ThingIFAPI インスタンス api を、mListener.onThingIFInitialized(api); によって、MainActivity に渡します。

2. MainActivity での保持

これは、図の「2. 保持/シリアライズ管理」の処理です。

MainActivity では、得られた ThingIFAPI インスタンスを mApi フィールドで保持します。また、シリアライズ機能を実装して、再起動時にもインスタンスが保持され続けるように管理します。

モバイルアプリの起動時には、シリアライズの結果から ThingIFAPI インスタンスを復元できたかどうかによって、画面遷移を決定します。

これらの処理を MainActivity から抜粋すると、以下のようになります。

public class MainActivity ... {
  private static final String BUNDLE_KEY_THING_IF_API = "ThingIFAPI";
  private ThingIFAPI mApi;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ......
    if (savedInstanceState != null) {
      // restore ThingIFAPI from the Bundle
      mApi = savedInstanceState.getParcelable(BUNDLE_KEY_THING_IF_API);
    }
    if (mApi == null) {
      // restore ThingIFAPI from the storage
      try {
          mApi = ThingIFAPI.loadFromStoredInstance(getApplicationContext());
      } catch (StoredThingIFAPIInstanceNotFoundException e) {
          mApi = null;
      }
    }
    if (savedInstanceState == null) {
      // create ui elements
      if (mApi == null) {
        // if mApi has not been set, restart the login page
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        transaction.replace(R.id.main, LoginFragment.newInstance());
        transaction.commit();
      } else {
        // if mApi has already been set, skip the login page
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        transaction.replace(R.id.main, CommandFragment.newInstance(mApi));
        transaction.commit();
        ......
      }
    }
  }

  @Override
  protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putParcelable(BUNDLE_KEY_THING_IF_API, mApi);
  }
}

onCreate() での ThingIFAPI の復元は、以下の 2 段階で行っています。

  1. プロセス再起動時の復元

    Android では、モバイルアプリがバックグラウンドに遷移したり言語設定が変更されたりした場合に Bundle を使って ThingIFAPI インスタンスを保存/復元する処理を行います。

    ThingIFAPI は、Parcelable インターフェイスを実装しているため、Bundle に保存できます。

    アクティビティが破棄されるとき、onSaveInstanceState() の処理で ThingIFAPI インスタンスをパラメーター BUNDLE_KEY_THING_IF_API で保存します。onCreate() ではこれを復元します。復元した結果は mApi に保持します。

  2. 新規起動時のデシリアライズ

    savedInstanceState.getParcelable() を行っても mApi が null の場合、Bundle からの復元に失敗しています。その場合は、ここでの処理を行います。

    バックグラウンドに Hello Thing-IF モバイルアプリがない状態で、ホーム画面からプロセスが新規に起動されると、savedInstanceState は null の状態で起動して if (mApi == null) { のロジックに入ります。

    このとき、前回の ThingIFAPI インスタンスを SharedPreferences から復元する処理を試みます。ThingIFAPI.loadFromStoredInstance() メソッドを実行するとシリアライズされた ThingIFAPI インスタンスを復元できます。Thing-IF SDK の内部では、ThingIFAPI が作成または更新されたタイミングで、自動的に SharedPreferences に保存されています。onCreate() では、これを mApi に復元します。

    モバイルアプリのインストール直後などは SharedPreferences に保存されていないため、API は StoredThingIFAPIInstanceNotFoundException 例外をスローします。この場合、mApi は null の状態を保持します。

次に、遷移先画面を決めます。

Activity の onCreate() では、savedInstanceState != null のとき、内部のビューは OS によって自動的に再生成されます。この場合、ビューの作成処理は不要です。たとえば、画面を回転した場合が該当します。

savedInstanceState が null の場合、ビューの再作成が必要です。ここでは、復元した mApi の状態によって遷移先の画面を決めます。mApi が null の場合は LoginFragment を作成してログイン画面に遷移します。mApi が復元できた場合は初期登録済みのため、CommandFragment を作成してコマンド画面に遷移します。

この実装では、 ThingIFAPI が SharedPreferences に保存された後は、モバイルアプリのデータを削除しない限り、必ずコマンド画面が開いてしまいます。アカウントの再設定ができないと不便であるため、メニューから LoginFragment を表示できる処理も用意しています。詳細は、MainActivity の onCreateOptionsMenu() を参照してください。

3.コマンド画面への中継

これは、図の「3. フラグメント初期化パラメーターとして ThingIFAPI を渡す」の処理です。フラグメントではパラメーターの渡し方に注意が必要です。

すでに見たように、MainActivity が作成されて ThingIFAPI インスタンスが有効な場合、onCreate() ではコマンド画面へ遷移します。ここでは以下のように CommandFragment に mApi を渡してフラグメントを作成し、ビューに設定しています。

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  ......
  FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
  transaction.replace(R.id.main, CommandFragment.newInstance(mApi));
  transaction.commit();
  ......
}

また、1. 初期登録完了時の中継処理 で、MainActivity の onThingIFInitialized が呼び出された場合も、同様に CommandFramgent を作成します。

@Override
public void onThingIFInitialized(ThingIFAPI api) {
  mApi = api;
  FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
  transaction.replace(R.id.main, CommandFragment.newInstance(mApi));
  transaction.commit();
  ......
}

CommandFragment は、ThingIFAPI を初期化時の引数に取ります。ただし、Fragment クラスは Android によって任意のタイミングで自動的に再作成されるため、new CommandFragment(mApi); のようにコンストラクタで引数を渡すことはできません。

これを解決するため、以下のように Bundle 経由でパラメーターを渡すように実装します。これは、Android Studio で Fragment クラスを新規作成したときのテンプレートに記載されている実装方法と同じです。

public class CommandFragment extends Fragment {
  private static final String ARG_THING_IF_API = "ThingIFAPI";
  private ThingIFAPI mApi;

  public CommandFragment() {
  }

  public static CommandFragment newInstance(ThingIFAPI api) {
    CommandFragment fragment = new CommandFragment();
    Bundle args = new Bundle();
    args.putParcelable(ARG_THING_IF_API, api);
    fragment.setArguments(args);
    return fragment;
  }

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (getArguments() != null) {
      mApi = getArguments().getParcelable(ARG_THING_IF_API);
    }
  }
}

newInstance メソッドでは、CommandFragment のインスタンス生成時に、ARG_THING_IF_API パラメーターとして引数の ThingIFAPI インスタンスを格納します。

onCreate() では、設定した ARG_THING_IF_API パラメーターから ThingIFAPI インスタンスを復元して、クラスのフィールドで保持します。

このように実装することで、CommandFragment が OS によって自動的に再作成されたときにも、引数で渡された ThingIFAPI インスタンスは OS が管理しているため、復元することができます。

4. CommandFragment での利用

これは、図中の「4. 保持/利用」に相当する処理です。

すでに見たように、CommandFragment では、ThingIFAPI インスタンスがクラスのフィールドとして保持されています。

public class CommandFragment extends Fragment {
  private ThingIFAPI mApi;
  ......
}

コマンド画面でのコマンドの送信ボタンやステートの取得ボタンのハンドラーでは、この ThingIFAPI インスタンスの API を呼び出すことで Thing-IF SDK の機能を利用することができます。

処理の詳細は、コマンド画面の実装 で説明します。

ProgressDialogFragment の利用

Hello Thing-IF では、処理中を表すダイアログとして、独自のクラス ProgressDialogFragment を使用しています。このクラスは DialogFragment の派生クラスとして実装しています。

Kii Cloud の API 呼び出しのように、ネットワークアクセスなどで時間がかかる処理で使用します。時間がかかる処理は、以下のように実装します。

// Open ProgressDialogFragment
ProgressDialogFragment.show(getActivity(), getFragmentManager(), R.string.add_user);

// Do something.
......

// Close ProgressDialogFragment
ProgressDialogFragment.close(getFragmentManager());

show のメソッドには、ダイアログのタイトルとメッセージに使用される文字列リソースを渡します。ProcessDialogFragment は画面中に 1 つしか表示されない想定のため、スタティックメソッドの showclose で表示と破棄を行うことができます。

非同期処理で ProcessDialog を表示中に画面を回転させると、アクティビティが再作成されるため、非同期処理完了時に ProcessDialogFragment にアクセスできなくなります。これを防止するため、フラグメントの開始時に ProcessDialogFragment を閉じる処理を実行しています。

@Override
public void onStart(){
  super.onStart();
  ProgressDialogFragment.close(getFragmentManager());
}

次のページ以降に示すサンプルコードでは、単純化のため、ProgressDialogFragment 関連のコードは省略しています。


次は...

ログイン画面の処理について説明します。API の使用方法などを見ていきます。

ログイン画面の実装 に移動してください。

より詳しく学びたい方へ