Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
0
Help us understand the problem. What is going on with this article?
@highcom

[Android]MVVMモデルでRoom+SQLite+LiveDataでRecyclerViewに表示する仕組みを理解する

この記事では、MVVMモデルやRoomDatabase、RecyclerViewをそれぞれ詳しく説明するのではなく、これらを組み合わせた時に、MVVMモデルのアーキテクチャがどのようなクラス設計・実装となり、それがどう振る舞うのかを全体を通しで説明してみたいと思います。では、いきましょう。

MVVMモデルとは

Wikipediaの「Model View ViewModel」記載内容を引用すると

MVVMはソフトウェアをModel・View・ViewModelの3要素に分割する。プレゼンテーションとドメインを分離し(V-VM / M)また宣言的Viewを分離し状態とマッピングを別にもつ(V / VM)ことでソフトウェアの保守性・開発生産性を向上させる。

となっています。MVVMモデルの詳しい内容については、上記リンクを参照してもらえれば良いですが、私の解釈での簡単な説明としては以下です。
Viewはユーザーによる操作を受け付けてそれをModelに伝える。Modelが受けた操作の結果をViewが表示するという関係になっていて、その中間を勤めるのがViewModel。
View→ViewModel→Modelの関係性になっており、Modelの結果をViewに伝えるときにViewModel→Viewへはデータバインディングという仕組みで実現します。
MVVMPattern.png

このMVVMモデルが、Androidアプリを開発する上で、推奨されているアーキテクチャです。
そして、Android Developersのサイトには「アプリ アーキテクチャ ガイド」として書かれています。このページで載っている図が
final-architecture.png
です。

  • Viewがこの図ではActivity/Fragmentに相当します。
  • ViewModelがこの図でもViewModelに相当します。LiveDataクラスの仕組みでデータバインディングをオブザーバーパターンで実現します。
  • Modelがこの図ではRepositoryとModel、Remote Data Sourceに相当します。Repositoryは具体的なデータソースへのインターフェースを隠蔽するものなので、Modelに含めて考えます。

このMVVMモデルがアプリアーキテクチャガイドに沿ってどのように実装されるのかを説明して行きます。

この記事で説明するコード

Android Developersでも紹介されている「Android Room でビューを使用する」Codelabのソースコードで説明して行きたいと思います。私は、このソースコードを非常に参考にさせてもらい「記録が残るToDoリスト」を制作しました。
なので、Codelabのソースコードからこのアプリを制作する過程で理解をした内容で説明させて頂きたいと思います。
では、Model部分→ViewModel部分→View部分の順番で説明して行きます。

Model部分:RoomDatabaseを利用する

Modelに相当する部分では、RoomDatabaseが利用されています。RoomDatabaseの詳細は、Android Developersのサイトの「Room を使用してローカル データベースにデータを保存する」を読んでもらうと分かりますが、注目したいのは以下の図。
room_architecture.png
Room Database、Data Access Object、EntitesがアーキテクチャガイドでいうところのModelとなり、Rest of The AppがActivityとなります。
では、それがCodelabのコードではどの部分のクラスになっているかというと以下の図になります。
RoomDatabaseクラス図.png
MainActivityがWordRoomDatabaseを呼び出して、Data Access ObjectであるWordDaoを利用してEntityに相当するWordクラスを生成します。そのWordクラスのデータを参照して画面に表示する形となります。

WordRoomDatabaseについて

まず、ここで注目すべきポイントは、@Databaseアノテーションを付けるところです。データベースを利用して、Wordクラスをエンティティとして利用することを宣言しています。このエンティティを利用するために、abstractでData Access ObjectであるWordDaoを定義しています。getDatabaseメソッドはシングルトンとなっており、データーベースを生成した、唯一の自分のインスタンスを返却するようにしています。

WordRoomDatabase.java
@Database(entities = {Word.class}, version = 1, exportSchema = false)
abstract class WordRoomDatabase extends RoomDatabase {

    abstract WordDao wordDao();

    // marking the instance as volatile to ensure atomic access to the variable
    private static volatile WordRoomDatabase INSTANCE;
    private static final int NUMBER_OF_THREADS = 4;
    static final ExecutorService databaseWriteExecutor =
            Executors.newFixedThreadPool(NUMBER_OF_THREADS);

    static WordRoomDatabase getDatabase(final Context context) {
        if (INSTANCE == null) {
            synchronized (WordRoomDatabase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
                            WordRoomDatabase.class, "word_database")
                            .addCallback(sRoomDatabaseCallback)
                            .build();
                }
            }
        }
        return INSTANCE;
    }
}

