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 package dev.msfjarvis.claw.android.injection
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import com.slack.eithernet.ApiResultCallAdapterFactory
import com.slack.eithernet.ApiResultConverterFactory
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -26,7 +28,9 @@ object ApiModule {
return Retrofit.Builder() return Retrofit.Builder()
.client(client) .client(client)
.baseUrl(LobstersApi.BASE_URL) .baseUrl(LobstersApi.BASE_URL)
.addConverterFactory(ApiResultConverterFactory)
.addConverterFactory(json.asConverterFactory(contentType)) .addConverterFactory(json.asConverterFactory(contentType))
.addCallAdapterFactory(ApiResultCallAdapterFactory)
.build() .build()
} }

View file

@ -2,11 +2,14 @@ package dev.msfjarvis.claw.android.paging
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.paging.PagingState import androidx.paging.PagingState
import com.slack.eithernet.ApiResult.Failure
import com.slack.eithernet.ApiResult.Success
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import dev.msfjarvis.claw.android.injection.IODispatcher import dev.msfjarvis.claw.android.injection.IODispatcher
import dev.msfjarvis.claw.model.LobstersPost import dev.msfjarvis.claw.model.LobstersPost
import java.io.IOException
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -17,19 +20,19 @@ constructor(
@IODispatcher private val ioDispatcher: CoroutineDispatcher, @IODispatcher private val ioDispatcher: CoroutineDispatcher,
) : PagingSource<Int, LobstersPost>() { ) : PagingSource<Int, LobstersPost>() {
@Suppress("TooGenericExceptionCaught") // Intentional
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, LobstersPost> { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, LobstersPost> {
return try { val page = params.key ?: 1
val page = params.key ?: 1 return when (val result = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }) {
val posts = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) } is Success ->
LoadResult.Page(
LoadResult.Page( data = result.value,
data = posts, prevKey = if (page == 1) null else page - 1,
prevKey = if (page == 1) null else page - 1, nextKey = page.plus(1)
nextKey = page.plus(1) )
) is Failure.NetworkFailure -> LoadResult.Error(result.error)
} catch (e: Exception) { is Failure.UnknownFailure -> LoadResult.Error(result.error)
LoadResult.Error(e) 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 package dev.msfjarvis.claw.android.paging
import com.slack.eithernet.ApiResult
/** SAM interface to abstract over a remote API that fetches paginated content. */ /** SAM interface to abstract over a remote API that fetches paginated content. */
fun interface RemoteFetcher<T> { 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.lifecycle.viewModelScope
import androidx.paging.Pager import androidx.paging.Pager
import androidx.paging.PagingConfig import androidx.paging.PagingConfig
import com.slack.eithernet.ApiResult.Failure
import com.slack.eithernet.ApiResult.Success
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.msfjarvis.claw.android.injection.IODispatcher import dev.msfjarvis.claw.android.injection.IODispatcher
import dev.msfjarvis.claw.android.paging.LobstersPagingSource import dev.msfjarvis.claw.android.paging.LobstersPagingSource
import dev.msfjarvis.claw.android.ui.toLocalDateTime import dev.msfjarvis.claw.android.ui.toLocalDateTime
import dev.msfjarvis.claw.api.LobstersApi import dev.msfjarvis.claw.api.LobstersApi
import dev.msfjarvis.claw.database.local.SavedPost import dev.msfjarvis.claw.database.local.SavedPost
import java.io.IOException
import java.time.Month import java.time.Month
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
@ -73,12 +76,28 @@ constructor(
suspend fun getPostComments(postId: String) = suspend fun getPostComments(postId: String) =
withContext(ioDispatcher) { 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) val extendedDetails = postDetailsRepository.getExtendedDetails(details)
extendedDetails 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() { fun refreshHottestPosts() {
hottestPostsPagingSource?.invalidate() hottestPostsPagingSource?.invalidate()

View file

@ -4,13 +4,13 @@ import android.content.Context
import androidx.hilt.work.HiltWorker import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.slack.eithernet.ApiResult
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
import dev.msfjarvis.claw.api.LobstersApi import dev.msfjarvis.claw.api.LobstersApi
import dev.msfjarvis.claw.common.posts.toDbModel import dev.msfjarvis.claw.common.posts.toDbModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll 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. * and for new-enough posts that are still getting comments to have an accurate one.
*/ */
@Suppress("DEPRECATION") // We're being nasty @Suppress("DEPRECATION") // We're being nasty
@OptIn(ExperimentalCoroutinesApi::class)
@HiltWorker @HiltWorker
class SavedPostUpdaterWorker class SavedPostUpdaterWorker
@AssistedInject @AssistedInject
@ -39,8 +38,8 @@ constructor(
.map { post -> .map { post ->
CoroutineScope(coroutineContext + Job()).async { CoroutineScope(coroutineContext + Job()).async {
val details = runCatching { lobstersApi.getPostDetails(post.shortId) }.getOrNull() val details = runCatching { lobstersApi.getPostDetails(post.shortId) }.getOrNull()
if (details != null) { if (details is ApiResult.Success) {
savedPostsRepository.savePost(details.toDbModel()) savedPostsRepository.savePost(details.value.toDbModel())
} }
} }
} }

View file

@ -8,6 +8,7 @@ plugins {
dependencies { dependencies {
api(projects.model) api(projects.model)
api(libs.retrofit.lib) api(libs.retrofit.lib)
api(libs.eithernet)
implementation(libs.kotlinx.serialization.core) implementation(libs.kotlinx.serialization.core)
testImplementation(libs.kotlinx.coroutines.core) testImplementation(libs.kotlinx.coroutines.core)
testImplementation(kotlin("test-junit")) testImplementation(kotlin("test-junit"))

View file

@ -1,5 +1,6 @@
package dev.msfjarvis.claw.api package dev.msfjarvis.claw.api
import com.slack.eithernet.ApiResult
import dev.msfjarvis.claw.model.LobstersPost import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.LobstersPostDetails import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.User import dev.msfjarvis.claw.model.User
@ -10,14 +11,17 @@ import retrofit2.http.Query
/** Simple interface defining an API for lobste.rs */ /** Simple interface defining an API for lobste.rs */
interface LobstersApi { 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") @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 { companion object {
const val BASE_URL = "https://lobste.rs" 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-android = { module = "com.google.dagger:hilt-android", version.ref = "dagger" }
dagger-hilt-compiler = { module = "com.google.dagger:hilt-compiler", 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" } 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" javapoet = "com.squareup:javapoet:1.13.0"
jsoup = "org.jsoup:jsoup:1.15.3" jsoup = "org.jsoup:jsoup:1.15.3"
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }