Android:Stateパターンを使ったアプリの状態遷移の実装例

今回はAndroidアプリ開発においてのStateパターンの実装を、サンプルソースを交えて紹介したいと思います。

その前に、なぜ状態管理が必要なのか。

実は以前、業務でやたらと状態とイベントの多いアプリケーションの作成に携わったことがあります。

どのくらい多かったかと言いますと、状態が約50個、イベントが約120個のマトリクス、つまり50×120=全6000パターンの処理を1つ1つ定義しなければなりませんでした。

普通にif文などで分岐をかけていたのではそれぞれの処理を管理し切れず、プロジェクトが爆発炎上するのは間違いなかったため、デザインパターンの1つ、状態遷移を行うためのStateパターンを用いて設計を行うに至ったのです。

以下で紹介するのは、その設計をもう少し改良した、小さなサンプルです。

画面をタッチするとトースト表示を行うアプリケーション。

アプリケーションが起動した状態で端末の画面をタッチすると、トーストの表示が行われるだけの簡単なアプリです。

クラス図。

状態定義がいくつに増えても、この構成は変わりません。

Stateパターンクラス図

状態遷移図。

今回は状態を、初期/起動/停止の3つと定義します。それぞれActivityライフサイクルメソッドの実行で状態が遷移します。

Stateパターン状態遷移図

状態クラス定義。

AbsState.java

以下はJavadocコメントの通り、各状態の親となるクラスです。

ここで定義しなければならないのは、状態開始/終了のメソッド、そしてActivityに振ってくるであろうイベント(ライフサイクルやリスナーコールバック)全てです。

/**
 * 各状態の親となるクラス
 *
 * @author
 *
 */
public abstract class AbsState {
	/** ログは各サブクラスの名前で出力したい */
	protected abstract String getTag();

	/**
	 * 状態開始
	 *
	 * @param activity
	 */
	public void entry(EventHandleActivity activity) {
		Log.d(getTag(), "Not implemented.");
	}

	/**
	 * 状態終了
	 *
	 * @param activity
	 */
	public void exit(EventHandleActivity activity) {
		Log.d(getTag(), "Not implemented.");
	}

	/*
	 * これより下は、EventHandleActivityにて宣言されるインターフェースと合わせる必要があります
	 */
	public void onCreate(EventHandleActivity activity, Bundle savedInstanceState) {
		Log.d(getTag(), "Not implemented.");
	}
	public void onResume(EventHandleActivity activity) {
		Log.d(getTag(), "Not implemented.");
	}
	public void onPause(EventHandleActivity activity) {
		Log.d(getTag(), "Not implemented.");
	}
	public void onDestroy(EventHandleActivity activity) {
		Log.d(getTag(), "Not implemented.");
	}
	public boolean dispatchTouchEvent(EventHandleActivity activity, MotionEvent ev) {
		Log.d(getTag(), "Not implemented.");
		return false;
	}
}
StateInit.java

初期状態クラス。onCreate()が発生すると初期化処理を行い、onResume()で起動状態へ遷移します。

なお、以降の状態定義クラスは全てSingleton(シングルトン)パターンで実装します。

/**
 * 初期状態
 *
 * @author
 *
 */
public class StateInit extends AbsState {
	/** 唯一のインスタンス(Singleton) */
	private static AbsState sInstance = new StateInit();
	private StateInit() {
	}
	public static AbsState getInstance() {
		return sInstance;
	}

	@Override
	protected String getTag() {
		return getClass().getSimpleName();
	}

	@Override
	public void onCreate(EventHandleActivity activity, Bundle savedInstanceState) {
		// 初期化処理を行う
		activity.init();
	}

	@Override
	public void onResume(EventHandleActivity activity) {
		// 起動状態へ移行する
		activity.next(StateIdle.getInstance());
	}
}
StateIdle.java

起動状態クラス。dispatchTouchEvent()が発生するとトースト表示、onPause()が発生すると停止状態への遷移が行われます。

/**
 * 起動状態
 *
 * @author
 *
 */
public class StateIdle extends AbsState {
	/** 唯一のインスタンス(Singleton) */
	private static AbsState sInstance = new StateIdle();
	private StateIdle() {
	}
	public static AbsState getInstance() {
		return sInstance;
	}

	@Override
	protected String getTag() {
		return getClass().getSimpleName();
	}

	@Override
	public boolean dispatchTouchEvent(EventHandleActivity activity, MotionEvent ev) {
		// タッチダウンならトースト表示
		if (ev.getAction() == MotionEvent.ACTION_DOWN) {
			activity.showHogeToast();
		}
		return false;
	}

	@Override
	public void onPause(EventHandleActivity activity) {
		// 停止状態へ移行する
		activity.next(StatePause.getInstance());
	}
}
StatePause.java

停止状態クラス。onResume()発生で起動状態へ、onDestroy()発生でログを出力します。

/**
 * 停止状態
 *
 * @author
 *
 */
public class StatePause extends AbsState {
	/** 唯一のインスタンス(Singleton) */
	private static AbsState sInstance = new StatePause();
	private StatePause() {
	}
	public static AbsState getInstance() {
		return sInstance;
	}

	@Override
	protected String getTag() {
		return getClass().getSimpleName();
	}

	@Override
	public void onResume(EventHandleActivity activity) {
		// 起動状態へ移行する
		activity.next(StateIdle.getInstance());
	}

	@Override
	public void onDestroy(EventHandleActivity activity) {
		// 終了ログを出力する
		activity.finishLog();
	}
}

