データ一覧の取得

データ一覧画面では、登録した収支の一覧表示と、収支の編集処理を行うことができます。このページでは、データの一覧の実装方法を説明します。編集処理は次のページで説明します。

クラス構成

データ一覧画面は、以下のクラス図のように、Android SDK の ListFragmentBaseAdapter のサブクラスによって実現します。

BalanceListFragment クラスは Kii Cloud にアクセスし、収支の一覧データを KiiObject のリストとして取得します。取得した KiiObjectKiiObjectAdapter に格納します。

ソースコードは以下のとおりです。

データ一覧の取得

データ一覧画面では、収支のデータが格納された KiiObject を、Kii Cloud から取得します。

Kii Cloud でのデータ取得には、主に以下の 2 通りの方法がありますが、今回は前者の検索機能を使用します。

  • 特定 Bucket 内の検索によって取得する方法

    指定した条件に一致する KiiObject をまとめて取得します。データ一覧画面では、この方法が適しています。

  • KiiObject の ID を指定して取得する方法

    過去に扱った特定の KiiObject の ID を記憶しておき、次回に取得し直す方法です。ID の管理が必要なため、データ一覧画面のケースには適していません。

データの取得処理は、onActivityCreated() メソッドを契機に実行します。このメソッドは、フラグメントの親となっているアクティビティの作成処理が完了したタイミングで呼び出されます。

実際の取得処理は、getItems() メソッドで以下のように実装されています。まずはクエリーの実行を行うまでの処理を示します。

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    ......
    getItems();
}

private void getItems() {
  KiiUser user = KiiUser.getCurrentUser();
  KiiBucket bucket = user.bucket(Constants.BUCKET_NAME);

  // Create a query instance.
  KiiQuery query = new KiiQuery();
  // Sort KiiObjects by the _created field.
  query.sortByAsc(Field._CREATED);

  final List<KiiObject> objectList = new ArrayList<KiiObject>();

  // Call the Kii Cloud API to query KiiObjects.
  ProgressDialogFragment.show(getActivity(), getFragmentManager(), R.string.loading);
  bucket.query(new KiiQueryCallBack<KiiObject>() {
    ......
  }, query);
}

ここでは、Bucket 内の KiiObject の全件を、作成日時昇順の条件でクエリーを実行しています。クエリーの実行方法は、Hello Kii と同じです。詳細は Hello Kii の解説 を参照してください。

ページネーション

今回は、クエリーの実行結果に対して、ページネーションの処理を行って、より完全なデータ一覧処理を実現します。

ページネーションは、検索結果の数が多い場合、それらをページ単位に分割して取得する手法です。

たとえば、420 件ヒットした KiiObject を取得する場合、以下のように複数のページに分割して取得できます。

デフォルトでは、bestEffortLimit が 200 に設定されているため、最大 200 件のページに分けて取得できます。ただし、転送サイズなどの様々な条件により、2 回目の取得例のように、次のページがあるのに 1 回分の取得件数が 200 件に満たない場合もあります。また、最後のページでは、取得件数が 0 件になる場合もあります。

上の図で、各 API が返す値は以下のとおりです。

  • 1 ページ目

    初めのページは、KiiBucket クラスの query() メソッドによって取得できます(:num1:)。得られた resultgetResult() メソッドから、List<KiiObject> として KiiObject のリストを取得できます。まだ次のページがあるため、hasNext()true を返します。

  • 2 ページ目

    次のページは、1 ページ目の result インスタンスに対して getNextResult() を実行することで取得できます(:num2:)。同様に、getResult() メソッドにより、List<KiiObject> を取得できます。さらに次のページがあるため、hasNext()true を返します。

  • 3 ページ目

    次のページは、2 ページ目の result インスタンスに対して getNextResult() を実行することで取得できます(:num3:)。ここで、hasNext()false を返しているため、次のページは存在しません。全件の取得が完了したことを表します。

ページネーションの実装を以下に示します。このコードは、上記のコードのクエリーの実行部分です。