WordDaoについて

次に、Data Access ObjectであるWordDaoクラスの注目すべきポイントは、@DaoアノテーションをつけてData Access Objectである事を宣言しています。そしてgetAlphabetizedWordsメソッドは、@QueryアノテーションでSQLのクエリを定義して、返却して欲しい形式でのReturn値を書きます。
また、データを挿入する場合は@Insertアノテーションで定義します。
実は、このクラスはinterfaceクラスであり実体を持っていません。これはボイラープレートコードを省くための仕組みであり、ビルドを行うと、WordDao_Impl.javaとして実体となるボイラープレートなコードがプロジェクト内に生成されるようになっています。本来であれば、データベースへのクエリの発行や取得したデータをエンティティに詰めるためにはそれなりの量の実装が必要ですが、このようにやりたい事だけを定義すれば良いようになっています。

WordDao.java
@Dao
public interface WordDao {
    @Query("SELECT * FROM word_table ORDER BY word ASC")
    LiveData<List<Word>> getAlphabetizedWords();

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    void insert(Word word);
}

Wordについて

最後に、エンティティであるWordクラスの注目すべきポイントは、@Entityアノテーションを付けて、エンティティである宣言と、データーベースのテーブル名を宣言しています。各メンバ変数には、@PrimaryKey@NonNullなどテーブルの属性になるアノテーションを宣言します。つまり、このクラスはデーターベースから見ればテーブルであり、Daoから見ればエンティティ、つまりオブジェクトの実体として定義したものになります。

Word.java
@Entity(tableName = "word_table")
public class Word {

    @PrimaryKey
    @NonNull
    @ColumnInfo(name = "word")
    private String mWord;

    public Word(@NonNull String word) {
        this.mWord = word;
    }

    @NonNull
    public String getWord() {
        return this.mWord;
    }
}

ところで、これらを利用するMainActivityですが、実際にはMainActivityが直接RoomDatabaseを利用するのではなく、アーキテクチャガイドより、ViewModelが間に入るのでした。では、次にViewとModelを繋ぐViewModelの部分について説明します。

ViewModel部分:LiveDataを利用する

Viewが受けたユーザー操作をModelに伝える際に間にViewModelが間に入るため、View→ViewModel→Modelになるのでした。
で、Modelの変化をデータバインディングという仕組みでViewに対して伝えますが、その際に利用される仕組みがLiveDataです。Android Developersのサイトには「LiveDataの概要」としてまとまっています。
ViewModelのクラスを反映させると以下のような関連図となります。
ViewModelクラス図.png
ここで変更が加わったのが、MainActivity→WordViewModel→WordRepository→WordDatabaseとなっている部分です。

WordRepositoryについて

まず、WordRepositoryですが、コンストラクタでデータベースを取得しています。
insertメソッドでは、Daoのinsertメソッドを呼び出しているのですが、databaseWrideExecutorで、ワーカースレッドでinsert処理を行うような実装となっています。実は、RoomDatabaseへのアクセスはメインスレッドでは呼び出せないような仕組みになっています。これは、Androidでは、メディアへのアクセスのような時間のかかる処理は、UXを損なうためメインスレッドでは行わない事が推奨されているためです。
getAllWordsメソッドではLiveDataでラップされたWordエンティティのリストを取得しています。こちらは、databaseWrideExecutorを使っていなのですが、LiveDataを返却するように定義する事で、ワーカースレッドで処理が実行される仕組みとなっています。

WordRepository.java
class WordRepository {

    private WordDao mWordDao;
    private LiveData<List<Word>> mAllWords;

    WordRepository(Application application) {
        WordRoomDatabase db = WordRoomDatabase.getDatabase(application);
        mWordDao = db.wordDao();
        mAllWords = mWordDao.getAlphabetizedWords();
    }

    LiveData<List<Word>> getAllWords() {
        return mAllWords;
    }

    void insert(Word word) {
        WordRoomDatabase.databaseWriteExecutor.execute(() -> {
            mWordDao.insert(word);
        });
    }
}

WordViewModelについて

次に、WordViewModelですが、AndroidViewModelクラスを継承する事で、Activityのライフサイクルに合わせたデータの管理を行うようになっています。簡単に言えばアプリが生きている間はデータの保証をするよって感じです。
getAllWordsメソッドですが、こいつがLiveDataを返却しています。このLiveDataクラスにはobserveメソッドがあり、observeメソッドに必要な処理を登録しておくと、LiveDataがラップしているListにデータの変化があったら通知されるという仕組みが備わっており、これがデータバインディングをするための仕掛けです。この変化通知もライフサイクルに合わせて生きている間だけ監視するようになります。

