0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2024-02-08

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の実装に関しては別のページで説明します。

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?