PagingするたびローカルDBにデータがなかったらWebApiで取得してローカルDBに保存する処理をRemoteMediatorを使って実装してみました
使い方は簡単でPager作成時にRemoteMediatorを指定するだけです
developerの説明は以下
https://developer.android.com/topic/libraries/architecture/paging/v3-network-db?hl=ja
まずは最もシンプルな構成で実装してみます
※ 図のPagingDataAdapterは実装しません
処理の見通しをわかりやすくするためPokeApiのポケモンリストだけを取得する内容としてます
そこそこの図鑑の実装は別途やります
依存関係の設定
WebApiはPokeApiを使用します
以下からPokeApiとHttpClientの.aarをダウンロードして(project)/app/libsに置いてください
https://github.com/bps-e/PokeApi/releases
https://github.com/bps-e/HttpClient/releases
今回はJetPack Compose + Hilt + Room + Paging3の構成でkotlinx-serialization.Jsonを使用するので以下を設定します
またHiltはα版ですがkspで使用します
[versions]
com-google-devtools-ksp = "1.9.0-1.0.13"
androidx-room = "2.6.0-beta01"
androidx-paging = "3.2.0"
dagger-hilt = "2.48"
[libraries]
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.6.0" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "androidx-room" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidx-room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidx-room" }
androidx-room-paging = { group = "androidx.room", name = "room-paging", version.ref = "androidx-room" }
androidx-paging-runtime-ktx = { group = "androidx.paging", name = "paging-runtime-ktx", version.ref = "androidx-paging" }
androidx-paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "androidx-paging" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "dagger-hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "dagger-hilt" }
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version = "1.1.0-alpha01" }
[plugins]
com-google-devtools-ksp = {id = "com.google.devtools.ksp", version.ref = "com-google-devtools-ksp" }
com-google-dagger-hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "dagger-hilt" }
plugins {
kotlin("plugin.serialization") version "1.9.0"
alias(libs.plugins.com.google.devtools.ksp) apply false
alias(libs.plugins.com.google.dagger.hilt.android) apply false
}
plugins {
id("kotlinx-serialization")
alias(libs.plugins.com.google.devtools.ksp)
alias(libs.plugins.com.google.dagger.hilt.android)
}
dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.paging)
implementation(libs.androidx.paging.runtime.ktx)
implementation(libs.androidx.paging.compose)
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
implementation(libs.androidx.hilt.navigation.compose)
ksp(libs.androidx.room.compiler)
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar"))))
}
実装
まずはApplicationクラス作ってHiltを有効にする
/**
* ```AndroidManifest.xml
* <application
* android:name=".Application"
* ```
*/
@HiltAndroidApp
class Application : android.app.Application() {
override fun onCreate() {
super.onCreate()
}
}
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
...
}
Roomまわり
const val TABLE_POKEMON = "pokemon"
@Entity(tableName = TABLE_POKEMON)
data class Pokemon(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val name: String,
)
@Dao
interface PokemonDao {
@Query("SELECT * FROM $TABLE_POKEMON")
fun pagingSource(): PagingSource<Int, Pokemon>
@Query("SELECT COUNT(*) FROM $TABLE_POKEMON")
suspend fun count(): Long
@Query("DELETE FROM $TABLE_POKEMON")
suspend fun deleteAll()
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(data: Pokemon)
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(list: List<Pokemon>)
}
@Database(entities = [Pokemon::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun dao(): PokemonDao
}
class PokemonRepository @Inject constructor(
private val pager: Pager<Int, Pokemon>,
) {
fun getPagingStream() = pager.flow
}
RemoteMediator
MediatorResult.Success(true)を指定すると読み込みを終了します
ここではdbのサイズがPokeApiのcount以上が終了条件です
const val PAGE_SIZE = 20
@OptIn(ExperimentalPagingApi::class)
class PokemonRemoteMediator @Inject constructor(
private val db: AppDatabase,
) : RemoteMediator<Int, Pokemon>() {
override suspend fun initialize(): InitializeAction {
return InitializeAction.SKIP_INITIAL_REFRESH
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Pokemon>,
): MediatorResult {
return try {
val dao = db.dao()
val count = dao.count()
val offset = when (loadType) {
LoadType.REFRESH -> if (count == 0L) 0L else return MediatorResult.Success(true)
LoadType.PREPEND -> return MediatorResult.Success(true)
LoadType.APPEND -> count
}
var poke_count = 0
val result = mutableListOf<Pokemon>()
PokeApi.Pokemon(limit = PAGE_SIZE, offset = offset.toInt(), onError = {
Log.e("App", it.localizedMessage ?: "")
}) { poke ->
poke_count = poke.count
result.addAll(poke.results.map {
Pokemon(name = it.name)
})
}
if (result.isNotEmpty()) {
db.withTransaction {
dao.insert(result)
}
}
return MediatorResult.Success(poke_count < count + PAGE_SIZE)
}
catch (e: Exception) {
MediatorResult.Error(e)
}
}
}
DIまわり
@Module
@InstallIn(ViewModelComponent::class)
object PokemonModule {
@Provides
fun provideDatabase(
@ApplicationContext context: Context
) = Room.databaseBuilder(context, AppDatabase::class.java, "app.db").build()
@Provides
fun provideDao(db: AppDatabase) = db.dao()
@OptIn(ExperimentalPagingApi::class)
@Provides
fun providePager(
db: AppDatabase,
): Pager<Int, Pokemon> {
return Pager(
config = PagingConfig(pageSize = PAGE_SIZE),
remoteMediator = PokemonRemoteMediator(
db = db
),
pagingSourceFactory = {
db.dao().pagingSource()
},
)
}
@Provides
fun provideRepository(pager: Pager<Int, Pokemon>) = PokemonRepository(pager)
}
UIまわり
リストを下にスクロールしていくとRemoteMediatorが実行されてリストが追加されていきます
@HiltViewModel
class PokeViewModel @Inject constructor(
private val repo: PokemonRepository,
) : ViewModel() {
val pokemon = repo.getPagingStream().cachedIn(viewModelScope)
}
@Composable
fun AppScreen(
modifier: Modifier = Modifier,
viewModel: PokeViewModel = hiltViewModel(),
) {
val items = viewModel.pokemon.collectAsLazyPagingItems()
LazyColumn(modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
items(
count = items.itemCount,
key = items.itemKey { it.id },
) { index ->
val poke = items[index]
poke?.apply {
ListItem(headlineContent = {
Text("$index. $name")
})
Divider(color = Color.LightGray)
}
}
}
}
Android Studio Giraffe 2022.3.1 Patch 1 built on August 17, 2023