From 8652d4ceaaafaee0f4cbb95772aee3a68b62a866 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Mon, 10 Oct 2022 00:02:53 +0530 Subject: [PATCH] refactor(api): adopt `EitherNet` --- .../claw/android/injection/ApiModule.kt | 4 +++ .../android/paging/LobstersPagingSource.kt | 27 ++++++++++--------- .../claw/android/paging/RemoteFetcher.kt | 4 ++- .../claw/android/viewmodel/ClawViewModel.kt | 23 ++++++++++++++-- .../android/work/SavedPostUpdaterWorker.kt | 7 +++-- api/build.gradle.kts | 1 + .../dev/msfjarvis/claw/api/LobstersApi.kt | 12 ++++++--- gradle/libs.versions.toml | 1 + 8 files changed, 56 insertions(+), 23 deletions(-) diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/injection/ApiModule.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/injection/ApiModule.kt index dc459fc4..4a30b17c 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/injection/ApiModule.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/injection/ApiModule.kt @@ -1,6 +1,8 @@ package dev.msfjarvis.claw.android.injection import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.slack.eithernet.ApiResultCallAdapterFactory +import com.slack.eithernet.ApiResultConverterFactory import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -26,7 +28,9 @@ object ApiModule { return Retrofit.Builder() .client(client) .baseUrl(LobstersApi.BASE_URL) + .addConverterFactory(ApiResultConverterFactory) .addConverterFactory(json.asConverterFactory(contentType)) + .addCallAdapterFactory(ApiResultCallAdapterFactory) .build() } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/paging/LobstersPagingSource.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/paging/LobstersPagingSource.kt index e68d8d69..e72116d8 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/paging/LobstersPagingSource.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/paging/LobstersPagingSource.kt @@ -2,11 +2,14 @@ package dev.msfjarvis.claw.android.paging import androidx.paging.PagingSource import androidx.paging.PagingState +import com.slack.eithernet.ApiResult.Failure +import com.slack.eithernet.ApiResult.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dev.msfjarvis.claw.android.injection.IODispatcher import dev.msfjarvis.claw.model.LobstersPost +import java.io.IOException import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext @@ -17,19 +20,19 @@ constructor( @IODispatcher private val ioDispatcher: CoroutineDispatcher, ) : PagingSource() { - @Suppress("TooGenericExceptionCaught") // Intentional override suspend fun load(params: LoadParams): LoadResult { - return try { - val page = params.key ?: 1 - val posts = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) } - - LoadResult.Page( - data = posts, - prevKey = if (page == 1) null else page - 1, - nextKey = page.plus(1) - ) - } catch (e: Exception) { - LoadResult.Error(e) + val page = params.key ?: 1 + return when (val result = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }) { + is Success -> + LoadResult.Page( + data = result.value, + prevKey = if (page == 1) null else page - 1, + nextKey = page.plus(1) + ) + is Failure.NetworkFailure -> LoadResult.Error(result.error) + is Failure.UnknownFailure -> LoadResult.Error(result.error) + is Failure.HttpFailure, + is Failure.ApiFailure -> LoadResult.Error(IOException("API returned an invalid response")) } } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/paging/RemoteFetcher.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/paging/RemoteFetcher.kt index a59e550b..939126da 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/paging/RemoteFetcher.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/paging/RemoteFetcher.kt @@ -1,6 +1,8 @@ package dev.msfjarvis.claw.android.paging +import com.slack.eithernet.ApiResult + /** SAM interface to abstract over a remote API that fetches paginated content. */ fun interface RemoteFetcher { - suspend fun getItemsAtPage(page: Int): List + suspend fun getItemsAtPage(page: Int): ApiResult, Unit> } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ClawViewModel.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ClawViewModel.kt index 08bac673..04dbbb60 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ClawViewModel.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ClawViewModel.kt @@ -4,12 +4,15 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.Pager import androidx.paging.PagingConfig +import com.slack.eithernet.ApiResult.Failure +import com.slack.eithernet.ApiResult.Success import dagger.hilt.android.lifecycle.HiltViewModel import dev.msfjarvis.claw.android.injection.IODispatcher import dev.msfjarvis.claw.android.paging.LobstersPagingSource import dev.msfjarvis.claw.android.ui.toLocalDateTime import dev.msfjarvis.claw.api.LobstersApi import dev.msfjarvis.claw.database.local.SavedPost +import java.io.IOException import java.time.Month import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -73,12 +76,28 @@ constructor( suspend fun getPostComments(postId: String) = withContext(ioDispatcher) { - val details = api.getPostDetails(postId) + val details = + when (val result = api.getPostDetails(postId)) { + is Success -> result.value + is Failure.NetworkFailure -> throw result.error + is Failure.UnknownFailure -> throw result.error + is Failure.HttpFailure, + is Failure.ApiFailure -> throw IOException("API returned an invalid response") + } val extendedDetails = postDetailsRepository.getExtendedDetails(details) extendedDetails } - suspend fun getUserProfile(username: String) = withContext(ioDispatcher) { api.getUser(username) } + suspend fun getUserProfile(username: String) = + withContext(ioDispatcher) { + when (val result = api.getUser(username)) { + is Success -> result.value + is Failure.NetworkFailure -> throw result.error + is Failure.UnknownFailure -> throw result.error + is Failure.HttpFailure, + is Failure.ApiFailure -> throw IOException("API returned an invalid response") + } + } fun refreshHottestPosts() { hottestPostsPagingSource?.invalidate() diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/work/SavedPostUpdaterWorker.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/work/SavedPostUpdaterWorker.kt index daac065d..3589296a 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/work/SavedPostUpdaterWorker.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/work/SavedPostUpdaterWorker.kt @@ -4,13 +4,13 @@ import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters +import com.slack.eithernet.ApiResult import dagger.assisted.Assisted import dagger.assisted.AssistedInject import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository import dev.msfjarvis.claw.api.LobstersApi import dev.msfjarvis.claw.common.posts.toDbModel import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.first * and for new-enough posts that are still getting comments to have an accurate one. */ @Suppress("DEPRECATION") // We're being nasty -@OptIn(ExperimentalCoroutinesApi::class) @HiltWorker class SavedPostUpdaterWorker @AssistedInject @@ -39,8 +38,8 @@ constructor( .map { post -> CoroutineScope(coroutineContext + Job()).async { val details = runCatching { lobstersApi.getPostDetails(post.shortId) }.getOrNull() - if (details != null) { - savedPostsRepository.savePost(details.toDbModel()) + if (details is ApiResult.Success) { + savedPostsRepository.savePost(details.value.toDbModel()) } } } diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 884d5d11..c136d2b4 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -8,6 +8,7 @@ plugins { dependencies { api(projects.model) api(libs.retrofit.lib) + api(libs.eithernet) implementation(libs.kotlinx.serialization.core) testImplementation(libs.kotlinx.coroutines.core) testImplementation(kotlin("test-junit")) diff --git a/api/src/main/kotlin/dev/msfjarvis/claw/api/LobstersApi.kt b/api/src/main/kotlin/dev/msfjarvis/claw/api/LobstersApi.kt index 18acf188..9445c705 100644 --- a/api/src/main/kotlin/dev/msfjarvis/claw/api/LobstersApi.kt +++ b/api/src/main/kotlin/dev/msfjarvis/claw/api/LobstersApi.kt @@ -1,5 +1,6 @@ package dev.msfjarvis.claw.api +import com.slack.eithernet.ApiResult import dev.msfjarvis.claw.model.LobstersPost import dev.msfjarvis.claw.model.LobstersPostDetails import dev.msfjarvis.claw.model.User @@ -10,14 +11,17 @@ import retrofit2.http.Query /** Simple interface defining an API for lobste.rs */ interface LobstersApi { - @GET("hottest.json") suspend fun getHottestPosts(@Query("page") page: Int): List + @GET("hottest.json") + suspend fun getHottestPosts(@Query("page") page: Int): ApiResult, Unit> - @GET("newest.json") suspend fun getNewestPosts(@Query("page") page: Int): List + @GET("newest.json") + suspend fun getNewestPosts(@Query("page") page: Int): ApiResult, Unit> @GET("s/{postId}.json") - suspend fun getPostDetails(@Path("postId") postId: String): LobstersPostDetails + suspend fun getPostDetails(@Path("postId") postId: String): ApiResult - @GET("u/{username}.json") suspend fun getUser(@Path("username") username: String): User + @GET("u/{username}.json") + suspend fun getUser(@Path("username") username: String): ApiResult companion object { const val BASE_URL = "https://lobste.rs" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1b2ab4da..151e341e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,6 +67,7 @@ crux = "com.chimbori.crux:crux:3.9.1" dagger-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "dagger" } dagger-hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "dagger" } dagger-hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "dagger" } +eithernet = "com.slack.eithernet:eithernet:1.2.1" javapoet = "com.squareup:javapoet:1.13.0" jsoup = "org.jsoup:jsoup:1.15.3" kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }