自分の過去のツイートをカレンダーでふりかえることができる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いただけるととても嬉しいです!