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:プロジェクト名)
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)
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")
}
これは一部、抜粋です。
<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のパラメーターと同じです。
data class Product(
val id: String,
val title: String,
val user: User,
val updated_at: String,
val url: String,
)
API通信のモジュール
今回は、QiitaAPIを使用しています。
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にしています。)
@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とは関係ないです。
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にしています。
interface QiitaRepository {
fun getQiita(query: String): Flow<PagingData<Product>>
}
APIを実行結果を取得するメソッドを実装しているクラスです。
QiitaRepositoryインタフェースを実装しています。
リクエストするAPIは、QiitaApiインタフェースのメソッドを呼んでいます。
@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のクラスです。
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun provideQiitaRepository(qiitaRepository: QiitaRepositoryImpl): QiitaRepository
@Binds
abstract fun provideMovieRepository(movieRepository: MovieRepositoryImpl): MovieRepository
}
ViewModelからAPIモジュールの呼び出しとPaging3のデータ取得です。
@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
}
}
}
}
共通クラスです。
class Constants {
companion object {
const val BASE_URL = "https://qiita.com/api/v2/"
}
}
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の実装に関しては別のページで説明します。
続きのページはこちらです。
以上で説明は終わりになります。
ご意見ご要望があれば、お気軽にお知らせください。