mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-14 08:17:04 +05:30
refactor(api): adopt EitherNet
This commit is contained in:
parent
484fac5779
commit
8652d4ceaa
8 changed files with 56 additions and 23 deletions
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue