1.初めに
- Jetpack Composeでpaging3を実装した内容を備忘録として残しています。
- Jetpack ComposeではViewModelを推奨していませんが、サンプルコードで使用している為、そのまま使用しています。
- 作ったアプリはレイアウト崩れが発生します。
サンプルコードは、こちらのサイトを参考にしました。
2.プロジェクト構成
拡張子が付いてるもの以外はディレクトリを指してます。
app
│
├── application
│ └── BaseApplication.kt
│
├── api
│ ├── di
│ │ └── NetworkModule.kt
│ ├── model
│ │ └── DogsModel.kt
│ ├── network
│ │ └── ApiService.kt
│ └── repository
│ ├── DogsPagingSource.kt
│ └── DogsRepository.kt
│
└── ui
├── MainActivity.kt
├── navigation
│ ├── InventoryApp.kt
│ └── InventoryNavHost.kt
├── screen
│ └── DogsScreen.kt
├── theme
│ ├── Color.kt
│ ├── Theme.kt
│ └── Type.kt
└── viewmodels
└── DogsViewModel.kt
3.設定ファイル
修正前の「build.gradle」は、リンク先のプロジェクトを参照して頂ければと思います。
2024/02/19時点
ルート直下の「build.gradle」(Project:プロジェクト名)
build.gradle.kts
id("com.android.application") version "8.1.2" apply false
//TODO https://androidx.dev/storage/compose-compiler/repository
// Compatibility table 「Kotlin Version」の項目のバージョンを設定する
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
id("com.google.dagger.hilt.android") version "2.49" apply false
id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false
appファイル直下の「build.gradle」(Module : app)
build.gradle.kts
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id ("com.google.devtools.ksp")
id ("com.google.dagger.hilt.android")
id ("kotlin-parcelize")
}
android {
namespace = "com.jp"
compileSdk = 34
defaultConfig {
applicationId = "com.jp"
minSdk = 21
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
//TODO https://androidx.dev/storage/compose-compiler/repository
// Compatibility table 「Compose Compiler Version」の項目のバージョンを設定する
kotlinCompilerExtensionVersion = "1.5.8"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2")
implementation(platform("androidx.compose:compose-bom:2023.03.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//TODO add
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// FRAGMENT Hilt viewModels()
implementation ("androidx.fragment:fragment-ktx:1.6.2")
// Hilt
val hiltVersion = "2.49"
implementation ("com.google.dagger:hilt-android:$hiltVersion")
//TODO 通常のJavaまたはKotlinプロジェクトでHiltコード生成を行うためのコンパイラ依存ライブラリ
// ksp ("com.google.dagger:hilt-compiler:$hiltVersion")
//TODO Android向けのHiltコード生成を行うためのコンパイラ依存ライブラリ
ksp ("com.google.dagger:hilt-android-compiler:$hiltVersion")
// implementation("androidx.hilt:hilt-navigation-fragment:1.1.0")
implementation ("androidx.hilt:hilt-navigation-compose:1.1.0")
implementation ("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
implementation ("androidx.navigation:navigation-compose:2.7.7")
// ROOM
val roomVersion = "2.6.1"
implementation ("androidx.room:room-ktx:$roomVersion")
implementation ("androidx.room:room-runtime:$roomVersion")
ksp ("androidx.room:room-compiler:$roomVersion")
//Coroutines コルーチン
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// RETROFIT
val retrofitVersion ="2.9.0"
implementation ("com.squareup.retrofit2:retrofit:$retrofitVersion")
implementation ("com.squareup.retrofit2:converter-gson:$retrofitVersion")
implementation ("com.squareup.okhttp3:logging-interceptor:4.12.0")
implementation ("com.squareup.retrofit2:converter-scalars:$retrofitVersion")
implementation ("com.squareup.retrofit2:converter-moshi:$retrofitVersion")
// MOSHI
implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
// PAGING
val pagingVersion: String = "3.2.1"
implementation ("androidx.paging:paging-runtime:$pagingVersion")
implementation ("androidx.paging:paging-compose:$pagingVersion")
// CARDVIEW
implementation("androidx.cardview:cardview:1.0.0")
//BottomNavigation コンポーネントと BottomNavigationItem コンポーネントを使用する
implementation ("androidx.compose.material:material:1.6.1")
//coil
implementation("io.coil-kt:coil-compose:2.4.0")
}
4.実装
Application
BaseApplication.kt
@HiltAndroidApp
class BaseApplication() : Application() {
}
navigation
InventoryApp.kt
@Composable
fun InventoryApp(
navController: NavHostController = rememberNavController()
) {
InventoryNavHost(
navController = navController
)
}
InventoryNavHost.kt
@Composable
fun InventoryNavHost(
navController: NavHostController,
modifier: Modifier = Modifier,
//TODO 初回起動はログイン画面
startDestination: String = "DogsScreen",
) {
//アプリ画面遷移処理
NavHost(
navController = navController,
startDestination = startDestination,
modifier = modifier,
) {
composable(
route = "DogsScreen",
) {
DogsScreen()
}
}
}
モデル
DogsModel.kt
data class DogsModel(
val id: String,
val url: String
)
ネットワーク、API
ApiService.kt
interface ApiService {
companion object {
const val BASE_URL = "https://api.thedogapi.com"
}
@GET("v1/images/search")
suspend fun getAllDogs(
@Query("page") page: Int,
@Query("limit") limit: Int
): List<DogsModel>
}
DI
NetworkModule.kt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideMoshi(): Moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
@Provides
@Singleton
fun providesApiService(moshi: Moshi): ApiService = Retrofit
.Builder()
.run {
baseUrl(ApiService.BASE_URL)
addConverterFactory(MoshiConverterFactory.create(moshi))
build()
}.create(ApiService::class.java)
}
repository
DogsRepository.kt
class DogsRepository @Inject constructor(
private val apiService: ApiService
) {
suspend fun getDogs(
page: Int,
limit: Int
): List<DogsModel> {
return apiService.getAllDogs(page, limit)
}
}
DogsPagingSource.kt
class DogsPagingSource @Inject constructor(
private val repository: DogsRepository
) : PagingSource<Int, DogsModel>() {
override fun getRefreshKey(state: PagingState<Int, DogsModel>): Int? {
return state.anchorPosition
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DogsModel> {
Log.d("DogsPagingSource", "### load() start ###")
val page = params.key ?: 1
val response = repository.getDogs(page, params.loadSize)
return try {
LoadResult.Page(
data = response,
prevKey = if (page == 1) null else page.minus(1),
nextKey = if (response.isEmpty()) null else page.plus(1)
)
} catch (e: IOException) {
LoadResult.Error(
e
)
} catch (e: HttpException) {
LoadResult.Error(
e
)
}
}
}
画面
DogsScreen.kt
@Composable
fun DogsScreen(
modifier: Modifier = Modifier,
viewModel: DogsViewModel = hiltViewModel()
) {
val response = viewModel.dogResponse.collectAsLazyPagingItems()
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(3),
modifier = modifier.fillMaxSize()
) {
items(response.itemCount) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(response[it]?.url ?: "-")
.crossfade(true)
.build(),
placeholder = painterResource(R.drawable.ic_launcher_foreground),
contentDescription = "",
contentScale = ContentScale.Crop,
modifier = Modifier
.padding(20.dp)
.clip(CircleShape)
)
}
response.apply {
when {
loadState.refresh is LoadState.Loading || loadState.append is LoadState.Loading -> {
item {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
}
}
loadState.refresh is LoadState.Error || loadState.append is LoadState.Error -> {
item {
Text(text = "Error")
}
}
loadState.refresh is LoadState.NotLoading -> {
}
}
}
}
}
MainActivity.kt
@AndroidEntryPoint
@OptIn(ExperimentalMaterial3Api::class)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetpackComposeHilt03Theme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
InventoryApp()
}
}
}
}
}
ViewModel
DogsViewModel.kt
@HiltViewModel
class DogsViewModel @Inject constructor(
private val dogsPagingSource: DogsPagingSource
) : ViewModel() {
private val _dogResponse: MutableStateFlow<PagingData<DogsModel>> =
MutableStateFlow(PagingData.empty())
var dogResponse = _dogResponse.asStateFlow()
private set
init {
viewModelScope.launch {
Pager(
config = PagingConfig(
10, enablePlaceholders = true
)
) {
dogsPagingSource
}.flow.cachedIn(viewModelScope).collect {
_dogResponse.value = it
}
}
}
}
5.最後に
元のサンプルがkaptを利用していたのでkspに置き換える際、エラーが出ましたが、エラー内容を見てもよく分からなかったので、一旦、新規でプロジェクトを作成してから、ライブラリーを最新のバージョンにしたアプリを作成しました。そのプロジェクトに今回のサンプルソースをコピーしながら実装しています。
以上となります。 間違っているなど、ご意見ご要望があれば、お気軽にコメントください。
Hiltの参考