1
1

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

Last updated at Posted at 2024-02-19

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の参考

1
1
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
1
1