🚆 Retrofit adapters for modeling network responses with Kotlin Result, Jetpack Paging3, and Arrow Either.
If you're interested in a more specified and lightweight Monad sealed API library for modeling Retrofit responses and handling exceptions, check out Sandwich.
This library allows you to model your Retrofit responses with Kotlin's Result class.
Add the dependency below to your module's build.gradle
file:
dependencies {
implementation "com.github.skydoves:retrofit-adapters-result:1.0.12"
}
You can return Kotlin's Result class to the Retrofit's service methods by setting ResultCallAdapterFactory
like the below:
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("BASE_URL")
.addConverterFactory(..)
.addCallAdapterFactory(ResultCallAdapterFactory.create())
.build()
Then you can return the Result
class with the suspend keyword.
interface PokemonService {
@GET("pokemon")
suspend fun fetchPokemonList(
@Query("limit") limit: Int = 20,
@Query("offset") offset: Int = 0
): Result<PokemonResponse>
}
Finally, you will get the network response, which is wrapped by the Result
class like the below:
viewModelScope.launch {
val result = pokemonService.fetchPokemonList()
if (result.isSuccess) {
val data = result.getOrNull()
// handle data
} else {
// handle error case
}
}
You can confine the response type as Unit when you need to handle empty body (content) API requests like the below:
@POST("/users/info")
suspend fun updateUserInfo(@Body userRequest: UserRequest): Result<Unit>
You can also inject your custom CoroutineScope
into the ResultCallAdapterFactory
and execute network requests on the scope.
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
val testScope = TestScope(testDispatcher)
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("BASE_URL")
.addConverterFactory(..)
.addCallAdapterFactory(ResultCallAdapterFactory.create(testScope))
.build()
Note: For more information about the Testing coroutines, check out the Testing Kotlin coroutines on Android.
This library allows you to return the paging source, which is parts of the Jetpack's Paging library.
Add the dependency below to your module's build.gradle
file:
dependencies {
implementation "com.github.skydoves:retrofit-adapters-paging:<version>"
}
You can return Jetpack's PagingSource class to the Retrofit's service methods by setting PagingCallAdapterFactory
like the below:
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("BASE_URL")
.addConverterFactory(..)
.addCallAdapterFactory(PagingCallAdapterFactory.create())
.build()
Then you can return the NetworkPagingSource
class with the @PagingKeyConfig
and @PagingKey
annotations:
interface PokemonService {
@GET("pokemon")
@PagingKeyConfig(
keySize = 20,
mapper = PokemonPagingMapper::class
)
suspend fun fetchPokemonListAsPagingSource(
@Query("limit") limit: Int = 20,
@PagingKey @Query("offset") offset: Int = 0,
): NetworkPagingSource<PokemonResponse, Pokemon>
}
To return the NetworkPagingSource
class, you must attach the @PagingKeyConfig
and @PagingKey
annotations to your Retrofit's service methods.
- @PagingKeyConfig: Contains paging configurations for the network request and delivery them to the call adapter internally. You should set the
keySize
andmapper
parameters. - @PagingKey: Marks the parameter in the service interface method as the paging key. This parameter will be paged by incrementing the page values continuously.
You should create a paging mapper class, which extends the PagingMapper<T, R>
interface like the below for transforming the original network response to the list of paging items. This class should be used in the @PagingKeyConfig
annotation.
class PokemonPagingMapper : PagingMapper<PokemonResponse, Pokemon> {
override fun map(value: PokemonResponse): List<Pokemon> {
return value.results
}
}
You will get the network response, which is wrapped by the NetworkPagingSource
class like the below:
viewModelScope.launch {
val pagingSource = pokemonService.fetchPokemonListAsPagingSource()
val pagerFlow = Pager(PagingConfig(pageSize = 20)) { pagingSource }.flow
stateFlow.emitAll(pagerFlow)
}
Finally, you should call the submitData
method by your PagingDataAdapter
to bind the paging data. If you want to learn more about the Jetpack's Paging, check out the Paging library.
This library allows you to model your Retrofit responses with arrow-kt's Either class.
Add the dependency below to your module's build.gradle
file:
dependencies {
implementation "com.github.skydoves:retrofit-adapters-arrow:<version>"
}
You can return Arrow's Either class to the Retrofit's service methods by setting EitherCallAdapterFactory
like the below:
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("BASE_URL")
.addConverterFactory(..)
.addCallAdapterFactory(EitherCallAdapterFactory.create())
.build()
Then you can return the Either
class with the suspend keyword.
interface PokemonService {
@GET("pokemon")
suspend fun fetchPokemonListAsEither(
@Query("limit") limit: Int = 20,
@Query("offset") offset: Int = 0
): Either<Throwable, PokemonResponse>
}
Finally, you will get the network response, which is wrapped by the Either
class like the below:
viewModelScope.launch {
val either = pokemonService.fetchPokemonListAsEither()
if (either.isRight()) {
val data = either.orNull()
// handle data
} else {
// handle error case
}
}
You can confine the response type as Unit when you need to handle empty body (content) API requests like the below:
@POST("/users/info")
suspend fun updateUserInfo(@Body userRequest: UserRequest): Either<Throwable, Unit>
You can also inject your custom CoroutineScope
into the EitherCallAdapterFactory
and execute network requests on the scope.
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
val testScope = TestScope(testDispatcher)
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("BASE_URL")
.addConverterFactory(..)
.addCallAdapterFactory(EitherCallAdapterFactory.create(testScope))
.build()
Note: For more information about the Testing coroutines, check out the Testing Kotlin coroutines on Android.
This library allows you to deserialize your error body of the Retrofit response as your custom error class with Kotlin's Serialization.
For more information about setting up the plugin and dependency, check out Kotlin's Serialization.
Add the dependency below to your module's build.gradle
file:
dependencies {
implementation "com.github.skydoves:retrofit-adapters-serialization:<version>"
}
You can deserialize your error body with the deserializeHttpError
extension and your custom error class. First, define your custom error class following your RESTful API formats as seen in the below:
@Serializable
public data class ErrorMessage(
val code: Int,
val message: String
)
Next, gets the result of the error class to the throwable
instance with the deserializeHttpError
extension like the below:
val result = pokemonService.fetchPokemonList()
result.onSuccessSuspend {
Timber.d("fetched as Result: $it")
}.onFailureSuspend { throwable ->
val errorBody = throwable.deserializeHttpError<ErrorMessage>()
}
Support it by joining stargazers for this repository. ⭐
Also, follow me on GitHub for my next creations! 🤩
Designed and developed by 2022 skydoves (Jaewoong Eum)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.