Android kotlin MVVM + Room + Retrofit2 + Paging3 + ksp でAndroid Hiltを利用したアプリ作成1

# 1.DIの勉強、備忘録用にアプリを作成しました

# 2.プロジェクト構成
```
拡張子が付いてるもの以外はディレクトリを指してます。

app
├── api
│   ├── data
│   │   ├── ApiResponseData.kt
│   │   ├── DataModel.kt
│   │   ├── Item.kt
│   │   ├── MovieResponse.kt
│   │   ├── Product.kt
│   │   └── User.kt
│   ├── di
│   │   └── NetworkMoshiModule.kt
│   ├── pagingdatasource
│   │   └── QiitaPagingDataSource.kt
│   ├── repository
│   │   ├── MovieRepository.kt
│   │   ├── MovieRepositoryImpl.kt
│   │   ├── QiitaRepository.kt
│   │   ├── QiitaRepositoryImpl.kt
│   │   ├── RemoteDataSource.kt
│   │   └── RepositoryModule.kt
│   └── service
│       ├── MovieApi.kt
│       └── QiitaApi.kt
│
├── application
│   └── MyApplication.kt
│
├── room
│   ├── AppDatabase.kt
│   ├── dao
│   │   ├── QiitaDao.kt
│   │   └── UserDao.kt
│   ├── di
│   │   └── DatabaseModule.kt
│   ├── entity
│   │   ├── Qiita.kt
│   │   └── User.kt
│   └── repository
│       ├── RoomRepositoryModule.kt
│       ├── UserRepository.kt
│       ├── UserSecondRepository.kt
│       └── UserSecondRepositoryImpl.kt
│
├── ui
│   ├── MainActivity.kt
│   ├── activity
│   │   └── QiitaActivity.kt
│   ├── adapter
│   │   └── QiitaPagingAdapter.kt
│   ├── fragment
│   │   └── QiitaFragment.kt
│   └── viewmodel
│       ├── MainViewModel.kt
│       └── QiitaViewModel.kt
│
└── utils
    ├── Constants.kt
    └── Resource.kt
    
```


# 3.設定ファイル
2024/02/08時点
ルート直下の「build.gradle」(Project:プロジェクト名)
```build.gradle
plugins {
    id("com.android.application") version "8.1.2" apply false
    id("org.jetbrains.kotlin.android") version "1.9.0" apply false

    id("com.google.dagger.hilt.android") version "2.49" apply false
    id("com.google.devtools.ksp") version "1.9.0-1.0.12" apply false

}

//TODO Hilt では、ライブラリの依存関係とは別に、プロジェクトに設定されている Gradle プラグインを使用します。
buildscript {
    dependencies {
        classpath("com.google.dagger:hilt-android-gradle-plugin:2.49")
    }
}

```

appファイル直下の「build.gradle」(Module : app)
```build.gradle
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")

    id ("com.google.devtools.ksp")
    id ("com.google.dagger.hilt.android")

}

android {
    namespace = "com.jp"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.jp"
        minSdk = 21
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    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 {
        viewBinding = true
        //noinspection DataBindingWithoutKapt
        dataBinding = true
    }

}

dependencies {

    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.11.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

    // FRAGMENT Hilt viewModels()
    implementation ("androidx.fragment:fragment-ktx:1.6.2")

    // Hilt
    val hiltVersion = "2.49"
    implementation ("com.google.dagger:hilt-android:$hiltVersion")
    ksp ("com.google.dagger:hilt-android-compiler:$hiltVersion")


    // 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 retrofit_version ="2.9.0"
    implementation ("com.squareup.retrofit2:retrofit:$retrofit_version")
    implementation ("com.squareup.retrofit2:converter-gson:$retrofit_version")
    implementation ("com.squareup.okhttp3:logging-interceptor:4.12.0")
    implementation ("com.squareup.retrofit2:converter-scalars:$retrofit_version")
    implementation ("com.squareup.retrofit2:converter-moshi:$retrofit_version")


    // MOSHI
    implementation("com.squareup.moshi:moshi-kotlin:1.15.0")

    // PAGING
    val paging_version: String = "3.2.1"
    implementation("androidx.paging:paging-runtime:$paging_version")


    // CARDVIEW
    implementation("androidx.cardview:cardview:1.0.0")


}
```

```AndroidManifest.xml
これは一部、抜粋です。
    <application
        //ここにapplicationクラスを定義する
        android:name=".application.MyApplication"
        >
        //割愛
    </application>

```


# 4.実装
Android HiltでDIを実装するにあたり以下のサイトを参考にしました。
https://developer.android.com/training/dependency-injection/hilt-cheatsheet?hl=ja


QiitaAPIから取得したJSONデータをパースしたパラメーターを保存するクラスです。
JSONデータの中身は何が入っているかは、以下のURLをブラウザで実行してもらえれば確認出来ます。
https://qiita.com/api/v2/items?page=1&per_page=10&query=kotlin
上記URLで指定しているパラメーターは、後で説明するQiitaApi.ktのパラメーターと同じです。
```Product.kt
data class Product(
    val id: String,
    val title: String,
    val user: User,
    val updated_at: String,
    val url: String,

)

```

API通信のモジュール
今回は、QiitaAPIを使用しています。
```QiitaApi.kt
interface QiitaApi {

    @GET("items")
    suspend fun getQiita(
        @Query("query")
        query: String,

        @Query("page")
        page: Int = 1,

        @Query("per_page")
        perPage: Int = 10
    ): Response<List<Product>>

}

```

ネットワークのモジュール
アノテーションがModuleのクラスです。（このアプリでは、objectにしています。）
```NetworkMoshiModule.kt
@InstallIn(SingletonComponent::class)
@Module
object NetworkMoshiModule {

    @Provides
    @Singleton
    fun provideOkhttpClient(): OkHttpClient {
        val logInterceptor = HttpLoggingInterceptor()
        logInterceptor.level = HttpLoggingInterceptor.Level.BODY

        return OkHttpClient.Builder()
            .addInterceptor {
                val httpUrl = it.request().url
                val requestBuilder = it.request().newBuilder().url(httpUrl)
                it.proceed(requestBuilder.build())
            }
            .addInterceptor(logInterceptor)
            .readTimeout(10, TimeUnit.SECONDS)
            .connectTimeout(10, TimeUnit.SECONDS)
            .build()
    }

    @Provides
    @Singleton
    fun provideMoshi(): MoshiConverterFactory {
        val moshi = Moshi.Builder()
            .add(KotlinJsonAdapterFactory())
            .build()
        return MoshiConverterFactory.create(moshi)
    }

    @Provides
    @Singleton
    fun provideRetrofit(
        okHttpClient: OkHttpClient,
        moshiConverterFactory: MoshiConverterFactory,
    ): Retrofit {
        return Retrofit.Builder()
            .client(okHttpClient)
            .baseUrl(BASE_URL)
            .addConverterFactory(moshiConverterFactory)
            .build()
    }

    /**
     * TODO APIの分だけ実装する。
     * ApiService　生成
     */
    @Singleton
    @Provides
    fun provideQiitaService(retrofit: Retrofit): QiitaApi =
        retrofit.create(QiitaApi::class.java)

    /**
     * TODO APIの分だけ実装する。
     * ApiService　生成
     */
    @Singleton
    @Provides
    fun provideMovieService(retrofit: Retrofit): MovieApi =
        retrofit.create(MovieApi::class.java)

}
```

Paging3のライブラリーを実装したクラスです。
このクラスはDIとは関係ないです。
```QiitaPagingDataSource.kt
class QiitaPagingDataSource(
    private val qiitaApi: QiitaApi,
    private val query: String,

) : PagingSource<Int, Product>() {

    companion object{
        // APIのpage指定の最小値
        private val FIRST_INDEX = 1
        // APIの1チャンクあたりの取得データ数
//        private val PAGE_SIZE = 30
        private val PAGE_SIZE = 10
    }


    /**
     * データを破棄した後、再取得する（再びloadを呼び出す）際に使用するkeyを設定する。
     */
    override fun getRefreshKey(state: PagingState<Int, Product>): Int? {
        Log.d("", "###   QiitaPagingDataSource getRefreshKey   ###")

        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }

    /**
     * データをソースからロードした結果を返す。
     */
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Product> {
        Log.d("", "###   QiitaPagingDataSource load   ###")
        Log.d("", "###   query : $query   ###")
        val page = params.key ?: FIRST_INDEX

        return try {
            val response: Response<List<Product>> = qiitaApi.getQiita(query = query, page = page, perPage = params.loadSize)
            Log.d("", "###   response : $response   ###")
            var data: List<Product> = mutableListOf()
            response.body()?.let {
                data = it
                Log.d("", "###   data size : ${data.size}   ###")
                data.forEach {
                    Log.d("", "###   Product title : ${it.title}   ###")
                }
            }


            LoadResult.Page(
                //取得したアイテムのList
                data = data,
                //現在のページより前のアイテムを取得する場合に用いるキー
                prevKey = if (page == FIRST_INDEX) null else page.minus(1),
                //現在のページより後のアイテムを取得する場合に用いるキー
                nextKey = if (data.isEmpty()) null else page.plus(1)
            )
        } catch (exception: Exception) {
            return LoadResult.Error(exception)
        }

    }
}
```

APIを実行結果を取得するメソッドのインタフェースです。
戻り値をPaging3で必要なPagingDataにしています。
```QiitaRepository.kt
interface QiitaRepository {

    fun getQiita(query: String): Flow<PagingData<Product>>

}
```

APIを実行結果を取得するメソッドを実装しているクラスです。
QiitaRepositoryインタフェースを実装しています。
リクエストするAPIは、QiitaApiインタフェースのメソッドを呼んでいます。
```QiitaRepositoryImpl.kt
@Singleton
class QiitaRepositoryImpl @Inject constructor(
    private val qiitaApi: QiitaApi,

) : QiitaRepository {

    /**
     * ページング処理
     */
    override fun getQiita(query: String): Flow<PagingData<Product>> {
        val flow = Pager(
            config = PagingConfig(

                //TODO これを設定すると無限ループして止まらない？
                // 初期取得数
//                    initialLoadSize = 10,

                //TODO pageSize　QiitaPagingDataSource#load　の　params.loadSize　と同じ値になる。
                // ページサイズを10件に設定。
                pageSize = 5,

                enablePlaceholders = true,
                maxSize = 200),
            pagingSourceFactory = {
                //PagingSource
                QiitaPagingDataSource(qiitaApi = qiitaApi, query = query)
            },

        ).flow

        return flow

    }

}
```

QiitaRepositoryクラスをバインドするクラスになります。
アノテーションがBindsのクラスです。
```RepositoryModule.kt
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

    @Binds
    abstract fun provideQiitaRepository(qiitaRepository: QiitaRepositoryImpl): QiitaRepository


    @Binds
    abstract fun provideMovieRepository(movieRepository: MovieRepositoryImpl): MovieRepository

}
```

ViewModelからAPIモジュールの呼び出しとPaging3のデータ取得です。
```QiitaViewModel.kt
@HiltViewModel
class QiitaViewModel @Inject constructor(
    private val qiitaRepository: QiitaRepository,

) : ViewModel() {

    companion object {
        private val TAG: String = QiitaViewModel::class.simpleName.toString()
    }

    //一般的に、クラスのカプセル化を意識して、MutableLiveDataプロパティを非公開にし、LiveDataプロパティのみを公開します。
    //StateFlow
    private var _myUiStatePagingData = MutableStateFlow<PagingData<Product>>(PagingData.empty())
    val myUiStatePagingData: StateFlow<PagingData<Product>> get() = _myUiStatePagingData


    /**
     * QiitaAPIからデータを取得
     */
    fun getQiitaPagingData(query: String){
        viewModelScope.launch {

            Log.d(TAG, "###   QiitaViewModel getQiitaPagingData   ###")
            qiitaRepository.getQiita(query)
                .cachedIn(viewModelScope)
                .collectLatest {
                it.map {
                    Log.d(TAG, "###   Product title : ${it.title}   ###")
                }
                _myUiStatePagingData.value = it
            }

        }
    }

}
```

共通クラスです。
```Constants.kt
class Constants {

    companion object {
        const val BASE_URL = "https://qiita.com/api/v2/"
    }

}
```

```Resource.kt
sealed class Resource<T>(
    val data: T? = null,
    val message: String? = null,
) {

    /**
     * 処理結果が成功時のイベント
     */
    class Success<T>(data: T) : Resource<T>(data)

    /**
     * 処理結果がエラー時のイベント
     */
    class Error<T>(data: T? = null, message: String, ) : Resource<T>(data, message)

    /**
     * 処理中のイベント
     */
    class Loading<T>(isLoading: Boolean) : Resource<T>()


}
```



Roomモジュール
説明が長くなりそうなので、Roomの実装に関しては別のページで説明します。

続きのページはこちらです。

https://qiita.com/android_aim_for_the_top/items/a0d58c1bb773953eb196


以上で説明は終わりになります。
ご意見ご要望があれば、お気軽にお知らせください。