From ad8fd4332ac17bae3ffd633de7bac2145c6b183c Mon Sep 17 00:00:00 2001 From: Michael Bely Date: Tue, 27 Feb 2024 23:13:59 +0300 Subject: [PATCH] Ktor (#234) --- core/network/build.gradle.kts | 6 +- .../michaelbel/movies/network/TmdbConfig.kt | 1 + .../movies/network/ktor/KtorAccountService.kt | 21 ++++++ .../network/ktor/KtorAuthenticationService.kt | 48 +++++++++++++ .../movies/network/ktor/KtorMovieService.kt | 42 ++++++++++++ .../movies/network/ktor/KtorSearchService.kt | 26 +++++++ .../movies/network/ktor/di/KtorModule.kt | 68 +++++++++++++++++++ .../network/okhttp/{ => di}/OkhttpModule.kt | 22 +++--- .../RetrofitAccountService.kt} | 5 +- .../RetrofitAuthenticationService.kt} | 5 +- .../movies/network/retrofit/RetrofitModule.kt | 30 -------- .../RetrofitMovieService.kt} | 5 +- .../RetrofitSearchService.kt} | 5 +- .../network/retrofit/di/RetrofitModule.kt | 58 ++++++++++++++++ .../{service => retrofit}/ktx/RetrofitKtx.kt | 2 +- .../serialization/ConverterFactoryModule.kt | 4 +- .../network/service/di/ServiceModule.kt | 42 ------------ .../repository/impl/AccountRepositoryImpl.kt | 15 ++-- .../impl/AuthenticationRepositoryImpl.kt | 25 +++---- .../repository/impl/ImageRepositoryImpl.kt | 11 ++- .../repository/impl/MovieRepositoryImpl.kt | 15 ++-- .../repository/impl/SearchRepositoryImpl.kt | 11 ++- .../impl/SuggestionRepositoryImpl.kt | 11 ++- core/widget/build.gradle.kts | 2 +- core/work/build.gradle.kts | 1 + gradle/libs.versions.toml | 19 ++++++ readme.md | 1 + 27 files changed, 371 insertions(+), 130 deletions(-) create mode 100644 core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/KtorAccountService.kt create mode 100644 core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/KtorAuthenticationService.kt create mode 100644 core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/KtorMovieService.kt create mode 100644 core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/KtorSearchService.kt create mode 100644 core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/di/KtorModule.kt rename core/network/src/main/kotlin/org/michaelbel/movies/network/okhttp/{ => di}/OkhttpModule.kt (74%) rename core/network/src/main/kotlin/org/michaelbel/movies/network/{service/account/AccountService.kt => retrofit/RetrofitAccountService.kt} (57%) rename core/network/src/main/kotlin/org/michaelbel/movies/network/{service/authentication/AuthenticationService.kt => retrofit/RetrofitAuthenticationService.kt} (83%) delete mode 100644 core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/RetrofitModule.kt rename core/network/src/main/kotlin/org/michaelbel/movies/network/{service/movie/MovieService.kt => retrofit/RetrofitMovieService.kt} (82%) rename core/network/src/main/kotlin/org/michaelbel/movies/network/{service/search/SearchService.kt => retrofit/RetrofitSearchService.kt} (69%) create mode 100644 core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/di/RetrofitModule.kt rename core/network/src/main/kotlin/org/michaelbel/movies/network/{service => retrofit}/ktx/RetrofitKtx.kt (56%) delete mode 100644 core/network/src/main/kotlin/org/michaelbel/movies/network/service/di/ServiceModule.kt diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 93ca55afa..b0cb772ed 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -51,10 +51,10 @@ android { dependencies { implementation(libs.androidx.startup.runtime) - api(libs.kotlin.serialization.json) + implementation(libs.kotlin.serialization.json) implementation(libs.okhttp.logging.interceptor) - implementation(libs.retrofit.converter.serialization) - api(libs.retrofit) + implementation(libs.bundles.retrofit) + implementation(libs.bundles.ktor) debugImplementation(libs.chucker.library) releaseImplementation(libs.chucker.library.no.op) debugImplementation(libs.flaker.android.okhttp) diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/TmdbConfig.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/TmdbConfig.kt index 6429bf25b..773b85f2f 100644 --- a/core/network/src/main/kotlin/org/michaelbel/movies/network/TmdbConfig.kt +++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/TmdbConfig.kt @@ -1,5 +1,6 @@ package org.michaelbel.movies.network +const val TMDB_API_ENDPOINT = "https://api.themoviedb.org/3/" const val TMDB_URL = "https://themoviedb.org" const val TMDB_TERMS_OF_USE = "$TMDB_URL/documentation/website/terms-of-use" const val TMDB_PRIVACY_POLICY = "$TMDB_URL/privacy-policy" diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/KtorAccountService.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/KtorAccountService.kt new file mode 100644 index 000000000..83b9eeafe --- /dev/null +++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/KtorAccountService.kt @@ -0,0 +1,21 @@ +package org.michaelbel.movies.network.ktor + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import javax.inject.Inject +import org.michaelbel.movies.network.model.Account + +class KtorAccountService @Inject constructor( + private val ktorHttpClient: HttpClient +) { + + suspend fun accountDetails( + sessionId: String + ): Account { + return ktorHttpClient.get("account") { + parameter("session_id", sessionId) + }.body() + } +} \ No newline at end of file diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/KtorAuthenticationService.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/KtorAuthenticationService.kt new file mode 100644 index 000000000..da1126ebe --- /dev/null +++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/KtorAuthenticationService.kt @@ -0,0 +1,48 @@ +package org.michaelbel.movies.network.ktor + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.delete +import io.ktor.client.request.get +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import javax.inject.Inject +import org.michaelbel.movies.network.model.DeletedSession +import org.michaelbel.movies.network.model.RequestToken +import org.michaelbel.movies.network.model.Session +import org.michaelbel.movies.network.model.SessionRequest +import org.michaelbel.movies.network.model.Token +import org.michaelbel.movies.network.model.Username + +class KtorAuthenticationService @Inject constructor( + private val ktorHttpClient: HttpClient +) { + + suspend fun createRequestToken(): Token { + return ktorHttpClient.get("authentication/token/new?").body() + } + + suspend fun createSessionWithLogin( + username: Username + ): Token { + return ktorHttpClient.post("authentication/token/validate_with_login?") { + setBody(username) + }.body() + } + + suspend fun createSession( + authToken: RequestToken + ): Session { + return ktorHttpClient.post("authentication/session/new?") { + setBody(authToken) + }.body() + } + + suspend fun deleteSession( + sessionRequest: SessionRequest + ): DeletedSession { + return ktorHttpClient.delete("authentication/session?") { + setBody(sessionRequest) + }.body() + } +} \ No newline at end of file diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/KtorMovieService.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/KtorMovieService.kt new file mode 100644 index 000000000..51c30f977 --- /dev/null +++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/KtorMovieService.kt @@ -0,0 +1,42 @@ +package org.michaelbel.movies.network.ktor + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import javax.inject.Inject +import org.michaelbel.movies.network.model.ImagesResponse +import org.michaelbel.movies.network.model.Movie +import org.michaelbel.movies.network.model.MovieResponse +import org.michaelbel.movies.network.model.Result + +class KtorMovieService @Inject constructor( + private val ktorHttpClient: HttpClient +) { + + suspend fun movies( + list: String, + language: String, + page: Int + ): Result { + return ktorHttpClient.get("movie/$list") { + parameter("language", language) + parameter("page", page) + }.body() + } + + suspend fun movie( + movieId: Int, + language: String + ): Movie { + return ktorHttpClient.get("movie/$movieId") { + parameter("language", language) + }.body() + } + + suspend fun images( + movieId: Int + ): ImagesResponse { + return ktorHttpClient.get("movie/$movieId/images").body() + } +} \ No newline at end of file diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/KtorSearchService.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/KtorSearchService.kt new file mode 100644 index 000000000..0c572dc67 --- /dev/null +++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/KtorSearchService.kt @@ -0,0 +1,26 @@ +package org.michaelbel.movies.network.ktor + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import javax.inject.Inject +import org.michaelbel.movies.network.model.MovieResponse +import org.michaelbel.movies.network.model.Result + +class KtorSearchService @Inject constructor( + private val ktorHttpClient: HttpClient +) { + + suspend fun searchMovies( + query: String, + language: String, + page: Int + ): Result { + return ktorHttpClient.get("search/movie") { + parameter("query", query) + parameter("language", language) + parameter("page", page) + }.body() + } +} \ No newline at end of file diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/di/KtorModule.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/di/KtorModule.kt new file mode 100644 index 000000000..a7dc59abd --- /dev/null +++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/ktor/di/KtorModule.kt @@ -0,0 +1,68 @@ +package org.michaelbel.movies.network.ktor.di + +import com.chuckerteam.chucker.api.ChuckerInterceptor +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import io.github.rotbolt.flakerokhttpcore.FlakerInterceptor +import io.ktor.client.HttpClient +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.http.ContentType +import io.ktor.http.contentType +import io.ktor.serialization.kotlinx.json.json +import javax.inject.Singleton +import kotlinx.serialization.json.Json +import okhttp3.logging.HttpLoggingInterceptor +import org.michaelbel.movies.network.TMDB_API_ENDPOINT +import org.michaelbel.movies.network.okhttp.di.OkhttpModule +import org.michaelbel.movies.network.okhttp.interceptor.ApikeyInterceptor + +@Module +@InstallIn(SingletonComponent::class) +internal object KtorModule { + + @Provides + @Singleton + fun provideKtorHttpClient( + chuckerInterceptor: ChuckerInterceptor, + flakerInterceptor: FlakerInterceptor, + httpLoggingInterceptor: HttpLoggingInterceptor, + apikeyInterceptor: ApikeyInterceptor + ): HttpClient { + val ktor = HttpClient(OkHttp) { + defaultRequest { + contentType(ContentType.Application.Json) + url(TMDB_API_ENDPOINT) + } + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + } + ) + } + install(HttpTimeout) { + requestTimeoutMillis = REQUEST_TIMEOUT_MILLIS + connectTimeoutMillis = OkhttpModule.CONNECT_TIMEOUT_MILLIS + socketTimeoutMillis = SOCKET_TIMEOUT_SECONDS + } + engine { + clientCacheSize = OkhttpModule.HTTP_CACHE_SIZE_BYTES + config { + addInterceptor(chuckerInterceptor) + addInterceptor(flakerInterceptor) + addInterceptor(httpLoggingInterceptor) + addInterceptor(apikeyInterceptor) + } + } + } + return ktor + } + + private const val REQUEST_TIMEOUT_MILLIS = 10_000L + private const val SOCKET_TIMEOUT_SECONDS = 10_000L +} \ No newline at end of file diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/okhttp/OkhttpModule.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/okhttp/di/OkhttpModule.kt similarity index 74% rename from core/network/src/main/kotlin/org/michaelbel/movies/network/okhttp/OkhttpModule.kt rename to core/network/src/main/kotlin/org/michaelbel/movies/network/okhttp/di/OkhttpModule.kt index d438302a3..b59212bda 100644 --- a/core/network/src/main/kotlin/org/michaelbel/movies/network/okhttp/OkhttpModule.kt +++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/okhttp/di/OkhttpModule.kt @@ -1,4 +1,4 @@ -package org.michaelbel.movies.network.okhttp +package org.michaelbel.movies.network.okhttp.di import android.content.Context import com.chuckerteam.chucker.api.ChuckerInterceptor @@ -25,7 +25,7 @@ internal object OkhttpModule { fun httpCache( @ApplicationContext context: Context ): Cache { - return Cache(context.cacheDir, HTTP_CACHE_SIZE_BYTES) + return Cache(context.cacheDir, HTTP_CACHE_SIZE_BYTES.toLong()) } @Provides @@ -56,10 +56,10 @@ internal object OkhttpModule { addInterceptor(flakerInterceptor) addInterceptor(httpLoggingInterceptor) addInterceptor(apikeyInterceptor) - callTimeout(CALL_TIMEOUT_SECONDS, TimeUnit.SECONDS) - connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS) - readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS) - writeTimeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS) + callTimeout(CALL_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) + connectTimeout(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) + readTimeout(READ_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) + writeTimeout(WRITE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) retryOnConnectionFailure(true) followRedirects(true) followSslRedirects(true) @@ -68,9 +68,9 @@ internal object OkhttpModule { return builder.build() } - private const val HTTP_CACHE_SIZE_BYTES = 1024 * 1024 * 50L - private const val CALL_TIMEOUT_SECONDS = 0L - private const val CONNECT_TIMEOUT_SECONDS = 10L - private const val READ_TIMEOUT_SECONDS = 10L - private const val WRITE_TIMEOUT_SECONDS = 10L + const val HTTP_CACHE_SIZE_BYTES = 1024 * 1024 * 50 + const val CONNECT_TIMEOUT_MILLIS = 10_000L + private const val READ_TIMEOUT_MILLIS = 10_000L + private const val WRITE_TIMEOUT_MILLIS = 10_000L + private const val CALL_TIMEOUT_MILLIS = 0L } \ No newline at end of file diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/account/AccountService.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/RetrofitAccountService.kt similarity index 57% rename from core/network/src/main/kotlin/org/michaelbel/movies/network/service/account/AccountService.kt rename to core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/RetrofitAccountService.kt index 41cf6ccee..2b3707491 100644 --- a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/account/AccountService.kt +++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/RetrofitAccountService.kt @@ -1,10 +1,11 @@ -package org.michaelbel.movies.network.service.account +package org.michaelbel.movies.network.retrofit import org.michaelbel.movies.network.model.Account import retrofit2.http.GET import retrofit2.http.Query -interface AccountService { +@Deprecated("Use KtorAccountService instead", ReplaceWith("KtorAccountService")) +interface RetrofitAccountService { @GET("account") suspend fun accountDetails( diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/authentication/AuthenticationService.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/RetrofitAuthenticationService.kt similarity index 83% rename from core/network/src/main/kotlin/org/michaelbel/movies/network/service/authentication/AuthenticationService.kt rename to core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/RetrofitAuthenticationService.kt index 2d89a4530..acbe0699c 100644 --- a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/authentication/AuthenticationService.kt +++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/RetrofitAuthenticationService.kt @@ -1,4 +1,4 @@ -package org.michaelbel.movies.network.service.authentication +package org.michaelbel.movies.network.retrofit import org.michaelbel.movies.network.model.DeletedSession import org.michaelbel.movies.network.model.RequestToken @@ -11,7 +11,8 @@ import retrofit2.http.GET import retrofit2.http.HTTP import retrofit2.http.POST -interface AuthenticationService { +@Deprecated("Use KtorAuthenticationService instead", ReplaceWith("KtorAuthenticationService")) +interface RetrofitAuthenticationService { @GET("authentication/token/new?") suspend fun createRequestToken(): Token diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/RetrofitModule.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/RetrofitModule.kt deleted file mode 100644 index da75c8039..000000000 --- a/core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/RetrofitModule.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.michaelbel.movies.network.retrofit - -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton -import okhttp3.OkHttpClient -import retrofit2.Converter -import retrofit2.Retrofit - -@Module -@InstallIn(SingletonComponent::class) -internal object RetrofitModule { - - private const val TMDB_API_ENDPOINT = "https://api.themoviedb.org/3/" - - @Provides - @Singleton - fun provideRetrofit( - converterFactory: Converter.Factory, - okHttpClient: OkHttpClient - ): Retrofit { - return Retrofit.Builder() - .baseUrl(TMDB_API_ENDPOINT) - .addConverterFactory(converterFactory) - .client(okHttpClient) - .build() - } -} \ No newline at end of file diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/movie/MovieService.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/RetrofitMovieService.kt similarity index 82% rename from core/network/src/main/kotlin/org/michaelbel/movies/network/service/movie/MovieService.kt rename to core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/RetrofitMovieService.kt index 7efac924e..ab217660c 100644 --- a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/movie/MovieService.kt +++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/RetrofitMovieService.kt @@ -1,4 +1,4 @@ -package org.michaelbel.movies.network.service.movie +package org.michaelbel.movies.network.retrofit import org.michaelbel.movies.network.model.ImagesResponse import org.michaelbel.movies.network.model.Movie @@ -8,7 +8,8 @@ import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query -interface MovieService { +@Deprecated("Use KtorMovieService instead", ReplaceWith("KtorMovieService")) +interface RetrofitMovieService { @GET("movie/{list}") suspend fun movies( diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/search/SearchService.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/RetrofitSearchService.kt similarity index 69% rename from core/network/src/main/kotlin/org/michaelbel/movies/network/service/search/SearchService.kt rename to core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/RetrofitSearchService.kt index 190ec4c5c..c53decec9 100644 --- a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/search/SearchService.kt +++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/RetrofitSearchService.kt @@ -1,11 +1,12 @@ -package org.michaelbel.movies.network.service.search +package org.michaelbel.movies.network.retrofit import org.michaelbel.movies.network.model.MovieResponse import org.michaelbel.movies.network.model.Result import retrofit2.http.GET import retrofit2.http.Query -interface SearchService { +@Deprecated("Use KtorSearchService instead", ReplaceWith("KtorSearchService")) +interface RetrofitSearchService { @GET("search/movie") suspend fun searchMovies( diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/di/RetrofitModule.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/di/RetrofitModule.kt new file mode 100644 index 000000000..15e00c04e --- /dev/null +++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/di/RetrofitModule.kt @@ -0,0 +1,58 @@ +package org.michaelbel.movies.network.retrofit.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +import okhttp3.OkHttpClient +import org.michaelbel.movies.network.TMDB_API_ENDPOINT +import org.michaelbel.movies.network.retrofit.RetrofitAccountService +import org.michaelbel.movies.network.retrofit.RetrofitAuthenticationService +import org.michaelbel.movies.network.retrofit.RetrofitMovieService +import org.michaelbel.movies.network.retrofit.RetrofitSearchService +import org.michaelbel.movies.network.retrofit.ktx.createService +import retrofit2.Converter +import retrofit2.Retrofit + +@Module +@InstallIn(SingletonComponent::class) +internal object RetrofitModule { + + @Provides + @Singleton + fun provideRetrofit( + converterFactory: Converter.Factory, + okHttpClient: OkHttpClient + ): Retrofit { + return Retrofit.Builder() + .baseUrl(TMDB_API_ENDPOINT) + .addConverterFactory(converterFactory) + .client(okHttpClient) + .build() + } + + @Provides + @Singleton + fun provideAuthenticationService( + retrofit: Retrofit + ): RetrofitAuthenticationService = retrofit.createService() + + @Provides + @Singleton + fun provideAccountService( + retrofit: Retrofit + ): RetrofitAccountService = retrofit.createService() + + @Provides + @Singleton + fun provideMovieService( + retrofit: Retrofit + ): RetrofitMovieService = retrofit.createService() + + @Provides + @Singleton + fun provideSearchService( + retrofit: Retrofit + ): RetrofitSearchService = retrofit.createService() +} \ No newline at end of file diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/ktx/RetrofitKtx.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/ktx/RetrofitKtx.kt similarity index 56% rename from core/network/src/main/kotlin/org/michaelbel/movies/network/service/ktx/RetrofitKtx.kt rename to core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/ktx/RetrofitKtx.kt index 9e8252897..c105dfbed 100644 --- a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/ktx/RetrofitKtx.kt +++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/retrofit/ktx/RetrofitKtx.kt @@ -1,4 +1,4 @@ -package org.michaelbel.movies.network.service.ktx +package org.michaelbel.movies.network.retrofit.ktx import retrofit2.Retrofit diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/serialization/ConverterFactoryModule.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/serialization/ConverterFactoryModule.kt index 2fa4a7a03..b128e5635 100644 --- a/core/network/src/main/kotlin/org/michaelbel/movies/network/serialization/ConverterFactoryModule.kt +++ b/core/network/src/main/kotlin/org/michaelbel/movies/network/serialization/ConverterFactoryModule.kt @@ -17,10 +17,8 @@ internal object ConverterFactoryModule { @Provides @Singleton fun provideSerializationConverterFactory(): Converter.Factory { - val contentType = MEDIA_TYPE_APPLICATION_JSON.toMediaType() + val contentType = "application/json".toMediaType() val format = Json { ignoreUnknownKeys = true } return format.asConverterFactory(contentType) } - - private const val MEDIA_TYPE_APPLICATION_JSON = "application/json" } \ No newline at end of file diff --git a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/di/ServiceModule.kt b/core/network/src/main/kotlin/org/michaelbel/movies/network/service/di/ServiceModule.kt deleted file mode 100644 index 13b2eb877..000000000 --- a/core/network/src/main/kotlin/org/michaelbel/movies/network/service/di/ServiceModule.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.michaelbel.movies.network.service.di - -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton -import org.michaelbel.movies.network.service.account.AccountService -import org.michaelbel.movies.network.service.authentication.AuthenticationService -import org.michaelbel.movies.network.service.ktx.createService -import org.michaelbel.movies.network.service.movie.MovieService -import org.michaelbel.movies.network.service.search.SearchService -import retrofit2.Retrofit - -@Module -@InstallIn(SingletonComponent::class) -internal object ServiceModule { - - @Provides - @Singleton - fun provideAuthenticationService( - retrofit: Retrofit - ): AuthenticationService = retrofit.createService() - - @Provides - @Singleton - fun provideAccountService( - retrofit: Retrofit - ): AccountService = retrofit.createService() - - @Provides - @Singleton - fun provideMovieService( - retrofit: Retrofit - ): MovieService = retrofit.createService() - - @Provides - @Singleton - fun provideSearchService( - retrofit: Retrofit - ): SearchService = retrofit.createService() -} \ No newline at end of file diff --git a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/AccountRepositoryImpl.kt b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/AccountRepositoryImpl.kt index e3ba2fc6f..8deb172a8 100644 --- a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/AccountRepositoryImpl.kt +++ b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/AccountRepositoryImpl.kt @@ -6,16 +6,21 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import org.michaelbel.movies.common.exceptions.AccountDetailsException -import org.michaelbel.movies.network.service.account.AccountService +import org.michaelbel.movies.network.ktor.KtorAccountService +import org.michaelbel.movies.network.retrofit.RetrofitAccountService import org.michaelbel.movies.persistence.database.dao.AccountDao import org.michaelbel.movies.persistence.database.entity.AccountDb import org.michaelbel.movies.persistence.datastore.MoviesPreferences import org.michaelbel.movies.repository.AccountRepository import org.michaelbel.movies.repository.ktx.mapToAccountDb +/** + * You can replace [ktorAccountService] with [retrofitAccountService] to use it. + */ @Singleton internal class AccountRepositoryImpl @Inject constructor( - private val accountService: AccountService, + private val retrofitAccountService: RetrofitAccountService, + private val ktorAccountService: KtorAccountService, private val accountDao: AccountDao, private val preferences: MoviesPreferences ): AccountRepository { @@ -33,15 +38,15 @@ internal class AccountRepositoryImpl @Inject constructor( } override suspend fun accountDetails() { - try { + runCatching { val sessionId = preferences.sessionId().orEmpty() - val account = accountService.accountDetails(sessionId) + val account = ktorAccountService.accountDetails(sessionId) preferences.run { setAccountId(account.id) setAccountExpireTime(System.currentTimeMillis()) } accountDao.insert(account.mapToAccountDb) - } catch (ignored: Exception) { + }.onFailure { throw AccountDetailsException } } diff --git a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/AuthenticationRepositoryImpl.kt b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/AuthenticationRepositoryImpl.kt index aa6e0a98b..108483d7d 100644 --- a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/AuthenticationRepositoryImpl.kt +++ b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/AuthenticationRepositoryImpl.kt @@ -6,26 +6,31 @@ import org.michaelbel.movies.common.exceptions.CreateRequestTokenException import org.michaelbel.movies.common.exceptions.CreateSessionException import org.michaelbel.movies.common.exceptions.CreateSessionWithLoginException import org.michaelbel.movies.common.exceptions.DeleteSessionException +import org.michaelbel.movies.network.ktor.KtorAuthenticationService import org.michaelbel.movies.network.model.RequestToken import org.michaelbel.movies.network.model.Session import org.michaelbel.movies.network.model.SessionRequest import org.michaelbel.movies.network.model.Token import org.michaelbel.movies.network.model.Username -import org.michaelbel.movies.network.service.authentication.AuthenticationService +import org.michaelbel.movies.network.retrofit.RetrofitAuthenticationService import org.michaelbel.movies.persistence.database.dao.AccountDao import org.michaelbel.movies.persistence.datastore.MoviesPreferences import org.michaelbel.movies.repository.AuthenticationRepository +/** + * You can replace [ktorAuthenticationService] with [retrofitAuthenticationService] to use it. + */ @Singleton internal class AuthenticationRepositoryImpl @Inject constructor( - private val authenticationService: AuthenticationService, + private val retrofitAuthenticationService: RetrofitAuthenticationService, + private val ktorAuthenticationService: KtorAuthenticationService, private val accountDao: AccountDao, private val preferences: MoviesPreferences ): AuthenticationRepository { override suspend fun createRequestToken(loginViaTmdb: Boolean): Token { return try { - val token = authenticationService.createRequestToken() + val token = ktorAuthenticationService.createRequestToken() if (!token.success) { throw CreateRequestTokenException(loginViaTmdb) } @@ -41,7 +46,7 @@ internal class AuthenticationRepositoryImpl @Inject constructor( requestToken: String ): Token { return try { - val token = authenticationService.createSessionWithLogin( + val token = ktorAuthenticationService.createSessionWithLogin( username = Username( username = username, password = password, @@ -59,9 +64,7 @@ internal class AuthenticationRepositoryImpl @Inject constructor( override suspend fun createSession(token: String): Session { return try { - val session = authenticationService.createSession( - authToken = RequestToken(token) - ) + val session = ktorAuthenticationService.createSession(RequestToken(token)) if (session.success) { preferences.setSessionId(session.sessionId) } else { @@ -74,12 +77,10 @@ internal class AuthenticationRepositoryImpl @Inject constructor( } override suspend fun deleteSession() { - try { + runCatching { val sessionId = preferences.sessionId().orEmpty() val sessionRequest = SessionRequest(sessionId) - val deletedSession = authenticationService.deleteSession( - sessionRequest = sessionRequest - ) + val deletedSession = ktorAuthenticationService.deleteSession(sessionRequest) if (deletedSession.success) { val accountId = preferences.accountId() accountDao.removeById(accountId) @@ -90,7 +91,7 @@ internal class AuthenticationRepositoryImpl @Inject constructor( } else { throw DeleteSessionException } - } catch (ignored: Exception) { + }.onFailure { throw DeleteSessionException } } diff --git a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/ImageRepositoryImpl.kt b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/ImageRepositoryImpl.kt index 9c3501241..15b3ddf2a 100644 --- a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/ImageRepositoryImpl.kt +++ b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/ImageRepositoryImpl.kt @@ -3,15 +3,20 @@ package org.michaelbel.movies.repository.impl import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.flow.Flow -import org.michaelbel.movies.network.service.movie.MovieService +import org.michaelbel.movies.network.ktor.KtorMovieService +import org.michaelbel.movies.network.retrofit.RetrofitMovieService import org.michaelbel.movies.persistence.database.dao.ImageDao import org.michaelbel.movies.persistence.database.entity.ImageDb import org.michaelbel.movies.persistence.database.ktx.imageDb import org.michaelbel.movies.repository.ImageRepository +/** + * You can replace [ktorMovieService] with [retrofitMovieService] to use it. + */ @Singleton internal class ImageRepositoryImpl @Inject constructor( - private val movieService: MovieService, + private val retrofitMovieService: RetrofitMovieService, + private val ktorMovieService: KtorMovieService, private val imageDao: ImageDao ): ImageRepository { @@ -20,7 +25,7 @@ internal class ImageRepositoryImpl @Inject constructor( } override suspend fun images(movieId: Int) { - val imageResponse = movieService.images(movieId) + val imageResponse = ktorMovieService.images(movieId) val posters = imageResponse.posters.mapIndexed { index, image -> image.imageDb( movieId = movieId, diff --git a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/MovieRepositoryImpl.kt b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/MovieRepositoryImpl.kt index 9410cb436..e711911b8 100644 --- a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/MovieRepositoryImpl.kt +++ b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/MovieRepositoryImpl.kt @@ -9,9 +9,10 @@ import org.michaelbel.movies.common.exceptions.MoviesUpcomingException import org.michaelbel.movies.common.list.MovieList import org.michaelbel.movies.common.localization.LocaleController import org.michaelbel.movies.network.isTmdbApiKeyEmpty +import org.michaelbel.movies.network.ktor.KtorMovieService import org.michaelbel.movies.network.model.MovieResponse import org.michaelbel.movies.network.model.Result -import org.michaelbel.movies.network.service.movie.MovieService +import org.michaelbel.movies.network.retrofit.RetrofitMovieService import org.michaelbel.movies.persistence.database.dao.MovieDao import org.michaelbel.movies.persistence.database.entity.MovieDb import org.michaelbel.movies.persistence.database.entity.mini.MovieDbMini @@ -21,9 +22,13 @@ import org.michaelbel.movies.repository.MovieRepository import org.michaelbel.movies.repository.ktx.checkApiKeyNotNullException import org.michaelbel.movies.repository.ktx.mapToMovieDb +/** + * You can replace [ktorMovieService] with [retrofitMovieService] to use it. + */ @Singleton internal class MovieRepositoryImpl @Inject constructor( - private val movieService: MovieService, + private val retrofitMovieService: RetrofitMovieService, + private val ktorMovieService: KtorMovieService, private val movieDao: MovieDao, private val localeController: LocaleController ): MovieRepository { @@ -41,7 +46,7 @@ internal class MovieRepositoryImpl @Inject constructor( checkApiKeyNotNullException() } - return movieService.movies( + return ktorMovieService.movies( list = movieList, language = localeController.language, page = page @@ -54,7 +59,7 @@ internal class MovieRepositoryImpl @Inject constructor( override suspend fun movieDetails(pagingKey: String, movieId: Int): MovieDb { return try { - movieDao.movieById(pagingKey, movieId) ?: movieService.movie(movieId, localeController.language).mapToMovieDb + movieDao.movieById(pagingKey, movieId) ?: ktorMovieService.movie(movieId, localeController.language).mapToMovieDb } catch (ignored: Exception) { throw MovieDetailsException } @@ -62,7 +67,7 @@ internal class MovieRepositoryImpl @Inject constructor( override suspend fun moviesWidget(): List { return try { - val movieResult = movieService.movies( + val movieResult = ktorMovieService.movies( list = MovieList.Upcoming.name, language = localeController.language, page = 1 diff --git a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/SearchRepositoryImpl.kt b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/SearchRepositoryImpl.kt index 062c2a082..b7e9aada2 100644 --- a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/SearchRepositoryImpl.kt +++ b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/SearchRepositoryImpl.kt @@ -4,15 +4,20 @@ import javax.inject.Inject import javax.inject.Singleton import org.michaelbel.movies.common.localization.LocaleController import org.michaelbel.movies.network.isTmdbApiKeyEmpty +import org.michaelbel.movies.network.ktor.KtorSearchService import org.michaelbel.movies.network.model.MovieResponse import org.michaelbel.movies.network.model.Result -import org.michaelbel.movies.network.service.search.SearchService +import org.michaelbel.movies.network.retrofit.RetrofitSearchService import org.michaelbel.movies.repository.SearchRepository import org.michaelbel.movies.repository.ktx.checkApiKeyNotNullException +/** + * You can replace [ktorSearchService] with [retrofitSearchService] to use it. + */ @Singleton internal class SearchRepositoryImpl @Inject constructor( - private val searchService: SearchService, + private val retrofitSearchService: RetrofitSearchService, + private val ktorSearchService: KtorSearchService, private val localeController: LocaleController ): SearchRepository { @@ -21,7 +26,7 @@ internal class SearchRepositoryImpl @Inject constructor( checkApiKeyNotNullException() } - return searchService.searchMovies( + return ktorSearchService.searchMovies( query = query, language = localeController.language, page = page diff --git a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/SuggestionRepositoryImpl.kt b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/SuggestionRepositoryImpl.kt index 90e87209e..4c57c1077 100644 --- a/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/SuggestionRepositoryImpl.kt +++ b/core/repository/src/main/kotlin/org/michaelbel/movies/repository/impl/SuggestionRepositoryImpl.kt @@ -4,16 +4,21 @@ import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.flow.Flow import org.michaelbel.movies.common.localization.LocaleController +import org.michaelbel.movies.network.ktor.KtorMovieService import org.michaelbel.movies.network.model.Movie -import org.michaelbel.movies.network.service.movie.MovieService +import org.michaelbel.movies.network.retrofit.RetrofitMovieService import org.michaelbel.movies.persistence.database.dao.MovieDao import org.michaelbel.movies.persistence.database.dao.SuggestionDao import org.michaelbel.movies.persistence.database.entity.SuggestionDb import org.michaelbel.movies.repository.SuggestionRepository +/** + * You can replace [ktorMovieService] with [retrofitMovieService] to use it. + */ @Singleton internal class SuggestionRepositoryImpl @Inject constructor( - private val movieService: MovieService, + private val retrofitMovieService: RetrofitMovieService, + private val ktorMovieService: KtorMovieService, private val movieDao: MovieDao, private val suggestionDao: SuggestionDao, private val localeController: LocaleController @@ -30,7 +35,7 @@ internal class SuggestionRepositoryImpl @Inject constructor( if (nowPlayingMovies.isNotEmpty()) { suggestionDao.insert(nowPlayingMovies.map { movieDb -> SuggestionDb(movieDb.title) }) } else { - val movieResponse = movieService.movies( + val movieResponse = ktorMovieService.movies( list = Movie.NOW_PLAYING, language = localeController.language, page = 1 diff --git a/core/widget/build.gradle.kts b/core/widget/build.gradle.kts index 2c2b35d39..7c6588a2f 100644 --- a/core/widget/build.gradle.kts +++ b/core/widget/build.gradle.kts @@ -3,8 +3,8 @@ plugins { alias(libs.plugins.library) alias(libs.plugins.kotlin) alias(libs.plugins.detekt) + alias(libs.plugins.kotlin.serialization) id("movies-android-hilt") - id("org.jetbrains.kotlin.plugin.serialization") } android { diff --git a/core/work/build.gradle.kts b/core/work/build.gradle.kts index 711be577b..17d008553 100644 --- a/core/work/build.gradle.kts +++ b/core/work/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(project(":core:network")) implementation(project(":core:notifications")) implementation(project(":core:ui")) + implementation(libs.kotlin.serialization.json) implementation(libs.androidx.paging.compose) implementation(libs.androidx.hilt.work) implementation(libs.androidx.work.runtime.ktx) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b5d383775..de6a11b4e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,6 +66,7 @@ junit = "4.13.2" lint-checks = "1.3.1" palantir-git = "3.0.0" flaker = "0.1.2" +ktor = "2.3.8" [libraries] kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" } @@ -145,6 +146,12 @@ junit = { module = "junit:junit", version.ref = "junit" } lint-checks = { module = "com.slack.lint.compose:compose-lint-checks", version.ref = "lint-checks" } flaker-android-okhttp = { module = "io.github.rotbolt:flaker-android-okhttp", version.ref = "flaker" } flaker-android-okhttp-noop = { module = "io.github.rotbolt:flaker-android-okhttp-noop", version.ref = "flaker" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "gradle" } kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } @@ -202,6 +209,18 @@ room = [ "androidx-room-ktx", "androidx-room-paging" ] +retrofit = [ + "retrofit", + "retrofit-converter-serialization" +] +ktor = [ + "ktor-client-core", + "ktor-client-cio", + "ktor-client-okhttp", + "ktor-client-android", + "ktor-client-content-negotiation", + "ktor-serialization-kotlinx-json" +] [plugins] kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } diff --git a/readme.md b/readme.md index e97b41d98..31f261b39 100644 --- a/readme.md +++ b/readme.md @@ -83,6 +83,7 @@ TMDB_API_KEY=your_own_tmdb_api_key - [x] [OkHttp](https://github.com/square/okhttp) - [x] [Retrofit](https://github.com/square/retrofit) - [x] [Retrofit Kotlinx Converter Serialization](https://github.com/JakeWharton/retrofit2-kotlinx-serialization-converter) +- [x] [Ktor](https://ktor.io) - [x] [Chucker](https://github.com/ChuckerTeam/chucker) - [x] [Flaker](https://github.com/rotbolt/flaker) - [x] [Coil](https://github.com/coil-kt/coil)