private void getItems() {
  ......

  final List<KiiObject> objectList = new ArrayList<KiiObject>();

  // Call the Kii Cloud API to query KiiObjects.
  ProgressDialogFragment.show(getActivity(), getFragmentManager(), R.string.loading);
  bucket.query(new KiiQueryCallBack<KiiObject>() {
    @Override
    public void onQueryCompleted(int token, KiiQueryResult<KiiObject> result, Exception e) {
      super.onQueryCompleted(token, result, e);
      if (e != null) {
        ProgressDialogFragment.close(getFragmentManager());
        ViewUtil.showToast(getActivity(), ViewUtil.showToast(getActivity(), getString(R.string.error_query) + e.getMessage());
        return;
      }
      objectList.addAll(result.getResult());

      // Check if more KiiObjects exit.
      if (!result.hasNext()) {
        ProgressDialogFragment.close(getFragmentManager());
        KiiObjectAdapter adapter = (KiiObjectAdapter) getListAdapter();
        adapter.clear();
        for (KiiObject object : objectList) {
            adapter.add(object);
        }
        adapter.computeTotalAmount();
        adapter.notifyDataSetChanged();
        refreshTotalAmount();
        mAddButton.setVisibility(View.VISIBLE);
        return;
      }
      // Get the remaining KiiObjects.
      result.getNextQueryResult(this);
    }
  }, query);
}

この処理での着目点は以下のとおりです。

  • ページネーションは、KiiQueryCallBack<KiiObject> クラスの onQueryCompleted() メソッドを再帰呼び出しすることで実装されます。2 ページ目以降を result.getNextQueryResult() メソッドで取得する際、コールバックを this とすることで、1 ページ目と同じ KiiQueryCallBack<KiiObject> インスタンスを指定し、再起呼び出しを実現しています。
  • ProgressDialogFragment クラスにより、進捗表示を行っています。進捗表示を終えるタイミングは、エラーの発生により次のページ以降を処理しないとき、または、全件の処理が完了したときです。
  • エラーからの回復処理のため、取得した KiiObjectKiiObjectAdapter に直接反映しません。取得結果は objectList にいったん保存され、全件の処理が完了したタイミングでまとめて反映されます。もし、KiiObjectAdapterKiiObject を直接更新するようにした実装で 2 ページ目がエラーとなると、エラー発生後は 1 ページ目だけが残り続けます。

3 番目に示したエラーからの回復処理は、実際のモバイルアプリで品質を確保する上で重要なテーマとなります。Kii Cloud の API の実行にはネットワークアクセスが必要なため、スマートフォンの電波状況などの不安定な要素が API のエラーに直結します。

モバイルアプリでは、単純にエラーメッセージを表示するだけでなく、エラー発生後にユーザーがどのように回復するかを考慮しておく必要があります。こうしたシナリオの考慮が不十分だと、データの不整合が発生したり、モバイルアプリの操作手段がなくなったりする不具合につながります。なお、ここでのエラーケースは、query.setLimit() でページサイズを小さく設定し、デバッガのブレークポイントと端末の機内モードを組み合わせるなどしてテストできます。

一覧の取得に成功したときは、次のセクションに示す KiiObjectAdapter に、取得した KiiObject を登録します。同時に、refreshTotalAmount() メソッドで収支の合計を画面表示し、データの追加操作のボタンを有効化します。失敗したときは、メニューから一覧のリフレッシュを行うように案内します。

KiiObjectAdapter

KiiObjectAdapter は、BaseAdapter を継承したクラスで、KiiObject による収支データをまとめて管理します。同時に、収支データの計算済みの合計値をフィールドに保存し、データ一覧画面からの問い合わせに応答します。

KiiObjectAdapter は、以下のような機能を実装しています。いずれも基本的なアダプターの実装方法に基づいて実装されているため、ここでは、詳細を省略します。ソースコードは KiiObjectAdapter を参照してください。

メソッド BaseAdapter の機能 説明
void add(KiiObject item) KiiObject を追加します。
void updateObject(KiiObject object, String objectId) 指定されたオブジェクト ID の KiiObject を更新します。
int delete(String objectId) 指定されたオブジェクト ID の KiiObject を削除します。
void clear() すべての KiiObject を削除します。
void computeTotalAmount() 収支の合計値を計算します。
int getTotalAmount() 収支の合計値を返します。
int getCount() 登録されている KiiObject の件数を返します。
Object getItem(int position) 指定したインデックスの KiiObject を返します。
long getItemId(int position) Kii Balance では常にダミー値 0 を返します。
View getView(int position, View convertView, ViewGroup parent) 画面表示のためのビューを返します。

ビューの作成

getView() メソッドでは、項目表示のためのビューを作成して返します。

Hello Kii の場合と同様、KiiObject インスタンスに格納されているデータを表示形式に加工し、BaseAdapter クラスの仕様に従って 1 項目分のビューを返します。

Kii Balance での実装は以下のとおりです。

@Override
public View getView(int position, View convertView, ViewGroup parent) {
  if (convertView == null) {
    Context context = parent.getContext();
    LayoutInflater inflater = LayoutInflater.from(context);

    View layout = inflater.inflate(R.layout.list_item, null);

    // Set textViews to ViewHolder.
    TextView nameText = (TextView) layout.findViewById(R.id.text_name);
    TextView amountText = (TextView) layout.findViewById(R.id.text_amount);
    TextView dateText = (TextView) layout.findViewById(R.id.text_date);
    layout.setTag(new ViewHolder(nameText, amountText, dateText));

    convertView = layout;
  }
  ViewHolder holder = (ViewHolder) convertView.getTag();
  KiiObject object = items.get(position);

  TextView nameText = holder.nameText;
  nameText.setText(object.getString(Field.NAME));

  TextView amountText = holder.amountText;
  int type = object.getInt(Field.TYPE);
  if (type == Field.Type.INCOME) {
      amountText.setText(AMOUNT_FORMAT.format(object.getInt(Field.AMOUNT) / 100.0));
      amountText.setTextColor(Color.BLACK);
  } else {
      amountText.setText(AMOUNT_FORMAT.format(-object.getInt(Field.AMOUNT) / 100.0));
      amountText.setTextColor(Color.RED);
  }

  TextView dateText = holder.dateText;
  dateText.setText(DATE_FORMAT.format(new Date(object.getCreatedTime())));

  return convertView;
}

private static class ViewHolder {
  TextView nameText;
  TextView amountText;
  TextView dateText;
  ViewHolder(TextView nameText, TextView amountText, TextView dateText) {
    this.nameText = nameText;
    this.amountText = amountText;
    this.dateText = dateText;
  }
}

ここでは、以下のように KiiObject の各フィールドと表示項目を対応付けます。コードでは、ViewHolder クラスを使って、findViewById() の実行結果をビューのタグに設定することで処理を高速化しています。


次は...

データ一覧画面でのデータ項目の編集機能について説明します。

データ項目の編集機能 に移動してください。