refactor(api): adopt EitherNet

This commit is contained in:
Harsh Shandilya 2022-10-10 00:02:53 +05:30
parent 484fac5779
commit 8652d4ceaa
No known key found for this signature in database
8 changed files with 56 additions and 23 deletions

View file

@ -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()
}

View file

@ -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<Int, LobstersPost>() {
@Suppress("TooGenericExceptionCaught") // Intentional
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, LobstersPost> {
return try {
val page = params.key ?: 1
val posts = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }
return when (val result = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }) {
is Success ->
LoadResult.Page(
data = posts,
data = result.value,
prevKey = if (page == 1) null else page - 1,
nextKey = page.plus(1)
)
} catch (e: Exception) {
LoadResult.Error(e)
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"))
}
}

View file

@ -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<T> {
suspend fun getItemsAtPage(page: Int): List<T>
suspend fun getItemsAtPage(page: Int): ApiResult<List<T>, Unit>
}

View file

@ -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()

View file

@ -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())
}
}
}

View file

@ -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"))

View file

@ -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<LobstersPost>
@GET("hottest.json")
suspend fun getHottestPosts(@Query("page") page: Int): ApiResult<List<LobstersPost>, Unit>
@GET("newest.json") suspend fun getNewestPosts(@Query("page") page: Int): List<LobstersPost>
@GET("newest.json")
suspend fun getNewestPosts(@Query("page") page: Int): ApiResult<List<LobstersPost>, Unit>
@GET("s/{postId}.json")
suspend fun getPostDetails(@Path("postId") postId: String): LobstersPostDetails
suspend fun getPostDetails(@Path("postId") postId: String): ApiResult<LobstersPostDetails, Unit>
@GET("u/{username}.json") suspend fun getUser(@Path("username") username: String): User
@GET("u/{username}.json")
suspend fun getUser(@Path("username") username: String): ApiResult<User, Unit>
companion object {
const val BASE_URL = "https://lobste.rs"

View file

@ -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" }