この 2 日間、冬休みが始まり、家にいるのも退屈なので、以前の Android の大御所が書いたプロジェクトを学ぶことに決めました。他の人のコードの構造や、コードの結合度を下げる方法を見てみるためです。そして、先輩のプロジェクトで BaseActivity に触れたので、ここに記録します。
なぜ基底クラスを設計するのか#
- コードの記述を便利にし、重複コードや冗長なロジックを減らし、コードを最適化するため
- プログラムのアーキテクチャを最適化し、結合度を下げ、拡張や修正を容易にするため
- レイアウトがよりクリーンになり、ライフサイクルログなどの重複ロジックによるレイアウトの占有を減らすため
設計の基本的な考え方#
- ライフサイクルのデバッグログ出力
- ビューのバインディング
- よく使う OnClick メソッド
- 戻るメソッド、トースト、アクティビティなどの操作
- よく使うサードパーティツール(ButterKnife など)
- タイトルバーの表示、全画面表示、データの初期化など
具体的な基底クラスのカプセル化#
BaseActivity#
public abstract class BaseActivity<P extends BasePresenter> extends AppCompatActivity
implements BaseView<P> {
@BindView(R.id.toolbar)
Toolbar mToolbar;
Unbinder bind;
/**
* プログレスダイアログ
*/
protected ProgressDialog mProgressDialog;
/**
* ジェネリックでPresenterを確定
*/
protected P mPresenter;
@Override
protected final void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 縦画面に設定
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
setContentView(bindLayout());
// ButterKnifeでレイアウトをバインド
bind = ButterKnife.bind(this);
mPresenter = createPresenter();
if (mPresenter != null) {
// Presenterの初期化メソッドを呼び出す
mPresenter.onStart();
}
// データの準備
prepareData();
// タイトルバーの初期化
initToolbar();
// ビューの初期化
initView();
// データの初期化
initData(savedInstanceState);
// イベントリスナーの初期化
initEvent();
}
/**
* Presenterを作成する
*
* @return ジェネリックPresenter
*/
protected abstract P createPresenter();
/**
* BasePresenterインターフェースのsetPresenterメソッドを実装
*
* @param presenter createPresenter()で作成されたPresenter
*/
@Override
public void setPresenter(P presenter) {
mPresenter = presenter;
}
/**
* Toolbarを初期化する
*/
private void initToolbar() {
setSupportActionBar(mToolbar);
}
/**
* Toolbarのタイトルを設定する
*
* @param title タイトル
*/
protected void setToolbarTitle(String title) {
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle(title);
}
}
/**
* Toolbarに戻るボタンとタイトルを表示する
*
* @param title タイトル
*/
protected void setToolbarBackEnable(String title) {
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_arrow_white_24dp);
setToolbarTitle(title);
}
}
/**
* レイアウトをバインドする
*
* @return レイアウトファイルのリソースID
*/
protected abstract int bindLayout();
/**
* データを準備する(Intentから前の画面から渡されたデータや他に初期化が必要なデータを取得)
*/
protected abstract void prepareData();
/**
* ビューを初期化する、findViewByIdなど
*/
protected abstract void initView();
/**
* データを初期化する、ローカルまたはサーバーからデータを取得する
*
* @param savedInstanceState 異常終了時に保存されたデータ
*/
protected abstract void initData(Bundle savedInstanceState);
/**
* イベントリスナーを初期化する、setOnClickListenerなど
*/
protected abstract void initEvent();
/**
* BaseViewのshowToast(CharSequence msg)を実装
*
* @param msg トーストに表示するメッセージ
*/
@Override
public void showToast(CharSequence msg) {
ToastUtils.shortToast(this, msg);
}
/**
* BaseViewのshowToast(int msgId)を実装
*
* @param msgId トーストに表示する文字列リソースID
*/
@Override
public void showToast(int msgId) {
ToastUtils.shortToast(this, msgId);
}
/**
* BaseViewのshowLoadingDialog(CharSequence msg)を実装
* ローディングダイアログを表示する
*
* @param msg ダイアログのヒント内容
*/
@Override
public void showLoadingDialog(CharSequence msg) {
if (mProgressDialog == null) {
mProgressDialog = new ProgressDialog(this);
mProgressDialog.setTitle(R.string.title_dialog_tips);
mProgressDialog.setMessage(msg);
} else {
mProgressDialog.setTitle(R.string.title_dialog_tips);
}
mProgressDialog.show();
}
/**
* BaseViewのhideLoadingDialog()を実装
* ローディングダイアログを非表示にする
*/
@Override
public void hideLoadingDialog() {
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
}
}
/**
* Activityが破棄されるときにリソースをクリーンアップする
*/
@Override
protected void onDestroy() {
// ButterKnifeのバインディングを解除
bind.unbind();
// Presenterを破棄
if (mPresenter != null) {
mPresenter.onDestroy();
}
super.onDestroy();
}
/**
* キーボードを隠す
*/
public void hideKeyboard() {
View view = getCurrentFocus();
if (view != null) {
((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)).
hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
}
}
@Override
protected void onPause() {
super.onPause();
// (ActivityのみのアプリではSDKが自動的に呼び出すため、個別に記述する必要はありません)
// onPageEndがonPauseの前に呼び出されることを保証します。onPauseでは情報が保存されます。
MobclickAgent.onPageEnd(this.getClass().getSimpleName());
MobclickAgent.onPause(this);
}
@Override
protected void onResume() {
super.onResume();
// ページを統計(ActivityのみのアプリではSDKが自動的に呼び出すため、個別に記述する必要はありません。)
MobclickAgent.onPageStart(this.getClass().getSimpleName());
// 統計の時間
MobclickAgent.onResume(this);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
}
return super.onOptionsItemSelected(item);
}
}
BaseFragment#
public abstract class BaseFragment<P extends BasePresenter> extends Fragment
implements BaseView<P> {
@BindView(R.id.toolbar)
Toolbar mToolbar;
private Unbinder mUnbinder;
protected P mPresenter;
protected ProgressDialog mProgressDialog;
private ActionBar mActionbar;
@Nullable
@Override
public final View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mPresenter = createPresenter();
if (mPresenter != null) {
mPresenter.onStart();
}
View root = inflater.inflate(bindLayout(), container, false);
mUnbinder = ButterKnife.bind(this, root);
prepareData(savedInstanceState);
initToolbar();
// ビューの初期化
initView(root);
initData(savedInstanceState);
initEvent();
return root;
}
protected abstract P createPresenter();
@Override
public void setPresenter(P presenter) {
mPresenter = presenter;
}
/**
* データを準備する
*
* @param savedInstanceState
*/
protected abstract void prepareData(Bundle savedInstanceState);
/**
* フラグメントのレイアウトファイルをバインドする
*
* @return
*/
protected abstract int bindLayout();
/**
* データを初期化する
*
* @param savedInstanceState
*/
protected abstract void initData(Bundle savedInstanceState);
/**
* インターフェースを初期化する
*
* @param rootView
*/
protected abstract void initView(View rootView);
/**
* イベントリスナーを初期化する
*/
protected abstract void initEvent();
/**
* Toolbarを初期化する
*/
private void initToolbar() {
((AppCompatActivity) getActivity()).setSupportActionBar(mToolbar);
mActionbar = ((AppCompatActivity) getActivity()).getSupportActionBar();
}
/**
* Toolbarのタイトルを設定する
*
* @param title タイトル
*/
protected void setToolbarTitle(String title) {
if (mActionbar != null) {
mActionbar.setTitle(title);
}
}
/**
* Toolbarに戻るボタンとタイトルを表示する
*
* @param title タイトル
*/
protected void setToolbarBackEnable(String title) {
if (mActionbar != null) {
mActionbar.setDisplayHomeAsUpEnabled(true);
mActionbar.setHomeAsUpIndicator(R.drawable.ic_arrow_white_24dp);
}
}
@Override
public void showToast(CharSequence msg) {
ToastUtils.shortToast(APP.getAppContext(), msg);
}
@Override
public void showToast(int msgId) {
ToastUtils.shortToast(APP.getAppContext(), msgId);
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public void showLoadingDialog(CharSequence msg) {
if (mProgressDialog == null) {
mProgressDialog = new ProgressDialog(getContext());
mProgressDialog.setMessage(msg);
} else {
mProgressDialog.setTitle(R.string.title_dialog_tips);
}
mProgressDialog.show();
}
@Override
public void hideLoadingDialog() {
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
}
}
@Override
public void onDetach() {
mUnbinder.unbind();
super.onDetach();
}
@Override
public void onDestroyView() {
if (mPresenter != null) {
mPresenter.onDestroy();
}
super.onDestroyView();
}
@Override
public void onResume() {
super.onResume();
MobclickAgent.onPageStart(this.getClass().getSimpleName());
}
@Override
public void onPause() {
super.onPause();
MobclickAgent.onPageEnd(this.getClass().getSimpleName());
}
}
BasePresenter#
public interface BasePresenter {
void onStart();
void onDestroy();
}
BaseView#
public interface BaseView<P> {
void setPresenter(P presenter);
void showToast(CharSequence msg);
void showToast(int msgId);
void showLoadingDialog(CharSequence msg);
void hideLoadingDialog();
}
Impl クラス#
public abstract class BasePresenterImpl implements BasePresenter {
protected CompositeDisposable mSubscriptions;
@Override
public void onStart() {
if (mSubscriptions == null) {
mSubscriptions = new CompositeDisposable();
}
}
@Override
public void onDestroy() {
if (mSubscriptions != null) {
mSubscriptions.dispose();
mSubscriptions.clear();
}
}
}
APP クラス#
グローバルなコンテキストを取得するためのクラス
public class APP extends Application {
private static Context appContext;
private static long exitTime = 0;
/**
* ApplicationのContextを取得する
*
* @return グローバルContext
*/
public static Context getAppContext() {
return appContext;
}
@Override
public void onCreate() {
super.onCreate();
appContext = getApplicationContext();
}
/**
* アプリを終了する
*/
public static void exitApp() {
if (System.currentTimeMillis() - exitTime > 2000) {
ToastUtils.shortToast(getAppContext(), appContext.getString(R.string.text_press_again));
exitTime = System.currentTimeMillis();
} else {
android.os.Process.killProcess(android.os.Process.myPid());
}
}
}
具体的な使用法#
Contract 契約クラス#
契約クラスは、同じ画面の view と presenter のインターフェースを定義するために使用され、メソッドの命名やコメントを規定することで、ページ全体のロジックを明確に見ることができます。
public interface myContract {
interface View extends BaseView<Presenter> {
}
interface Presenter extends BasePresenter {
}
}
Presenter#
public class SamplePresenter extends BasePresenterImpl implements myContract.Presenter {
private final myContract.View mView;
public SamplePresenter(myContract.View view) {
mView = view;
this.mView.setPresenter(this);
}
}
Activity#
public class SampleActivity extends BaseActivity<myContract.Presenter> implements myContract.View {
@BindView(R.id.tv_sample_text)
TextView mTvSample;
@Override
protected myContract.Presenter createPresenter() {
return new SamplePresenter(this);
}
@Override
protected int bindLayout() {
//TODO:ビューを追加、androidmanifestに追加することを忘れないでください
return R.layout.activity_sample;
}
@Override
protected void prepareData() {
//TODO:データを準備する 例えば:データベースからデータを読み込む、またはネットワークリクエストデータなど
}
@Override
protected void initView() {
//TODO:ビューを初期化する 例えば:recycleviewの準備、adapterの追加など
mTvSample.setText("これはサンプルです");
}
@Override
protected void initData(Bundle savedInstanceState) {
//TODO:データを初期化する 例えば:データをビューに追加する
}
@Override
protected void initEvent() {
//TODO:イベントリスナーを初期化する 例えば:リスナーを追加、プルダウンリフレッシュ、もっと読み込むなど
}
}