Help us understand the problem. What is going on with this article?

AndroidでStateパターンを使って状態を管理してみた

More than 3 years have passed since last update.

少し複雑な操作が要求される画面を作る機会があったので、こんな感じに状態を表現できるかなということで試してみました。

作るもの

  • お試しなので単純にAPIを読んでリストに表示するだけの画面です。APIは何でもよかったのですが、ATNDさんのイベントを引っ張っています。

状態遷移

  • こんな感じ

スクリーンショット 2016-03-13 19.44.39.png

  • 初期状態から始まって、表示するデータを持ってなければ、準備中に状態遷移してAPIを呼んで取ってきたデータを表示して、入力待ちに状態遷移。初期状態時にすでにデータを持っていればそのまま表示して入力待ちに状態遷移、というイメージ。

登場するクラス

  • MainFragment ・・・ stateを管理するだけのクラス。ライフサイクルやイベントに応じて保持しているstateの対応するメソッドを呼ぶ。
  • MainFragmentLogic ・・・ Viewの操作やアプリの挙動などを定義する。
  • MainFragmentState ・・・ Fragmentの状態を表現するenum。各状態は必要に応じてメソッドをオーバライドする。

    • INIT : 初期状態
    • GETTING_READY : 準備中
    • IDLE : 入力待ち
    • PAUSE : 停止状態
  • 例えばINITの場合

    INIT {
        @Override
        public void onActivityCreated(MainFragment fragment, Bundle savedInstanceState) {
            super.onActivityCreated(fragment, savedInstanceState);
            fragment.initView();
        }

        @Override
        public void onResume(MainFragment fragment) {
            super.onResume(fragment);
            if (fragment.hasEventData()) {
                // すでにイベントのデータを持っていたら表示して入力待ちに状態遷移
                fragment.displayContents();
                fragment.next(IDLE);
            } else {
                // 持っていなかったら準備中に状態遷移
                fragment.next(GETTING_READY);
            }
        }
    },

実装

MainFragment.java
package me.rei_m.statepatternsample.fragment;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import me.rei_m.statepatternsample.fragment.state.MainFragmentState;
import me.rei_m.statepatternsample.model.AtndModel;

public class MainFragment extends MainFragmentLogic {

    public static MainFragment newInstance() {
        MainFragment fragment = new MainFragment();
        Bundle args = new Bundle();
        fragment.setArguments(args);
        return fragment;
    }

    private MainFragmentState state = MainFragmentState.INIT;

    public MainFragment() {
        // Required empty public constructor
    }

    public void next(MainFragmentState nextState) {
        state.exit(this);
        state = nextState;
        state.entry(this);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        state.onCreate(this, savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = super.onCreateView(inflater, container, savedInstanceState);
        state.onCreateView(this, savedInstanceState);
        return view;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        state.onActivityCreated(this, savedInstanceState);
    }

    @Override
    public void onStart() {
        super.onStart();
        state.onStart(this);
    }

    @Override
    public void onResume() {
        super.onResume();
        state.onResume(this);
    }

    @Override
    public void onPause() {
        super.onPause();
        state.onPause(this);
    }

    @Override
    public void onStop() {
        super.onStop();
        state.onStop(this);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        state.onDestroyView(this);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        state.onDestroy(this);
    }

    @Override
    public void subscribe(Object o) {
        if (o instanceof AtndModel.AtndEventLoadedEvent) {
            state.subscribe(this, (AtndModel.AtndEventLoadedEvent) o);
        }
    }
}
MainFragmentLogic.java
package me.rei_m.statepatternsample.fragment;

import android.app.ProgressDialog;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.TextView;

import butterknife.Bind;
import butterknife.ButterKnife;
import me.rei_m.statepatternsample.R;
import me.rei_m.statepatternsample.event.RxBusProvider;
import me.rei_m.statepatternsample.manager.ModelLocator;
import me.rei_m.statepatternsample.model.AtndModel;
import me.rei_m.statepatternsample.view.adaptor.AtndEventListAdapter;
import rx.android.schedulers.AndroidSchedulers;
import rx.subscriptions.CompositeSubscription;

abstract public class MainFragmentLogic extends Fragment {

    @Bind(R.id.list_atnd_event)
    ListView listAtndEvent;

    @Bind(R.id.text_list_atnd_empty)
    TextView emptyView;

    @Bind(R.id.text_list_atnd_error)
    TextView errorView;

    private CompositeSubscription compositeSubscription;

