From 5c0a31d6485d4677dd76a034a0eccece19ed4d95 Mon Sep 17 00:00:00 2001 From: Adrian Kuta Date: Sat, 10 Aug 2024 19:05:20 +0200 Subject: [PATCH] Refactor: Migrate from Retrofit to Ktor This commit migrates the network layer from Retrofit to Ktor. Specific changes include: - Replaced Retrofit with Ktor for network requests. - Updated dependencies to include Ktor libraries. - Refactored network service and data classes to use Ktor's API. - Removed Retrofit-specific code and dependencies. - Adjusted network module to provide Ktor client and services. - Updated PixabayImageRepository to use the new Ktor-based PixabayService. --- .idea/misc.xml | 1 - .../dev/adriankuta/pixabay/PixabayNavGraph.kt | 1 - data/build.gradle.kts | 17 +++-- .../pixabay/data/di/NetworkModule.kt | 76 ++++++++++--------- .../pixabay/data/dto/request/PixabayImages.kt | 12 +++ .../data/dto/request/PixabayImagesById.kt | 8 ++ .../response/SearchPixabayImagesResponse.kt | 1 + .../data/network/KtorPixabayService.kt | 26 +++++++ .../pixabay/data/network/PixabayService.kt | 15 +--- .../data/paging/PixabayPagingSource.kt | 3 - .../data/paging/PixabayRemoteMediator.kt | 4 - .../data/repository/PixabayImageRepository.kt | 3 +- .../feature/details/PhotoDetailRoute.kt | 1 - gradle/libs.versions.toml | 14 ++-- 14 files changed, 114 insertions(+), 68 deletions(-) create mode 100644 data/src/main/kotlin/dev/adriankuta/pixabay/data/dto/request/PixabayImages.kt create mode 100644 data/src/main/kotlin/dev/adriankuta/pixabay/data/dto/request/PixabayImagesById.kt create mode 100644 data/src/main/kotlin/dev/adriankuta/pixabay/data/network/KtorPixabayService.kt diff --git a/.idea/misc.xml b/.idea/misc.xml index 0ad17cb..8978d23 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/src/main/java/dev/adriankuta/pixabay/PixabayNavGraph.kt b/app/src/main/java/dev/adriankuta/pixabay/PixabayNavGraph.kt index 50af23a..081cda3 100644 --- a/app/src/main/java/dev/adriankuta/pixabay/PixabayNavGraph.kt +++ b/app/src/main/java/dev/adriankuta/pixabay/PixabayNavGraph.kt @@ -43,7 +43,6 @@ fun PixabayNavGraph( ) { entry -> PhotoDetailRoute( photoId = entry.arguments?.getInt(PHOTO_ID_ARG)!!, - onBack = { navController.popBackStack() }, ) } } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 3c29364..1f49072 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -23,16 +23,19 @@ android { } dependencies { - implementation(libs.retrofit) - - implementation(platform(libs.okhttp.bom)) - implementation(libs.okhttp) - implementation(libs.logging.interceptor) - implementation(libs.androidx.paging.compose) implementation(libs.kotlinx.serialization.json) - implementation(libs.retrofit2.kotlinx.serialization.converter) + + //Ktor + implementation(libs.ktor.client.android) + implementation(libs.ktor.client.resources) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.client.logging) + implementation(libs.ktor.serialization.kotlinx.json) + + //Logging + implementation(libs.slf4j.android) implementation(libs.androidx.room.runtime) ksp(libs.androidx.room.compiler) diff --git a/data/src/main/kotlin/dev/adriankuta/pixabay/data/di/NetworkModule.kt b/data/src/main/kotlin/dev/adriankuta/pixabay/data/di/NetworkModule.kt index 7311950..0e0e7b7 100644 --- a/data/src/main/kotlin/dev/adriankuta/pixabay/data/di/NetworkModule.kt +++ b/data/src/main/kotlin/dev/adriankuta/pixabay/data/di/NetworkModule.kt @@ -1,52 +1,60 @@ package dev.adriankuta.pixabay.data.di -import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dev.adriankuta.pixabay.data.BuildConfig +import dev.adriankuta.pixabay.data.network.KtorPixabayService import dev.adriankuta.pixabay.data.network.PixabayService +import io.ktor.client.HttpClient +import io.ktor.client.engine.android.Android +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.plugins.resources.Resources +import io.ktor.http.URLProtocol +import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module -internal class NetworkModule { - @Singleton - @Provides - fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor { - val logging = HttpLoggingInterceptor() - logging.level = - if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE - return logging - } +internal abstract class NetworkModule { @Singleton - @Provides - fun provideOkHttpClient( - httpLoggingInterceptor: HttpLoggingInterceptor - ): OkHttpClient { - return OkHttpClient.Builder() - .addInterceptor(httpLoggingInterceptor) - .build() - } + @Binds + abstract fun provideKtorApi(ktorPixabayService: KtorPixabayService): PixabayService + + companion object { + @Singleton + @Provides + fun provideKtorClient(): HttpClient { + val httpClient = HttpClient(Android) { + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + } + ) + } + install(Resources) + install(Logging) + defaultRequest { + url { + protocol = URLProtocol.HTTP + host = "pixabay.com" + parameters.append("key", BuildConfig.PIXABAY_API_KEY) + } + } + engine { + connectTimeout = 100_000 + socketTimeout = 100_000 + } + } + return httpClient + } - @Singleton - @Provides - fun providePixabayApi( - okHttpClient: OkHttpClient - ): PixabayService { - val networkJson = Json { ignoreUnknownKeys = true } - return Retrofit.Builder() - .client(okHttpClient) - .baseUrl("https://pixabay.com/") - .addConverterFactory(networkJson.asConverterFactory("application/json".toMediaType())) - .build() - .create(PixabayService::class.java) } } \ No newline at end of file diff --git a/data/src/main/kotlin/dev/adriankuta/pixabay/data/dto/request/PixabayImages.kt b/data/src/main/kotlin/dev/adriankuta/pixabay/data/dto/request/PixabayImages.kt new file mode 100644 index 0000000..96f2419 --- /dev/null +++ b/data/src/main/kotlin/dev/adriankuta/pixabay/data/dto/request/PixabayImages.kt @@ -0,0 +1,12 @@ +package dev.adriankuta.pixabay.data.dto.request + +import io.ktor.resources.Resource +import kotlinx.serialization.SerialName + +@Resource("/api") +internal class PixabayImages( + @SerialName("q") + val query: String, + val page: Int, + val pageSize: Int +) \ No newline at end of file diff --git a/data/src/main/kotlin/dev/adriankuta/pixabay/data/dto/request/PixabayImagesById.kt b/data/src/main/kotlin/dev/adriankuta/pixabay/data/dto/request/PixabayImagesById.kt new file mode 100644 index 0000000..82a63b2 --- /dev/null +++ b/data/src/main/kotlin/dev/adriankuta/pixabay/data/dto/request/PixabayImagesById.kt @@ -0,0 +1,8 @@ +package dev.adriankuta.pixabay.data.dto.request + +import io.ktor.resources.Resource + +@Resource("/api") +internal class PixabayImagesById( + val id: String, +) \ No newline at end of file diff --git a/data/src/main/kotlin/dev/adriankuta/pixabay/data/dto/response/SearchPixabayImagesResponse.kt b/data/src/main/kotlin/dev/adriankuta/pixabay/data/dto/response/SearchPixabayImagesResponse.kt index f94be5f..b1f2112 100644 --- a/data/src/main/kotlin/dev/adriankuta/pixabay/data/dto/response/SearchPixabayImagesResponse.kt +++ b/data/src/main/kotlin/dev/adriankuta/pixabay/data/dto/response/SearchPixabayImagesResponse.kt @@ -1,5 +1,6 @@ package dev.adriankuta.pixabay.data.dto.response +import io.ktor.resources.Resource import kotlinx.serialization.Serializable @Serializable diff --git a/data/src/main/kotlin/dev/adriankuta/pixabay/data/network/KtorPixabayService.kt b/data/src/main/kotlin/dev/adriankuta/pixabay/data/network/KtorPixabayService.kt new file mode 100644 index 0000000..995a900 --- /dev/null +++ b/data/src/main/kotlin/dev/adriankuta/pixabay/data/network/KtorPixabayService.kt @@ -0,0 +1,26 @@ +package dev.adriankuta.pixabay.data.network + +import dev.adriankuta.pixabay.data.dto.request.PixabayImages +import dev.adriankuta.pixabay.data.dto.request.PixabayImagesById +import dev.adriankuta.pixabay.data.dto.response.SearchPixabayImagesResponse +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.plugins.resources.get +import javax.inject.Inject + +internal class KtorPixabayService @Inject constructor( + private val client: HttpClient, +) : PixabayService { + + override suspend fun searchImages( + query: String, + page: Int, + pageSize: Int + ): SearchPixabayImagesResponse { + return client.get(PixabayImages(query, page, pageSize)).body() + } + + override suspend fun searchImageById(id: String): SearchPixabayImagesResponse { + return client.get(PixabayImagesById(id)).body() + } +} \ No newline at end of file diff --git a/data/src/main/kotlin/dev/adriankuta/pixabay/data/network/PixabayService.kt b/data/src/main/kotlin/dev/adriankuta/pixabay/data/network/PixabayService.kt index ea45959..f66fa54 100644 --- a/data/src/main/kotlin/dev/adriankuta/pixabay/data/network/PixabayService.kt +++ b/data/src/main/kotlin/dev/adriankuta/pixabay/data/network/PixabayService.kt @@ -1,23 +1,16 @@ package dev.adriankuta.pixabay.data.network -import dev.adriankuta.pixabay.data.BuildConfig import dev.adriankuta.pixabay.data.dto.response.SearchPixabayImagesResponse -import retrofit2.http.GET -import retrofit2.http.Query internal interface PixabayService { - @GET("api") suspend fun searchImages( - @Query("q") query: String, - @Query("page") page: Int, - @Query("per_page") pageSize: Int, - @Query("key") key: String = BuildConfig.PIXABAY_API_KEY + query: String, + page: Int, + pageSize: Int ): SearchPixabayImagesResponse - @GET("api") suspend fun searchImageById( - @Query("id") query: String, - @Query("key") key: String = BuildConfig.PIXABAY_API_KEY + id: String, ): SearchPixabayImagesResponse } \ No newline at end of file diff --git a/data/src/main/kotlin/dev/adriankuta/pixabay/data/paging/PixabayPagingSource.kt b/data/src/main/kotlin/dev/adriankuta/pixabay/data/paging/PixabayPagingSource.kt index 941422c..d698dfd 100644 --- a/data/src/main/kotlin/dev/adriankuta/pixabay/data/paging/PixabayPagingSource.kt +++ b/data/src/main/kotlin/dev/adriankuta/pixabay/data/paging/PixabayPagingSource.kt @@ -5,7 +5,6 @@ import androidx.paging.PagingState import dev.adriankuta.pixabay.data.model.PixabayImage import dev.adriankuta.pixabay.data.network.PixabayService import dev.adriankuta.pixabay.data.repository.PixabayImageRepository.Companion.NETWORK_PAGE_SIZE -import retrofit2.HttpException import java.io.IOException private const val STARTING_PAGE_INDEX = 1 @@ -34,8 +33,6 @@ internal class PixabayPagingSource( ) } catch (exception: IOException) { return LoadResult.Error(exception) - } catch (exception: HttpException) { - return LoadResult.Error(exception) } } diff --git a/data/src/main/kotlin/dev/adriankuta/pixabay/data/paging/PixabayRemoteMediator.kt b/data/src/main/kotlin/dev/adriankuta/pixabay/data/paging/PixabayRemoteMediator.kt index f370b4c..7d87918 100644 --- a/data/src/main/kotlin/dev/adriankuta/pixabay/data/paging/PixabayRemoteMediator.kt +++ b/data/src/main/kotlin/dev/adriankuta/pixabay/data/paging/PixabayRemoteMediator.kt @@ -7,12 +7,10 @@ import androidx.paging.LoadType import androidx.paging.PagingState import androidx.paging.RemoteMediator import androidx.room.withTransaction -import dev.adriankuta.pixabay.data.model.PixabayImage import dev.adriankuta.pixabay.data.network.PixabayService import dev.adriankuta.pixabay.data.room.AppDatabase import dev.adriankuta.pixabay.data.room.entity.PixabayImageEntity import dev.adriankuta.pixabay.data.room.entity.RemoteKeys -import retrofit2.HttpException import java.io.IOException private const val STARTING_PAGE_INDEX = 1 @@ -78,8 +76,6 @@ internal class PixabayRemoteMediator( return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) } catch (exception: IOException) { return MediatorResult.Error(exception) - } catch (exception: HttpException) { - return MediatorResult.Error(exception) } } diff --git a/data/src/main/kotlin/dev/adriankuta/pixabay/data/repository/PixabayImageRepository.kt b/data/src/main/kotlin/dev/adriankuta/pixabay/data/repository/PixabayImageRepository.kt index d910c2a..1bed424 100644 --- a/data/src/main/kotlin/dev/adriankuta/pixabay/data/repository/PixabayImageRepository.kt +++ b/data/src/main/kotlin/dev/adriankuta/pixabay/data/repository/PixabayImageRepository.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import timber.log.Timber import javax.inject.Inject +import javax.inject.Named internal class PixabayImageRepository @Inject constructor( private val database: AppDatabase, @@ -75,6 +76,6 @@ internal class PixabayImageRepository @Inject constructor( companion object { const val NETWORK_PAGE_SIZE = 30 - const val USE_CACHE_PAGER = true + const val USE_CACHE_PAGER = false } } \ No newline at end of file diff --git a/feature/details/src/main/kotlin/dev/adriankuta/pixabay/feature/details/PhotoDetailRoute.kt b/feature/details/src/main/kotlin/dev/adriankuta/pixabay/feature/details/PhotoDetailRoute.kt index 9a704db..685d326 100644 --- a/feature/details/src/main/kotlin/dev/adriankuta/pixabay/feature/details/PhotoDetailRoute.kt +++ b/feature/details/src/main/kotlin/dev/adriankuta/pixabay/feature/details/PhotoDetailRoute.kt @@ -39,7 +39,6 @@ import dev.adriankuta.pixabay.feature.details.di.PhotoDetailsViewModelFactory @Composable fun PhotoDetailRoute( photoId: Int, - onBack: () -> Unit, modifier: Modifier = Modifier, viewModel: PhotoDetailViewModel = hiltViewModel( creationCallback = { factory: PhotoDetailsViewModelFactory -> diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3aecf0c..78d91e7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] androidxNavigation = "2.7.7" androidGradlePlugin = "8.1.4" -agp = "8.6.0-beta02" +agp = "8.6.0-rc01" coilCompose = "2.6.0" composeCompiler = "1.5.14" kotlin = "1.9.24" @@ -15,13 +15,13 @@ androidxLifecycle = "2.8.3" activityCompose = "1.9.0" composeBom = "2024.06.00" kotlinxSerializationJson = "1.6.0" +ktor = "2.3.12" okhttpBom = "4.12.0" pagingCompose = "3.3.0" -retrofit2KotlinxSerializationConverter = "1.0.0" room = "2.6.1" +slf4jAndroid = "1.7.36" timber = "5.0.1" hilt = "2.51.1" -retrofit = "2.11.0" ksp = "1.9.24-1.0.20" [libraries] @@ -52,14 +52,18 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktor" } +ktor-client-resources = { group = "io.ktor", name = "ktor-client-resources", version.ref = "ktor" } +ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } +ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor" } okhttp = { module = "com.squareup.okhttp3:okhttp" } okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttpBom" } -retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit2KotlinxSerializationConverter" } +slf4j-android = { module = "org.slf4j:slf4j-android", version.ref = "slf4jAndroid" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } -retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }