読者です 読者をやめる 読者になる 読者になる

tomoima525's blog

Androidとか技術とかその他気になったことを書いているブログ。世界の秘密はカレーの中にある!サンフランシスコから発信中。

RealmDBでEndlessListView、あとMVPアーキテクチャ

f:id:tomoima525:20150615172653p:plain:w250
自分の過去のツイートをカレンダーでふりかえることができるTwitCalというツイッタークライアントアプリを作っています。このアプリではデータのキャッシュ、ロードにRealmというモバイル向けDBを利用しています。
Realmは高速なので、Listviewで3000件のデータを表示する場合もほとんどディレイはありません。 ただし、格納されているデータが10000件くらいになった場合に、速度の保証はありませんし、Out of Memoryも心配です。そこで、必要な分だけデータをロードし、下までスクロールしたら追加でデータを取得するようなListViewの実装をこころみました。果たしてRealmでこの実装方法が適切なのかどうか不明なので、Qiitaではなくブログに投下します。

ツイートデータ取得の課題点

各ツイートにはidがふられています。このidはツイート順に増加するものですが、世の中の全てのツイートに与えられるものなので、ランダムに増加するidといえます。そのため、データを20件ずつ取得したいと考えたときには、指定したidより大きいidのうち上位20件(ただし20件未満もOK)を取り出す必要があります。

MySQLであれば式(1)の様な構文で取得できます。

Select * from tbl where id < (指定したid) order by id limit 0,20; //式(1)

limit句が使えない場合でも、サブクエリを指定して式(2)のように取得もできると考えられます。

Select * 
from (Select * from tbl where id <(指定したid) order by id)
where rownum <=20;  //式(2)

一方で、Realmにはそのようなメソッドはないです。しかしながら、結果(RealmResults)に対してクエリを実行できるので、式(2)のような処理を自前で作ることは簡単そうです。

実装の検討

検討のために以下のようなデータを用意し、名前を表示するためのリストビューを作りました。

  • idと名前のjsonデータが500件
  • idは0-9999の間でランダムに振られている

実装は以下のように行いました。

1.初回データ取得

final static int RESULT_NUM = 20;
public List<SimpleData> getInitSimpleData() {
RealmResults<SimpleData> partialResults;
mRealm = Realm.getInstance(mContext);
RealmQuery<SimpleData> realmQuery = mRealm.where(SimpleData.class);

//データの取得
RealmResults<SimpleData> realmResults = realmQuery.findAll();
realmResults.sort("id");
int firstId = realmResults.get(0).getId(); //(1)
int lastId = realmResults.get(realmResults.size() > RESULT_NUM ? RESULT_NUM : realmResults.size() - 1).getId(); //(2)
partialResults = realmResults.where().between("id", firstId, lastId).findAll();
partialResults.sort("id");
LocalStorage.putInt(Const.LAST_ID, lastId); //(3)
return partialResults;
}

(1)データインサート後の初回データ取得は、最初のidが不明なので、ソートしたデータの最初のid(firstId)を取得します。
(2)取得したいデータ範囲の最初のfirstIdから20件目のid(lastId)を取得(取得したいデータが20件未満の場合は最後のid)
(3)firstId,lastIdの範囲でデータを所得し、last_idはプリファレンスに保存しておきます。

2.初回以降のデータ取得

public List<SimpleData> getNextData() {
RealmResults<SimpleData> partialResults;
mRealm = Realm.getInstance(mContext);
int lastId = LocalStorage.getInt(Const.LAST_ID); //(4)
if (lastId < 0) return null;
RealmQuery<SimpleData> realmQuery = mRealm.where(SimpleData.class);
RealmResults<SimpleData> realmResults = realmQuery.greaterThan("id", lastId).findAll();//(5)
if (realmResults.size() == 0) {
    Log.d(TAG, "¥¥no more data!");
    return null;
}
realmResults.sort("id");
int nextLastId = realmResults.get(realmResults.size() > RESULT_NUM ? RESULT_NUM : realmResults.size() - 1).getId();//(6)
partialResults = realmResults.where().between("id", lastId, nextLastId).findAll();
partialResults.sort("id");

LocalStorage.putInt(Const.LAST_ID, nextLastId); // (7)

return partialResults;
}

(4)プリファレンスに保存しておいた前回のlastIdを取得します。
(5)lastIdよりも大きいidを取得し、ソートします。式(2)のサブクエリに当たる部分です。
(6)(2)と同様の処理で次のnextLastIdを取得します。
(7)lastIdからnextLastIdのデータを取得。lastIdを次の取得範囲のidとして保存

ソースコード

ソースコードhttp://github.com/tomoima525/RealmEndlessViewになります。
EndlessViewはこちらのソースを参考に、データの最後までに来た場合の処理などを追加しています。
当初はデータをエンドレスにスクロールできるだけのサンプルだったのですが、MVP +ドメインモデルにチャレンジしたところ、結構大げさなサンプルになってしまいました。今回やりたかったことの実装はdata/repository/SimpleDataRepositoryImpl配下にあります。
当初のサンプルも一応OldArchitechure配下にあるので、違いをご覧になると良いかもです。
MVPって何さ、という方は、

kgmyshinさんのkgmyshin/Android-arch , これからの「設計」の話をしよう

konifarさんのAndroidではMVCよりMVPの方がいいかもしれない

をご覧になると、理解が深まると思います。平たく言うとビジネスロジックをActivityやFragmentから分離してpure javaにするためのアーキテクチャです。

Realm部分の実装についてのご意見だけでなく、DI周りで自分も不慣れな部分があるので、気づいた点があればPRいただけるととても嬉しいです!