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

Yuya Kaido
Eureka Engineering

--

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

はじめに

こんにちは!Androidエンジニアのyuyakaidoです。

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

本記事では、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に近いアーキテクチャを採用しており、これらのアーキテクチャ上でデータを正規化するアプローチの実装方法を紹介します。

実装

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

Book一覧ページ

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

Author一覧ページ

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

データ管理

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

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

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

エンティティ

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> // 利便性のために実体を参照する
)
}

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

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

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の計算とViewへの反映が行われます。

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

さいごに

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

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

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

本記事で紹介した内容はDroidKaigi 2019にて発表予定の「Redux for Android」でも取り上げます。DroidKaigiの発表では、本記事で紹介したアプローチのより詳細な解説も予定していますので、ご興味ある方はぜひお越しください!

--

--