大規模Androidアプリにおけるデータ管理

この記事はeureka Advent Calendar 2018 7日目の記事です。


はじめに

こんにちは!エウレカでAndroidエンジニアをやっている@yuyakaidoです。

エウレカでは日夜アプリ改善のためにプラットフォームを跨いで情報共有を行っています。直近では、クライアントアプリにおける効率的なデータの一貫性管理について議論をしており、実際にプロダクトでも検証し始めています。その導入記事は2日目に@muukiiが書いてくれています。

本記事では、muukiiが紹介しているデータの一貫性管理をAndroidで実装するとどうなるかを書いてみたいと思います。


大規模アプリとは

まずは大規模アプリとはどのようなものを指すのかを定義しておきます。

本記事における大規模アプリとは、複数の画面に同一のデータが表示される可能性があり、それらのデータの一貫性を担保する必要があるアプリと定義したいと思います。まさに私が開発しているPairsもこのような状況が多く、データの一貫性管理は悩ましい問題の1つです。

Android界隈では「いいねボタン問題」とも呼ばれていますね。


前回のおさらい

muukiiの記事ではこの問題に対して、データを正規化して管理するアプローチを紹介しています。

例えば、以下のような1人のAuthorが複数のBookを持つようなデータ構造を例に挙げています。

上記のデータ構造を素直に実装すると、以下のようになります。

data class Author(
val id: Long,
val name: String
)
data class Book(
val id: Long,
val title: String,
val author: Author
)

このデータ構造でAuthorを更新することを考えると、該当のAuthorを持つBook全てを更新する必要があります。

このアプローチでもデータの一貫性を担保することはもちろん可能ですが、これがアプリ内の色々な画面で必要になることを想像してみてください。

結構大変そうですよね…?

そこで、リレーショナルデータベースのようにデータを正規化して管理してみます。

上記のデータ構造で管理する場合、Authorを更新する場合は1つのAuthorを更新するだけで良く、Authorを参照するBookは更新の必要がありません。

正規化されたデータ構造をコードで表現すると、以下のようになります。

data class Author(
val id: Long,
val name: String
)

data class Book(
val id: Long,
val title: String,
val authorId: Long
)

実は、このアプローチは我々が考え出したものではなく、Reduxの公式サイトで言及されている「Normalizing State Shape」を参考にしています。

PairsのAndroid/iOS/WebではアーキテクチャとしてRedux/Fluxを採用しており、これらのアーキテクチャ上でデータを正規化するアプローチの実装方法を紹介します。


実装

実装を紹介する上で、以下のようなユースケースを考えていきます。

Book一覧ページ

  • Bookがリスト形式で一覧表示される
  • Bookの情報に加えて、Author情報も表示される

Author一覧ページ

  • Authorがリスト形式で一覧表示される
  • Author情報を変更することが可能である

データ管理

まずは、データ管理部分の実装から考えていきます。

上記のユースケースを実装することを考えると、さきに紹介した正規化されたデータ構造では実装がやりにくい場合があります。具体的には、Book一覧ページを実装する場合にAuthorの実体が欲しいですが、データ構造上はIDしか存在せず、都度実体を取り出す必要があります。画面数が少なければ都度取り出してもそれほど問題にはなりませんが、規模が大きくなってくると同じようなコードが色々な画面に散らばってしまいます。

そこで、バックエンド用のデータとフロントエンド用のデータを分離してみるとどうでしょうか。

Entity

class Entity {
data class Author(
val id: Long,
val name: String
)
data class Book(
val id: Long,
val title: String,
val author: Author
)
}

バックエンド用のデータ

class StoreState {
data class Author(
val id: Long,
val name: String
)
data class Book(
val id: Long,
val title: String,
val authorId: Long // 正規化のためにIDを参照する
)
data class BookList(
val books: List<Long> // 実体は別管理なため、IDの配列のみを保持する
)
data class AuthorList(
val authors: List<Long> // 実体は別管理なため、IDの配列のみを保持する
)
}

フロントエンド用のデータ

class ViewState {
data class BookList( // Book一覧ではこのデータ構造を参照する
val books: List<Book> // 利便性のために実体を参照する
)
data class AuthorList( // Author一覧ではこのデータ構造を参照する
val authors: List<Author> // 利便性のために実体を参照する
)
}

このように要件に応じてデータ構造を変えることで、各レイヤーの都合に合わせたデータ管理を行うことが可能になります。

アプリ全体のデータは以下のように管理し、StoreStateが変更される度にtoBookListメソッドやtoAuthorListメソッドでViewStateを再計算してViewに渡します。View側では渡されたデータをレンダリングすることだけに集中することができます。

data class AppState(
val domain: Domain,
val presentation: Presentation
) {
data class Domain(
val books: Map<Long, Entity.Book>,
val authors: Map<Long, Entity.Author>
)
data class Presentation(
val book: StoreState.BookList,
val author: StoreState.AuthorList
)
fun toBookList(): ViewState.BookList {
return ViewState.BookList(
books = presentation.book.ids
.map { id -> domain.books.getValue(id) } // 実体に変換
)
}
fun toAuthorList(): ViewState.AuthorList {
return ViewState.AuthorList(
authors = presentation.author.ids
.map { id -> domain.authors.getValue(id) } // 実体に変換
)
}
}

データ変換過程をアニメーションで表現すると以下のようになります。

データ更新

次に、データを更新する場合を考えていきます。

データを更新する場合は更新対象のデータを受け取り、Domainが保持する実体データを更新するだけです。あとはその変更によってViewStateが再計算され、それを購読している画面に変更が通知されます。

data class Domain(
val books: MutableMap<Long, Entity.Book>,
val authors: MutableMap<Long, Entity.Author>
) {
fun update(author: Entity.Author) {
authors[author.id] = author
}
}

※ StoreStateが変わった際にViewStateを再計算する機構はRedux/Fluxで実現しますが、今回は本筋と逸れるので省略しています。

さいごに

今回はフロントエンドにおける効率的なデータの一貫性管理方法を紹介しました。

UI構造をそのまま反映した構造はデータの重複が発生するため、データを更新する際は全てのデータを更新する必要がありますが、データを正規化して管理することで効率的にデータの一貫性を担保することができます。

一方で、データを正規化するとフロントエンドとしては実装が複雑になってしまうことから、バックエンドの正規化されたデータ構造と、フロントエンドのネストされたデータ構造を分離するアプローチも紹介しました。

実は、本記事で紹介した内容はDroidKaigi 2019にて発表予定の「Redux for Android」でも取り上げる予定です。当日発表では、今回紹介したアプローチを実装したサンプルアプリをもとにより詳細な解説を予定していますので、ご興味ある方は是非お越しください!