WordViewModel.java
public class WordViewModel extends AndroidViewModel {

    private WordRepository mRepository;
    private final LiveData<List<Word>> mAllWords;

    public WordViewModel(Application application) {
        super(application);
        mRepository = new WordRepository(application);
        mAllWords = mRepository.getAllWords();
    }

    LiveData<List<Word>> getAllWords() {
        return mAllWords;
    }

    void insert(Word word) {
        mRepository.insert(word);
    }
}

View部分:RecyclerViewを利用する

MainActivityからWordViewModelを利用して、変化通知によって表示を更新します。
この表示する部分にRecyclerViewを利用して、データの一覧表示を実現します。最終的に全体を通してのクラス図は以下となります。
クラス図_RoomWithView.png
RecyclerViewを利用したデータの一覧表示は、MainActivityがRecyclerViewを利用して、WordListAdapterでWordViewHolderに1セルずつデータを紐付けていく仕組みですがRecyclerViewの使い方についてはここでは省略し、WordViewModelから受けた変化通知をどうやってRecyclerViewに伝えるか、そこの仕組みについて説明します。

MainActivityについて

まず、ここで注目したいのがonCreateメソッドの以下の部分。
WordViewModelのgetAllWordsメソッドで返却されるLiveDataに対して、observeメソッドを呼び出し、変化通知を受けた時の処理を登録しています。
ここで登録している処理がWordListAdapterのsubmitListメソッドで、毎回、状態変化後の結果であるListを渡しています。

MainActivity.java
public class MainActivity extends AppCompatActivity {

    public static final int NEW_WORD_ACTIVITY_REQUEST_CODE = 1;

    private WordViewModel mWordViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        final WordListAdapter adapter = new WordListAdapter(new WordListAdapter.WordDiff());

        ...

        mWordViewModel.getAllWords().observe(this, words -> {
            adapter.submitList(words);
        });
    }
}

WordListAdapterについて

このクラスで注目したいのが、インナークラスのWordDiffクラスです。DiffUtil.ItemCallbackを継承しており、submitListによって変化したデータを受けた後、差分をとる仕組みを提供しています。この処理で差分と判断されたセルだけが、RecyclerViewに通知され、必要なセルだけが更新される仕組みとなっています。
2つのメソッドをオーバーライドする必要がありますが、それぞれの役割は以下です

  • areItemsTheSame

変化前であるoldItemと変化後であるnewItemで同じアイテムかどうかを判定します。この処理ではインスタンスが同じかを判定していますが、IDとしてPrimaryKeyを定義している場合には、PrimaryKeyのID値が同じかどうかで判断します。

  • areContentsTheSame

areItemsTheSameで同じと判断されたoldItemとnewItemの中身が同じかを判断します。この実装例ではWordクラスが持っているのはStringだけなので、getWordしたStringが同じかどうかで判定しています。複数のメンバを持つクラスを比較したい場合には、equalsメソッドをOverrideして全メンバを比較する処理を実装する必要があります。

WordListAdapter.java
public class WordListAdapter extends ListAdapter<Word, WordViewHolder> {

    ...

    static class WordDiff extends DiffUtil.ItemCallback<Word> {

        @Override
        public boolean areItemsTheSame(@NonNull Word oldItem, @NonNull Word newItem) {
            return oldItem == newItem;
        }

        @Override
        public boolean areContentsTheSame(@NonNull Word oldItem, @NonNull Word newItem) {
            return oldItem.getWord().equals(newItem.getWord());
        }
    }
}

まとめ

長くなりましたが、以上で、Model部分、ViewModel部分、View部分について説明は終わりとなります。
MVVMモデルのアーキテクチャによる関心毎の分離をしたクラス設計・実装と、それを支えるRoomDatabaseやLiveDataの仕組みを説明してきました。
ここでの説明では、全体を流れで説明してきたので、説明が詳細でない部分もあるかと思います。ですが、この全体の流れを把握した上でAndroid DevelopersのドキュメントやCodelabのソースコードを読み込んでみるとより一層の理解が深められるのではないかと思い、執筆してみました。
私の認識の不足や誤りなどもあると思いますが、その際には指摘いただけると助かります。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
0
Help us understand the problem. What is going on with this article?