Activityクラス定義。

EventHandleActivity.java

こちらもJavadocの通り、発生したイベントと同名のメソッド(これはAbsStateクラスに定義していなければならない)を実行するだけのクラスです。

一言で言えば、単なるトンネルです。

/**
 * ライフサイクルやリスナーイベントなどをハンドリングするだけのActivity
 *
 * @author
 *
 */
public class EventHandleActivity extends OperationActivity {
	/** 現在の状態 */
	private AbsState mState = StateInit.getInstance();

	/**
	 * 現在の状態名(状態クラスの名前)を取得する
	 *
	 * @return 現在の状態名
	 */
	private String getStateName() {
		return mState.getClass().getSimpleName();
	}

	/**
	 * 次の状態へ遷移する
	 *
	 * @param nextState
	 *            次の状態
	 */
	public void next(AbsState nextState) {
		if (nextState == null) {
			// nullならエラー終了
			throw new RuntimeException();
		}

		// これまでの状態が終了する時の処理
		Log.d(getStateName(), "exit in.");
		mState.exit(this);
		Log.d(getStateName(), "exit out.");
		mState = nextState;

		// 次の状態が開始する時の処理
		Log.d(getStateName(), "entry in.");
		nextState.entry(this);
		Log.d(getStateName(), "entry out.");
	}

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		Log.d(getStateName(), "onCreate in.");
		// 引数には自身のインスタンス(this)と、元々のイベントの引数(このメソッドであれば Bundle
		// savedInstanceState)を渡すようにする
		mState.onCreate(this, savedInstanceState);
		Log.d(getStateName(), "onCreate out.");
	}

	@Override
	public void onResume() {
		super.onResume();
		// このように in/out のログで囲っておけば、デバッグ時の追跡がしやすい
		Log.d(getStateName(), "onResume in.");
		mState.onResume(this);
		Log.d(getStateName(), "onResume out.");
	}

	@Override
	public void onPause() {
		super.onPause();
		Log.d(getStateName(), "onPause in.");
		mState.onPause(this);
		Log.d(getStateName(), "onPause out.");
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		Log.d(getStateName(), "onDestroy in.");
		mState.onDestroy(this);
		Log.d(getStateName(), "onDestroy out.");
	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.d(getStateName(), "dispatchTouchEvent in.");
		// 戻り値なども状態クラス側に任せる
		boolean ret = mState.dispatchTouchEvent(this, ev);
		Log.d(getStateName(), "dispatchTouchEvent out.");
		return ret;
	}
}
OperationActivity.java

こちらには実際のアプリの挙動のみを定義します。

注意したいのは、ここでif文やswitch文などで分岐処理を行なってはいけません。それをしてしまうとStateパターンの設計の意味が無くなり、全ての苦労が水の泡となります。

/**
 * 実際に行う処理を定義していくだけのActivity
 *
 * @author
 *
 */
public abstract class OperationActivity extends Activity {
	private static final String TAG = "OperationActivity";

	/**
	 * 初期化処理
	 */
	protected void init() {
		Log.d(TAG, "init");
		setContentView(R.layout.activity_main);
	}

	/**
	 * 文字列(hogehoge)をトースト表示する
	 */
	protected void showHogeToast() {
		Log.d(TAG, "showHogeToast");
		Toast.makeText(this, "hogehoge.", Toast.LENGTH_SHORT).show();
	}

	/**
	 * 終了時ログ出力
	 */
	protected void finishLog() {
		Log.d(TAG, "finishLog");
		Log.d(getClass().getSimpleName(), "owari masu.");
	}
}

結局、この設計は何が利点なのか。

それは絶対ありえない!と言い切れる幸せ。

例えば上のサンプルの例で言えば、初期状態(StateInit)である時にActivity#onPause()が実行されたとしても、LogCatに”Not implemented.”と出力されるだけで何も処理が行われません。

これはStateInitクラスにonPause()の定義がされていないため、AbsState#onPause()が代わりに実行されるからです。

このように定義すると、ある状態の時に実行するべき処理と、実行するべきではない処理が明確に区別できます。

つまりアプリがプログラマが期待した通りの状態であれば、そこで行われる処理もプログラマが期待した通りのものしか存在しないということです。

StateInit状態の時に「onPause()が発生してStatePauseに移行してしまう」「dispatchTouchEvent()でトーストを表示しようとしてエラー終了してしまう」などといったバグは「絶対にありえない」と言い切れるのです。

バグは無いと言い切れるなんて、プログラマにとっては幸せでしょう?

またユニットテストも簡単になります。

Activityクラスに実装した処理(メソッド)の単体テストというのは、中々頭を悩ませるところではないでしょうか。普通に実装すれば、分岐だらけになってしまうからです。

しかし上の実装では、分岐は全て状態定義クラスに任せてしまっているため、Activityの実装が簡潔かつ必要最小限になり、結果としてテストやデバッグ、修正が行い易くなっています。

ただし、代わりにクラス定義が増えます。

定義された状態の数だけ、クラスの定義を作らなければならないのがこの設計の難点でしょうか。

ただ実際、仕事としてデバッグまで行なってみると、どこでバグが出ているのか、なぜバグが出たのか、それらの解析がとてもしやすかったです。

状態定義が多くて、とてもじゃないけど全ての遷移を網羅し切れなかった。そういう経験があるプログラマの方は、Stateパターンを覚えてみると良いかもしれません。おすすめです。

スポンサーリンク


コメントを残す

このページの先頭へ