    private ProgressDialog progressDialog;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_main, container, false);
        ButterKnife.bind(this, view);
        return view;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        ButterKnife.unbind(this);
    }

    @Override
    public void onPause() {
        super.onPause();
        if (compositeSubscription != null) {
            compositeSubscription.unsubscribe();
            compositeSubscription = null;
        }
    }

    public void initView() {
        AtndEventListAdapter listAdapter = new AtndEventListAdapter(getContext(), R.layout.list_item_atnd_event);
        listAtndEvent.setAdapter(listAdapter);
        emptyView.setVisibility(View.GONE);
        errorView.setVisibility(View.GONE);
    }

    public void fetchEvent() {
        compositeSubscription = new CompositeSubscription();
        compositeSubscription.add(RxBusProvider.INSTANCE.get()
                .toObservable()
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(this::subscribe));
        ((AtndModel) ModelLocator.get(ModelLocator.Tag.ATND)).fetch();
    }

    public void showProgressDialog() {
        progressDialog = new ProgressDialog(getActivity());
        progressDialog.setCancelable(false);
        progressDialog.show();
    }

    public void hideProgressDialog() {
        progressDialog.dismiss();
    }

    abstract public void subscribe(Object o);

    public void displayContents() {
        AtndModel atndModel = ((AtndModel) ModelLocator.get(ModelLocator.Tag.ATND));
        AtndEventListAdapter listAdapter = (AtndEventListAdapter) listAtndEvent.getAdapter();
        listAdapter.clear();
        listAdapter.addAll(atndModel.getAtndEventList());
        listAdapter.notifyDataSetChanged();
    }

    public void displayEmptyMessage() {
        emptyView.setVisibility(View.VISIBLE);
    }

    public void displayErrorMessage() {
        errorView.setVisibility(View.VISIBLE);
    }

    public boolean hasEventData() {
        AtndModel atndModel = ((AtndModel) ModelLocator.get(ModelLocator.Tag.ATND));
        return (0 < atndModel.getEventCount());
    }
}
MainFragmentState.java
package me.rei_m.statepatternsample.fragment.state;

import android.os.Bundle;
import android.util.Log;

import me.rei_m.statepatternsample.fragment.MainFragment;
import me.rei_m.statepatternsample.model.AtndModel;

public enum MainFragmentState {

    INIT {
        @Override
        public void onActivityCreated(MainFragment fragment, Bundle savedInstanceState) {
            super.onActivityCreated(fragment, savedInstanceState);
            fragment.initView();
        }

        @Override
        public void onResume(MainFragment fragment) {
            super.onResume(fragment);
            if (fragment.hasEventData()) {
                fragment.displayContents();
                fragment.next(IDLE);
            } else {
                fragment.next(GETTING_READY);
            }
        }
    },
    GETTING_READY {
        @Override
        public void entry(MainFragment fragment) {
            super.entry(fragment);
            fragment.showProgressDialog();
            fragment.fetchEvent();
        }

        @Override
        public void onPause(MainFragment fragment) {
            super.onPause(fragment);
            fragment.next(PAUSE);
        }

        @Override
        public void subscribe(MainFragment fragment, AtndModel.AtndEventLoadedEvent event) {
            super.subscribe(fragment, event);
            fragment.hideProgressDialog();
            if (event.isSuccess()) {
                if (fragment.hasEventData()) {
                    fragment.displayContents();
                } else {
                    fragment.displayEmptyMessage();
                }
            } else {
                fragment.displayErrorMessage();
            }
            fragment.next(IDLE);
        }
    },
    IDLE {
        @Override
        public void onPause(MainFragment fragment) {
            super.onPause(fragment);
            fragment.next(PAUSE);
        }
    },
    PAUSE {
        @Override
        public void onCreateView(MainFragment fragment, Bundle savedInstanceState) {
            super.onCreateView(fragment, savedInstanceState);
            fragment.next(INIT);
        }
    };

    MainFragmentState() {
    }

    private String tag = MainFragmentState.class.getSimpleName() + "/" + name();

    public void entry(MainFragment fragment) {
        Log.d(tag, "entry");
    }

    public void exit(MainFragment fragment) {
        Log.d(tag, "exit");
    }

    public void onCreate(MainFragment fragment, Bundle savedInstanceState) {
        Log.d(tag, "onCreate");
    }

    public void onCreateView(MainFragment fragment, Bundle savedInstanceState) {
        Log.d(tag, "onCreateView");
    }

    public void onActivityCreated(MainFragment fragment, Bundle savedInstanceState) {
        Log.d(tag, "onActivityCreated");
    }

    public void onStart(MainFragment fragment) {
        Log.d(tag, "onStart");
    }

    public void onResume(MainFragment fragment) {
        Log.d(tag, "onResume");
    }

    public void onPause(MainFragment fragment) {
        Log.d(tag, "onPause");
    }

    public void onStop(MainFragment fragment) {
        Log.d(tag, "onStop");
    }

    public void onDestroyView(MainFragment fragment) {
        Log.d(tag, "onDestroyView");
    }

    public void onDestroy(MainFragment fragment) {
        Log.d(tag, "onDestroy");
    }

    public void subscribe(MainFragment fragment, AtndModel.AtndEventLoadedEvent event) {
        Log.d(tag, "subscribe/AtndEventLoadedEvent");
    }
}

おまけ

  • 上記のコードはこちらに。一気に書いたのでちょっと変なところがあるかも。。。
  • 状態はEnumで表現しましたが、親クラスをabstractとしたり、interfaceにする形でも同じようにできると思います。

参考

rei-m
AndroidやったりRailsやったりReactやったり。フロント寄りのお仕事が多いです。
http://rei19.hatenablog.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした