Android kotlin Jetpack Compose retrofit2 paging3 ksp Android Hilt を利用したアプリ作成

# 1.初めに
:::note info
* Jetpack Composeでpaging3を実装した内容を備忘録として残しています。
* Jetpack ComposeではViewModelを推奨していませんが、サンプルコードで使用している為、そのまま使用しています。 
* 作ったアプリはレイアウト崩れが発生します。
:::
<BR>
サンプルコードは、こちらのサイトを参考にしました。

https://nameisjayant.medium.com/infinite-scrolling-with-paging-3-in-jetpack-compose-48c6593b4d35



# 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に置き換える際、エラーが出ましたが、エラー内容を見てもよく分からなかったので、一旦、新規でプロジェクトを作成してから、ライブラリーを最新のバージョンにしたアプリを作成しました。そのプロジェクトに今回のサンプルソースをコピーしながら実装しています。

<br>
以上となります。
間違っているなど、ご意見ご要望があれば、お気軽にコメントください。

<br>
<br>
<br>
Hiltの参考

https://qiita.com/android_aim_for_the_top/items/6feeb13e411b79405b39