レイヤードアーキテクチャでRealmを使う


はじめに

昨今のAndroid開発ではActivityやFragmentのマッチョ化を防止するために、CleanArchitectureやMVP、MVVMなどのレイヤードアーキテクチャを採用している人が多いと思います。

Realmは素晴らしいツールで大好きなのですが、ドキュメントのベストプラクティスに従おうとするとガッツリと画面のライフサイクルに依存しているため、設計に与える影響は大きいなぁと感じていました。

これからActivity/FragmentがRealmオブジェクトを保持することなく使うための一例を紹介したいと思います。
全ての人にオススメできるTipsではないかもしれませんが、参考にしていただけたら幸いです。

3行まとめ

  1. カスタムApplicationクラスでUIスレッド用のRealmオブジェクトを保持する
  2. DB層のDaoパターンなどで①のRealmオブジェクトを渡す(Dagger2など)
  3. ①のRealmオブジェクトはcloseしない

詳細

断片的なコードで申し訳ないですが、カスタムApplicationクラスのRealmオブジェクトを、Daoオブジェクトで使用する流れです。

MyApplication
public class MyApplication extends Application {
    private Realm realm;
    public Realm getRealm() {
        return realm;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Realm.init(this);
        Realm.setDefaultConfiguration(new RealmConfiguration.Builder().name("sample.realm")).build());
        /*onCreate時に生成*/
        realm = Realm.getDefaultInstance();
    }
}
AppModule
@Module
public class AppModule {
    private Application app;

    public AppModule(Application app) {
        this.app = app;
    }

    @Provides
    public Application provideApplication() {
        return app;
    }

    @Provides
    public Context provideContext() {
        return app.getApplicationContext();
    }
}
InfraModule
@Module
public class InfraModule {
    @Singleton
    @Provides
    public Realm provideRealm(Application app) {
        return ((MyApplication) app).getRealm();
    }
}
SampleDao
public class SampleDao {
    private final Realm realm;

    @Inject
    public SampleDao(Realm realm) {
        this.realm = realm;
    }

    // do something
}

これで完全にActivity/FragmentがRealmに関心を持たなくていいようになりました。

おまけ

Dagger2を導入していない場合は単純にカスタムアプリケーションでstaticを使ってもいいと思います。

MyApplication
public class MyApplication extends Application {
    private static Realm realm;
    public static Realm getRealm() {
        return realm;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Realm.init(this);
        Realm.setDefaultConfiguration(new RealmConfiguration.Builder().name("sample.realm")).build());
        /*onCreate時に生成*/
        realm = Realm.getDefaultInstance();
    }
}
SampleDao
public class SampleDao {
    private final Realm realm;

    /*UI thread*/
    public SampleDao() {
        this.realm = MyApplication.getRealm();
    }

    /*worker thread*/
    public SampleDao(Realm realm) {
        this.realm = realm;
    }

    // do something
}

Applicationクラスで保持したRealmオブジェクトはcloseしなくていいのか

現在業務で公開している4つのアプリでは有難いことにRealmに関するエラーはほとんどありません。(nativeのエラーが4件ほど)

ただそれだけでは非常に不安だったので、RealmのSlackチャンネルで質問してみました。

結論としては、問題ないとのことでした。

注意が必要なのは以下の2点のケース

1. Realm.compactRealm()などいくつかの特殊なメソッドは全Realmインスタンスがcloseされているときしか使えない

2. Instant Runとの相性が良くない

Realmはトランザクションがコミットされた段階ですべてのデータをファイルに書き込んでいるため、アプリ終了時にcloseがなくても、データを失うことはないそうです。

close漏れが問題になるのには、Realm.compactRealm()などいくつかの特殊なメソッドが使えないことと、Looperのないスレッドでは古いデータを見続けてしまうということでした。

@zaki50さんの説明が大変わかりやすかったので転記させていただきます。

まずRealmの基本として、データはトランザクションのcommitを単位としてバージョン管理されています。
どのバージョンのデータが見えるかはスレッドごとに管理されています。

Looperのあるスレッドの場合、そのスレッド用のRealmインスタンスがopenされている状態でも、
Looperのイベント処理の過程で自動的に最新バージョンのデータが見えるように更新されます。

Looperがないスレッドの場合、RealmクラスのwaitForChange()が呼ばれるまでRealmインスタンスが
オープンされた時点での最新バージョンを見続けます。

なので、Looperがないスレッドであっても適切にclose()していれば次にRealm.getInstance()したさいに
新たにRealmインスタンスが作られるのでその時点での最新バージョンが見えます。

close()していないと、Realmインスタンスはキャッシュされたままなので、getInstance()したときに古いバージョン
のデータが見える状態のRealmインスタンスが返ってくることになります。

古いバージョンのデータが見えるということ自体がアプリにとって問題になることがほとんどですし、古いバージョン
をみているRealmインスタンスがあると内部の処理としてその古いバージョンから最新バージョンまでのすべての
変更差分をデータベース内に保持する必要がでてきます。

このことがデータベースファイルの肥大化の原因になって、OOMなどによるクラッシュを発生させます。

通常は古いバージョンのデータを見ているRealmインスタンスはそれほど長い時間をまたずに最新バージョンのデータ
を見るようになるので、古いバージョンのために使われていた領域は新たなバージョンのデータによって再利用されます。

雑感

Realmオブジェクトを無規則に散りばめない方がいい

Realm.getDefaultInstance()を呼び出す箇所を、カッチリ決めた方が良いと思いました。色々な層に無規則に散りばめられると管理が複雑になりclose漏れや、IllegalStateExceptionを生む温床になります。
UIスレッド用ではカスタムApplicationで保持ベストプラクティスで管理し、IntentSerivceやLooperがないThreadやRxJavaなどのワーカースレッドではtry-finallyで確実にcloseすると楽だと思います。
RealmUtil.dumpRealmCount()で漏れ数チェックRealm Instance = total: xxx instance(s)

realmfieldnameshelperを使う

カラム名を手入力で書いていている方はこちらのライブラリがオススメです。

Fine-grained Collection Notification最高

サンプルもなく雑ですが、以上です