はじめに
Android Developersで紹介されている階層型アーキテクチャの中にはデータレイヤという層が存在するので、簡単にまとめてみる。
データレイヤの構造
データレイヤにはアプリに必要なデータとビジネスロジックが含まれる。UI関連の状態やUIロジックに関してはUIレイヤの責任範囲になる。
また、データレイヤはRepositoriesとDataSourcesによって構成される。
Repositories
命名規則は
データの種類 + Repository
例:NewsRepositoryなど
リポジトリの持つ役割
- アプリの他の部分にデータを公開する。
- データの変更を一元管理する。
- 複数のデータソース間の競合を解決する。
- アプリの他の部分からデータソースを抽象化する。
- ビジネス ロジックを格納する。
リポジトリはコンストラクタでデータソースを受け取る
class ContentRepository(
private val contentRemoteDataSource: ContentRemoteDataSource
) { /* ... */ }
データソースはDI時に生成してRepositryへ渡す。
@Module
@InstallIn(SingletonComponent::class)
object ContentModule {
@Provides
@Singleton
fun provideContentRepository(@ApplicationContext context: Context): ContentRepository {
return ContentRepository(
createContentRemoteDatasource(context)
)
}
}
DIを使うことで例えばViewModelの単体テストをやる場合データソースをテスト用に差し替えてテストするようなことがやりやすくなる。
DataSources
命名規則は
データの種類 + ソースの種類 + DataSource
例:NewsRemoteDataSourcesなど
ソースの種類に関してはRemoteやLocal、NetworkやDiskがおすすめ。SharedPreferenceなど実装の詳細に基づく名前は付けない。
データソースとしてよく使いそうなもの
- ネットワーク(例:Kotr, Retrofit)
- ローカルデータベース(例:Room)
- ローカル設定値(例:SharedPreference, DataStore)
- バックグラウンドタスク(例:WrokMangaer)
これらを○○DataSourceクラスから呼び出して使う。
Repositoryから直接呼び出さないのは、そうすることで差し替えが容易になりテストしやすい。SharedPreferenceからDataStoreに移行するようなことも楽になる。
キャッシュ
フェッチしたデータをRepositoryでキャッシュする方法も紹介されていた。
あとはOKHttpでキャッシュする方法もある
// 参考:https://developer.android.com/topic/architecture/data-layer?hl=ja
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource
) {
// Mutex to make writes to cached values thread-safe.
private val latestNewsMutex = Mutex()
// Cache of the latest news got from the network.
private var latestNews: List<ArticleHeadline> = emptyList()
suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
if (refresh || latestNews.isEmpty()) {
val networkResult = newsRemoteDataSource.fetchLatestNews(
// Thread-safe write to latestNews
latestNewsMutex.withLock {
this.latestNews = networkResult
}
}
return latestNewsMutex.withLock { this.latestNews }
}
}
データの分離
Apiで取得したデータから必要なデータだけを取り出すことでアプリのメモリを節約できる。
また、アプリ側で使いやすいデータ型への変換などもデータレイヤで行う。
data class DataApiModel(
val id: Long,
val name: String,
val created: Date,
val lastUpdated: Date,
val authorId: Int
)
data class DataModel(
val id: Long,
val name: String,
val created: Date
)
WorkerMangaer
WorkMangaerはDataSourceクラスでラップすることで、データソースとして扱う。
// 参考:https://developer.android.com/topic/architecture/data-layer?hl=ja
private const val REFRESH_RATE_HOURS = 4L
private const val FETCH_LATEST_NEWS_TASK = "FetchLatestNewsTask"
private const val TAG_FETCH_LATEST_NEWS = "FetchLatestNewsTaskTag"
class NewsTasksDataSource(
private val workManager: WorkManager
) {
fun fetchNewsPeriodically() {
val fetchNewsRequest = PeriodicWorkRequestBuilder<RefreshLatestNewsWorker>(
REFRESH_RATE_HOURS, TimeUnit.HOURS
).setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.TEMPORARILY_UNMETERED)
.setRequiresCharging(true)
.build()
)
.addTag(TAG_FETCH_LATEST_NEWS)
workManager.enqueueUniquePeriodicWork(
FETCH_LATEST_NEWS_TASK,
ExistingPeriodicWorkPolicy.KEEP,
fetchNewsRequest.build()
)
}
fun cancelFetchingNewsPeriodically() {
workManager.cancelAllWorkByTag(TAG_FETCH_LATEST_NEWS)
}
}