Coder Social home page Coder Social logo

skydoves / sandwich Goto Github PK

View Code? Open in Web Editor NEW
1.5K 17.0 103.0 2.36 MB

🥪 Sandwich is an adaptable and lightweight sealed API library designed for handling API responses and exceptions in Kotlin for Retrofit, Ktor, and Kotlin Multiplatform.

Home Page: https://skydoves.github.io/sandwich/

License: Apache License 2.0

Kotlin 100.00%
android retrofit api network apiresponse datasource kotlin skydoves

sandwich's Introduction

sandwich

License API Build Status
Google Medium Profile Profile Dokka

Why Sandwich?

Sandwich was conceived to streamline the creation of standardized interfaces to model responses from Retrofit, Ktor, and whatever. This library empowers you to handle body data, errors, and exceptional cases more succinctly, utilizing functional operators within a multi-layer architecture. With Sandwich, the need to create wrapper classes like Resource or Result is eliminated, allowing you to concentrate on your core business logic. Sandwich boasts features such as global response handling, Mapper, Operator, and exceptional compatibility, including ApiResponse With Coroutines.

Download

Maven Central

Sandwich has achieved an impressive milestone, being downloaded in over 300,000 Android projects worldwide!

Gradle

Add the dependency below into your module's build.gradle file:

dependencies {
    implementation("com.github.skydoves:sandwich:2.0.7")
    implementation("com.github.skydoves:sandwich-retrofit:2.0.7") // For Retrofit (Android)
}

For Kotlin Multiplatform, add the dependency below to your module's build.gradle.kts file:

sourceSets {
    val commonMain by getting {
        dependencies {
            implementation("com.github.skydoves:sandwich:$version")
            implementation("com.github.skydoves:sandwich-ktor:$version")
            implementation("com.github.skydoves:sandwich-ktorfit:$version")
        }
    }
}

SNAPSHOT

Sandwich

See how to import the snapshot

Including the SNAPSHOT

Snapshots of the current development version of Sandwich are available, which track the latest versions.

To import snapshot versions on your project, add the code snippet below on your gradle file:

repositories {
   maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
}

Next, add the dependency below to your module's build.gradle file:

dependencies {
    implementation "com.github.skydoves:sandwich:1.3.10-SNAPSHOT"
}

R8 / ProGuard

The specific rules are already bundled into the JAR which can be interpreted by R8 automatically.

Documentation

For comprehensive details about Sandwich, please refer to the complete documentation available here.

Use Cases

You can also check out nice use cases of this library in the repositories below:

  • Pokedex: 🗡️ Android Pokedex using Hilt, Motion, Coroutines, Flow, Jetpack (Room, ViewModel, LiveData) based on MVVM architecture.
  • ChatGPT Android: 📲 ChatGPT Android demonstrates OpenAI's ChatGPT on Android with Stream Chat SDK for Compose.
  • DisneyMotions: 🦁 A Disney app using transformation motions based on MVVM (ViewModel, Coroutines, LiveData, Room, Repository, Koin) architecture.
  • MarvelHeroes: ❤️ A sample Marvel heroes application based on MVVM (ViewModel, Coroutines, LiveData, Room, Repository, Koin) architecture.
  • Neko: Free, open source, unofficial MangaDex reader for Android.
  • TheMovies2: 🎬 A demo project using The Movie DB based on Kotlin MVVM architecture and material design & animations.

Usage

For comprehensive details about Sandwich, please refer to the complete documentation available here.

ApiResponse

ApiResponse serves as an interface designed to create consistent responses from API or I/O calls, such as network, database, or whatever. It offers convenient extensions to manage your payloads, encompassing both body data and exceptional scenarios. ApiResponse encompasses three distinct types: Success, Failure.Error, and Failure.Exception.

ApiResponse.Success

This represents a successful response from API or I/O tasks. You can create an instance of [ApiResponse.Success] by giving the generic type and data.

val apiResponse = ApiResponse.Success(data = myData)
val data = apiResponse.data

Depending on your model designs, you can also utilize tag property. The tag is an additional value that can be held to distinguish the origin of the data or to facilitate post-processing of successful data.

val apiResponse = ApiResponse.Success(data = myData, tag = myTag)
val tag = apiResponse.tag

ApiResponse.Failure.Exception

This signals a failed tasks captured by unexpected exceptions during API request creation or response processing on the client side, such as a network connection failure. You can obtain exception details from the ApiResponse.Failure.Exception.

val apiResponse = ApiResponse.Failure.Exception(exception = HttpTimeoutException())
val exception = apiResponse.exception
val message = apiResponse.message

ApiResponse.Failure.Error

This denotes a failed API or I/O request, typically due to bad requests or internal server errors. You can additionally put an error payload that can contain detailed error information.

val apiResponse = ApiResponse.Failure.Error(payload = errorBody)
val payload = apiResponse.payload

You can also define custom error responses that extend ApiResponse.Failure.Error or ApiResponse.Failure.Exception, as demonstrated in the example below:

data object LimitedRequest : ApiResponse.Failure.Error(
  payload = "your request is limited",
)

data object WrongArgument : ApiResponse.Failure.Error(
  payload = "wrong argument",
)

data object HttpException : ApiResponse.Failure.Exception(
  throwable = RuntimeException("http exception")
)

The custom error response is very useful when you want to explicitly define and handle error responses, especially when working with map extensions.

val apiResponse = service.fetchMovieList()
apiResponse.onSuccess {
    // ..
}.flatMap {
  // if the ApiResponse is Failure.Error and contains error body, then maps it to a custom error response.  
  if (this is ApiResponse.Failure.Error) {
    val errorBody = (payload as? Response)?.body?.string()
    if (errorBody != null) {
      val errorMessage: ErrorMessage = Json.decodeFromString(errorBody)
      when (errorMessage.code) {
        10000 -> LimitedRequest
        10001 -> WrongArgument
      }
    }
  }
  this
}

Then you can handle the errors based on your custom message in other layers:

val apiResponse = repository.fetchMovieList()
apiResponse.onError {
  when (this) {
    LimitedRequest -> // update your UI
    WrongArgument -> // update your UI
  }
}

You might not want to use the flatMap extension for all API requests. If you aim to standardize custom error types across all API requests, you can explore the Global Failure Mapper.

Creation of ApiResponse

Sandwich provides convenient ways to create an ApiResponse using functions such as ApiResponse.of or apiResponseOf, as shown below:

val apiResponse = ApiResponse.of { service.request() }
val apiResponse = apiResponseOf { service.request() }

If you need to run suspend functions inside the lambda, you can use ApiResponse.suspendOf or suspendApiResponseOf instead:

val apiResponse = ApiResponse.suspendOf { service.request() }
val apiResponse = suspendApiResponseOf { service.request() }

Note: If you intend to utilize the global operator or global ApiResponse mapper in Sandwich, you should create an ApiResponse using the ApiResponse.of method to ensure the application of these global functions.

ApiResponse Extensions

You can effectively handling ApiResponse using the following extensions:

  • onSuccess: Executes when the ApiResponse is of type ApiResponse.Success. Within this scope, you can directly access the body data.
  • onError: Executes when the ApiResponse is of type ApiResponse.Failure.Error. Here, you can access the messareOrNull and payload here.
  • onException: Executes when the ApiResponse is of type ApiResponse.Failure.Exception. You can access the messareOrNull and exception here.
  • onFailure: Executes when the ApiResponse is either ApiResponse.Failure.Error or ApiResponse.Failure.Exception. You can access the messareOrNull here.

Each scope operates according to its corresponding ApiResponse type:

val response = disneyService.fetchDisneyPosterList()
response.onSuccess {
    // this scope will be executed if the request successful.
    // handle the success case
  }.onError {
    // this scope will be executed when the request failed with errors.
    // handle the error case
  }.onException {
   // this scope will be executed when the request failed with exceptions.
   // handle the exception case
  }

If you don't want to specify each failure case, you can simplify it by using the onFailure extension:

val response = disneyService.fetchDisneyPosterList()
response.onSuccess {
    // this scope will be executed if the request successful.
    // handle the success case
  }.onFailure {
      
  }

ApiResponse Extensions With Coroutines

With the ApiResponse type, you can leverage Coroutines extensions to handle responses seamlessly within coroutine scopes. These extensions provide a convenient way to process different response types. Here's how you can use them:

  • suspendOnSuccess: This extension runs if the ApiResponse is of type ApiResponse.Success. You can access the body data directly within this scope.

  • suspendOnError: This extension is executed if the ApiResponse is of type ApiResponse.Failure.Error. You can access the error message and the error body in this scope.

  • suspendOnException: If the ApiResponse is of type ApiResponse.Failure.Exception, this extension is triggered. You can access the exception message in this scope.

  • suspendOnFailure: This extension is executed if the ApiResponse is either ApiResponse.Failure.Error or ApiResponse.Failure.Exception. You can access the error message in this scope.

Each extension scope operates based on the corresponding ApiResponse type. By utilizing these extensions, you can handle responses effectively within different coroutine contexts.

flow {
  val response = disneyService.fetchDisneyPosterList()
  response.suspendOnSuccess {
    posterDao.insertPosterList(data) // insertPosterList(data) is a suspend function.
    emit(data)
  }.suspendOnError {
    // handles error cases
  }.suspendOnException {
    // handles exceptional cases
  }
}.flowOn(Dispatchers.IO)

Flow

Sandwich offers some useful extensions to transform your ApiResponse into a Flow by using the toFlow extension:

val flow = disneyService.fetchDisneyPosterList()
  .onError {
    // handles error cases when the API request gets an error response.
  }.onException {
    // handles exceptional cases when the API request gets an exception response.
  }.toFlow() // returns a coroutines flow
  .flowOn(Dispatchers.IO)

If you want to transform the original data and work with a Flow containing the transformed data, you can do so as shown in the examples below:

val response = pokedexClient.fetchPokemonList(page = page)
response.toFlow { pokemons ->
  pokemons.forEach { pokemon -> pokemon.page = page }
  pokemonDao.insertPokemonList(pokemons)
  pokemonDao.getAllPokemonList(page)
}.flowOn(Dispatchers.IO)

Retrieving

Sandwich provides effortless methods to directly extract the encapsulated body data from the ApiResponse. You can take advantage of the following functionalities:

getOrNull

Returns the encapsulated data if this instance represents ApiResponse.Success or returns null if this is failed.

val data: List<Poster>? = disneyService.fetchDisneyPosterList().getOrNull()

getOrElse

Returns the encapsulated data if this instance represents ApiResponse.Success or returns a default value if this is failed.

val data: List<Poster> = disneyService.fetchDisneyPosterList().getOrElse(emptyList())

getOrThrow

Returns the encapsulated data if this instance represents ApiResponse.Success or throws the encapsulated Throwable exception if this is failed.

try {
  val data: List<Poster> = disneyService.fetchDisneyPosterList().getOrThrow()
} catch (e: Exception) {
  e.printStackTrace()
}

Retry

Sandwich offers seamless ways to run and retry tasks. To execute and retry network or I/O requests, you can employ the RetryPolicy interface along with the runAndRetry extension, as demonstrated in the code below:

val retryPolicy = object : RetryPolicy {
  override fun shouldRetry(attempt: Int, message: String?): Boolean = attempt <= 3

  override fun retryTimeout(attempt: Int, message: String?): Int = 3000
}

val apiResponse = runAndRetry(retryPolicy) { attempt, reason ->
  mainRepository.fetchPosters()
}.onSuccess {
  // Handle a success case
}.onFailure {
  // Handle failure cases
}

Sequential

Sandwich provides sequential solutions for scenarios where you require sequential execution of network requests.

then and suspendThen

If you have a scenario where you need to execute tasks A, B, and C in a dependent sequence, for example, where task B depends on the completion of task A, and task C depends on the completion of task B, you can effectively utilize the then or suspendThen extensions, as demonstrated in the example below:

service.getUserToken(id) suspendThen { tokenResponse ->
    service.getUserDetails(tokenResponse.token) 
} suspendThen { userResponse ->
    service.queryPosters(userResponse.user.name)
}.mapSuccess { posterResponse ->
  posterResponse.posters
}.onSuccess {
    posterStateFlow.value = data
}.onFailure {
    Log.e("sequential", message())
}

Operator

The Operator feature stands out as one of the most powerful capabilities provided by Sandwich. It empowers you to establish well-defined, preconfigured processors for your ApiResponse instances. This enables you to encapsulate and reuse a consistent sequence of procedures across your API requests.

You can streamline the handling of onSuccess, onError, and onException scenarios by utilizing the operator extension alongside the ApiResponseOperator. Operator proves particularly valuable when you're aiming for global handling of ApiResponse instances and wish to minimize boilerplate code within your ViewModel and Repository classes. Here are a few illustrative examples:

/** A common response operator for handling [ApiResponse]s regardless of its type. */
class CommonResponseOperator<T>(
  private val success: suspend (ApiResponse.Success<T>) -> Unit
) : ApiResponseOperator<T>() {

  // handles error cases when the API request gets an error response.
  override fun onSuccess(apiResponse: ApiResponse.Success<T>) = success(apiResponse)

  // handles error cases depending on the status code.
  // e.g., internal server error.
  override fun onError(apiResponse: ApiResponse.Failure.Error) {
    apiResponse.run {
      Timber.d(message())
      
      // map the ApiResponse.Failure.Error to a customized error model using the mapper.
      map(ErrorEnvelopeMapper) {
        Timber.d("[Code: $code]: $message")
      }
    }
  }

  // handles exceptional cases when the API request gets an exception response.
  // e.g., network connection error, timeout.
  override fun onException(apiResponse: ApiResponse.Failure.Exception) {
    apiResponse.run {
      Timber.d(message())
    }
  }
}

disneyService.fetchDisneyPosterList().operator(
    CommonResponseOperator(
      success = {
        emit(data)
        Timber.d("success data: $data")
     }
    )
)

By embracing the Operator pattern, you can significantly simplify the management of various ApiResponse outcomes and promote cleaner, more maintainable code within your application's architecture.

Operator With Coroutines

For scenarios where you aim to delegate and operate a suspension lambda using the operator pattern, the suspendOperator extension and the ApiResponseSuspendOperator class come into play. These tools facilitate the process, as showcased in the examples below:

class CommonResponseOperator<T>(
  private val success: suspend (ApiResponse.Success<T>) -> Unit
) : ApiResponseSuspendOperator<T>() {

  // handles the success case when the API request gets a successful response.
  override suspend fun onSuccess(apiResponse: ApiResponse.Success<T>) = success(apiResponse)

  // ... //
}

You can use suspend functions like emit in the success scope.

val response = disneyService.fetchDisneyPosterList().suspendOperator(
    CommonResponseOperator(
      success = {
        emit(data)
        Timber.d("success data: $data")
      }
    )
)

Incorporating the suspendOperator extension alongside the ApiResponseSuspendOperator class allows you to efficiently manage suspension lambdas in conjunction with the operator pattern, promoting a more concise and maintainable approach within your codebase.

Global Operator

The global operator is undoubtedly a robust feature offered by Sandwich. It empowers you to operate on operators globally across all ApiResponse instances in your application by employing the SandwichInitializer. This way, you can avoid the necessity of creating operator instances for every API call or employing dependency injection for common operations. The following examples illustrate how to use a global operator to handle both ApiResponse.Failure.Error and ApiResponse.Failure.Exception scenarios. You can leverage the global operator to refresh your user token or implement any other additional processes necessary for specific API requests within your application. The example below demonstrates how you can automatically check and refresh the user token depending on the response status using Sandwich's global operator:

Initialize Global Operator

First, it's highly recommended to initialize the global operator in the Application class or using another initialization solution like App Startup. This ensures that the global operator is set up before any API requests are made.

class SandwichDemoApp : Application() {

  override fun onCreate() {
    super.onCreate()
    
    // We will handle only the error and exceptional cases,
    // so we don't need to mind the generic type of the operator.
    SandwichInitializer.sandwichOperators += listOf(TokenRefreshGlobalOperator<Any>(this))

    // ... //
  }
}

By configuring the global operator within SandwichInitializer, you enable your application to consistently process and handle various ApiResponse situations. This can include tasks such as managing success cases, handling errors, or dealing with exceptions, all on a global scale.

Implement Your Global Operator

Create your custom GlobalResponseOperator class that extends operators such as ApiResponseSuspendOperator and ApiResponseOperator. This operator will allow you to define common response handling logic that can be applied globally.

class TokenRefreshGlobalOperator<T> @Inject constructor(
  private val context: Context,
  private val authService: AuthService,
  private val userDataStore: UserDataStore,
  coroutineScope: CoroutineScope,
) : ApiResponseSuspendOperator<T>() {

  private var userToken: UserToken? = null

  init {
    coroutineScope.launch {
      userDataStore.tokenFlow.collect { token ->
        userToken = token
      }
    }
  }

  override suspend fun onError(apiResponse: ApiResponse.Failure.Error) {
    // verify whether the current request was previously issued as an authenticated request
    apiResponse.headers["Authorization"] ?: return

    // refresh an access token if the error response is Unauthorized or Forbidden
    when (apiResponse.statusCode) {
      StatusCode.Unauthorized, StatusCode.Forbidden -> {
        userToken?.let { token ->
          val result = authService.refreshToken(token)
          result.onSuccessSuspend { data ->
            userDataStore.updateToken(
              UserToken(
                accessToken = data.accessToken,
                refreshToken = data.refreshToken,
              ),
            )
            toast(R.string.toast_refresh_token_succeed)
          }.onFailureSuspend {
            toast(R.string.toast_refresh_token_failed)
          }
        }
      }
      else -> Unit
    }
  }

  override suspend fun onSuccess(apiResponse: ApiResponse.Success<T>) = Unit

  override suspend fun onException(apiResponse: ApiResponse.Failure.Exception) = Unit

  private suspend fun toast(@StringRes resource: Int) = withContext(Dispatchers.Main) {
    Toast.makeText(context, resource, Toast.LENGTH_SHORT).show()
  }
}

In this example, the global operator's onError function is used to automatically check for Unauthorized and Forbidden status code (HTTP 401 and 403) in the error response. If an unauthorized error occurs, the user token is refreshed, and the failed request is retried with the updated token using runAndRetry. This way, you can seamlessly manage token expiration and refresh for your API requests.

Global Operator With Hilt and App Startup

If you want to initialize the global operator by using with Hilt and App Startup, you can follow the instructions below.

1. Implement an Entry Point

First, you should implement an entry point for injecting the global operator into an App Startup initializer.

@EntryPoint
@InstallIn(SingletonComponent::class)
interface NetworkEntryPoint {

  fun inject(networkInitializer: NetworkInitializer)

  companion object {

    fun resolve(context: Context): NetworkEntryPoint {
      val appContext = context.applicationContext ?: throw IllegalStateException(
        "applicationContext was not found in NetworkEntryPoint",
      )
      return EntryPointAccessors.fromApplication(
        appContext,
        NetworkEntryPoint::class.java,
      )
    }
  }
}
2. Provide Global Operator Dependency

Next, provide your global operator with Hilt like the exambple below:

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

  @Provides
  @Singleton
  fun provideTokenRefreshGlobalOperator(
    @ApplicationContext context: Context,
    authService: AuthService,
    userDataStore: UserDataStore
  ): TokenRefreshGlobalOperator<Any> {
    return TokenRefreshGlobalOperator(
      context = context,
      authService = authService,
      userDataStore = userDataStore,
      coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
    )
  }
}
3. Implement App Startup Initializer

Finally, implement the App Startup Initializer and initialize the Initializer following the App Startup guidance.

public class NetworkInitializer : Initializer<Unit> {

  @set:Inject
  internal lateinit var tokenRefreshGlobalOperator: TokenRefreshGlobalOperator<Any>

  override fun create(context: Context) {
    NetworkEntryPoint.resolve(context).inject(this)

    SandwichInitializer.sandwichOperators += listOf(tokenRefreshGlobalOperator)
  }

  override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}

This setup allows you to define a retry policy that determines whether a retry attempt should occur and specifies the retry timeout. The runAndRetry extension then encapsulates the execution logic, applying the defined policy, and providing the response in a clean and structured manner.

Find this library useful? ❤️

Support it by joining stargazers for this repository. ⭐
And follow me for my next creations! 🤩

License

Copyright 2020 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.

sandwich's People

Contributors

5abhisheksaxena avatar chandroidx avatar danielpassos avatar goooler avatar johnjohndoe avatar renovate[bot] avatar rhkrwngud445 avatar skydoves avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sandwich's Issues

Do not working when requesting api using coroutine.

  • Library Version v1.0.5
  • Affected Device(s) Samsung Galaxy s10 with Android 10
var testLiveData: LiveData<String>? = null
init {
    testRequest()
}

fun testRequest() {
    Timber.w("testRequest")
    testLiveData = launchOnViewModelScope {
        this.loginRepository.test(
            onSuccess = {
                Timber.w("onSuccess")
            },
            onError = {
                Timber.w("onError : $it")
            }
        ).asLiveData()
    }
}

button.setOnClickListener {
    viewModel.testRequest()
}

init{} is working...
but button click is not working

what is problem??

Couldn't resolve ApiResponseCallAdapter class

사용 버전: implementation("com.github.skydoves:sandwich:2.0.6")

문제 현상:
retrofit의 addConverterFactory(ApiResponseCallAdapterFactory.create()) 코드를 추가하려고 했으나
ApiResponseCallAdapterFactory를 인식하지 못함

addConverterFactory 없이 코드를 실행시키면 아래 에러가 남
java.lang.IllegalArgumentException: Unable to create converter for com.skydoves.sandwich.ApiResponse

�dependency 추가된 파일을 보면 ApiResponseCallAdapterFactory 모듈인 refrofit이 존재하지 않음
image

라이브러리 사용을 잘못 한 것인지 아니면 이슈가 있는지 확인 부탁드립니다.

Difference Exception Handling between Ktor and Retrofit

For Retrofit, In ApiResponseCallDelegate.kt
The part that waits for the API response is within a try-catch block

  override fun enqueueImpl(callback: Callback<ApiResponse<T>>) {
    coroutineScope.launch {
      try {
        val response = proxy.awaitResponse()
        val apiResponse = ApiResponse.responseOf { response }
        callback.onResponse(this@ApiResponseCallDelegate, Response.success(apiResponse))
      } catch (e: Exception) {
        callback.onResponse(
          this@ApiResponseCallDelegate,
          Response.success(ApiResponse.exception(e)),
        )
      }
    }
  }

But for Ktor, In HttpClientExtension.kt
'response' already holds requested HttpResponse.
Therefore, within the try-catch block in 'apiResponseOf', it's not possible to handle errors occur during the request process
(e.g. UnknownHostException)

public suspend inline fun <reified T> HttpClient.getApiResponse(
  builder: HttpRequestBuilder,
): ApiResponse<T> {
  builder.method = HttpMethod.Get
  val response = request(builder)
  return apiResponseOf { response }
}

It seems more fitting for the purpose to pass the function itself as a parameter rather than receiving the result of the request

Why is this error not intercepted

Please complete the following information:

  •          sandwich:1.0.4
    
  • Affected Device(s) [e.g. Samsung Galaxy s10 with Android 9.0]
    compileSdkVersion: 30,
    buildToolsVersion: "29.0.3",
    Describe the Bug:
    retrofit2.HttpException: HTTP 400 Response.error()
    at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:53)
    at com.skydoves.sandwich.coroutines.ApiResponseCallDelegate$enqueueImpl$1.onResponse(ApiResponseCallDelegate.kt:33)
    at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:161)
    at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
    at java.lang.Thread.run(Thread.java:818)
    Add a clear description about the problem.

Expected Behavior:
net: <-- 400 http://******.com/auth.json (165ms)
net: Server: nginx/1.11.6
net: Date: Fri, 12 Nov 2021 01:20:42 GMT
net: Content-Type: application/json;char
net: Transfer-Encoding: chunked
net: X-Application-Context: ydt-api-gate
net: Connection: keep-alive
net: {"code":11015,"message":"无效的客户端"}

[Help] Is it the best approach to validate responses?

First, thanks for the fantastic lib!

I would like to double-check that I'm not over-complicating things 😅
A pretty typical use case on my project is validating some fields in the successful response that might indicate abnormal behavior (some of them the app might know to handle)

Inside our repositories, we usually check some properties, and if everything is correct, we then map the network model to the UI model, otherwise will return an error result class wrapping it:

suspend fun doSomething(): MyResultWrapper<MyUIModel> {
        // We could get some error here and it will be handled by the VM
        val location = settingsStore.getLocation().dataOrNull() ?: return ApiResponse.Failure.Exception(
            IllegalArgumentException("Location should not be null")
        )

        return service.doSomething(request = location.id).let { response ->
            when {
                response is ApiResponse.Success && isInvalid(response.data) ->
                    ApiResponse.error(IllegalArgumentException("Some meaningful message"))
                else -> response
            }
        }.mapSuccess {
            toDomainModel()
        }
    }

Is that the correct way to handle that or am I missing something?

Thanks!!

runAndRetry not working when type of ApiResponse had type variable

hello @skydoves, i had a case like this. the endpoint return the json with template like @dempsey. my problem is when I created the base class for the response it's working. but when I'm trying to use runAndRetry extension, it's show the warning :
Type mismatch. Required: Any Found: BaseResponse<TokenResponseModel>?

Screenshot 2023-12-01 135918

but when i change the body without the BaseResponse model, the 'runAndRetry ' method working well without the warning show up.

how do i solve this?

this is my base response model class
@Keep @Serializable data class BaseResponse<T>( @SerializedName("error") val error: ErrorModel? = null, @SerializedName("success") val success: Boolean?, @SerializedName("data") val data: T? = null )

Originally posted by @ldileh in #30 (comment)

After enable R8 full mode getting ParameterizedType error

Please complete the following information:

  • Library Version: 1.3.5

Describe the Bug:

This issue is similar to the issue in Retrofit (square/retrofit#3751) but I'm not sure, but I've duplicated it here. This happens with AGP 8.0. As a solution, it's proposed to add these proguard lines to the project (square/retrofit#3751 (comment)):

 # Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items). 
 -keep,allowobfuscation,allowshrinking interface retrofit2.Call 
 -keep,allowobfuscation,allowshrinking class retrofit2.Response 
  
 # With R8 full mode generic signatures are stripped for classes that are not 
 # kept. Suspend functions are wrapped in continuations where the type argument 
 # is used. 
 -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation 

but it doesn't actually work, After adding this proguard lines I get a slightly different error:

 java.lang.IllegalArgumentException: Unable to create call adapter for retrofit2.Call<com.skydoves.sandwich.ApiResponse>
                     for method ComicVineService.issues
                 	at retrofit2.Utils.methodError(SourceFile:47)
                 	at retrofit2.HttpServiceMethod.parseAnnotations(SourceFile:394)
                 	at retrofit2.Retrofit.loadServiceMethod(SourceFile:31)
                 	at retrofit2.Retrofit$1.invoke(SourceFile:45)
                 	at java.lang.reflect.Proxy.invoke(Proxy.java:1006)
                 	at $Proxy9.issues(Unknown Source)
                        ...
Caused by: java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType
                 	at com.skydoves.sandwich.adapters.ApiResponseCallAdapterFactory.get(SourceFile:53)
                 	at retrofit2.Retrofit.callAdapter(SourceFile:33)
                 	at retrofit2.HttpServiceMethod.parseAnnotations(SourceFile:375)
                 	... 41 more
 @GET("issues?format=$FORMAT&field_list=${Fields.Issues}")
    @JvmSuppressWildcards
    suspend fun issues(
        @Query("api_key") apiKey: String,
        @Query("offset") offset: Int,
        @Query("limit") limit: Int,
        @Query("sort", encoded = true) sort: ComicVineSort?,
        @Query("filter[]", encoded = true) filter: List<ComicVineFilter>?,
    ): ApiResponse<IssuesResponse>

Any Roadmap to Support Kotlin Multiplatform?

Is your feature request related to a problem?

No

Describe the solution you'd like:

Sample apps using KMM or KMP application with Hilt and ktor.

Describe alternatives you've considered:

Nil

crash on 401 unauthorized status

Please complete the following information:

  • Library Version [e.g. v1.0.0]
  • Affected Device(s) [e.g. Samsung Galaxy s10 with Android 9.0]

Describe the Bug:
when I send A user pass to my api which unauthorized and throws 401 status code. the apiresponse call adapter factory crashed.

--------- beginning of crash

2020-09-12 14:16:55.564 7771-7771/com.skydoves.pokedex E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.skydoves.pokedex, PID: 7771
retrofit2.HttpException: HTTP 401 Response.error()
at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:53)
at com.skydoves.sandwich.coroutines.ApiResponseCallDelegate$enqueueImpl$1.onResponse(ApiResponseCallDelegate.kt:33)
at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:161)
at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:504)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
Add a clear description about the problem.

Expected Behavior:

A clear description of what you expected to happen.

onError expected to be fired

How to retry request?

in readme section about coroutine, it will call like this

  init {
    posterListLiveData = liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
      emitSource(disneyService.fetchDisneyPosterList()
        .onSuccess {
          // stub success case
          livedata.post(response.data)
        }.onError {
          // stub error case
        }.onException {
          // stub exception case
        }.toLiveData()) // returns an observable LiveData
    }
  }

when onError is trigger, i show a snackbar, and have button retry, but how to retry the request again?

[ASK] How to create sequential or parallel request?

right now i'm using CoroutinesResponseCallAdapterFactory, how to create sequential request and parallel request with sandwich?
Example for sequential = i have API for getConfig and getBanner, frist getConfig, when succeed it will call getBanner
Example for parallel = API getProductDetail, getUserLimit. both API will parralel then result of 2 api will combine into 1 result

thank you

[ASK] Is there any feature that is equivalent to Single in sandwich?

I need to call sequential APIs. Is there any way to make it simple?

It's maybe related to 'map' feature or this one

    suspend fun reCheckout(
        orderData: OrderData
    ) = flow {
        loginAndRegister(orderData.phoneNumber).suspendOnSuccess {
            val jwt = "jwt ${data.auth.token}"
            Client.saveAuth(data)
            cancelOrder(
                orderData.orderId,
                OrderCancelRequestBody("store", "re-checkout")
            ).suspendOnSuccess {
                addMultiCartItem(jwt, orderData.cartItems).suspendOnSuccess {
                    checkOut(orderData.checkoutBody!!).suspendOnSuccess {
                        emit(this.data)
                    }.suspendOnError {
                        emit(this)
                    }.suspendOnException {
                        emit(this)
                    }
                }.suspendOnError {
                    emit(this)
                }.suspendOnException {
                    emit(this)
                }
            }.suspendOnError {
                emit(this)
            }.suspendOnException {
                emit(this)
            }
        }.suspendOnError {
            emit(this)
        }.suspendOnException {
            emit(this)
        }
    }

I don't know if it's the right way or not. I'd like to have only one onError and onException here like Single in RxJava. Is that possible?

I also want to handle the data in ViewModel with onSuccess, onException, onError, etc. How can I implement it?

EmptyBodyInterceptor doesn't work with GsonConverterFactory

Please complete the following information:

  • Library Version v1.3.4

Describe the Bug:
When usingEmptyBodyInterceptor to enable nullable bodies I get an exception

OFException: End of input at line 1 column 1 path $
    at com.google.gson.stream.JsonReader.nextNonWhitespace(JsonReader.java:1421)
    at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:549)
    at com.google.gson.stream.JsonReader.peek(JsonReader.java:425)
    at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:207)
    at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:42)
...

Issue is caused by GsonConverterFactory because "" is not a valid Json.
google/gson#330

Expected Behavior:
No exception is thrown when using EmptyBodyInterceptor with GsonConverterFactory

Crash on HTTP 403 when api type is Call<T>

Please complete the following information:

  • Library Version: v1.0.8
  • Affected Device(s): Pixel 3a with Android 11

Describe the Bug:

The problem looks similar with crash on 401 unauthorized status #5

Crash log:

2021-01-04 18:14:25.391 14244-14534/net.hikingbook.hikingbook E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-2
    Process: net.hikingbook.hikingbook, PID: 14244
    retrofit2.HttpException: HTTP 403 
        at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:53)
        at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:161)
        at com.google.firebase.perf.network.InstrumentOkHttpEnqueueCallback.onResponse(InstrumentOkHttpEnqueueCallback.java:69)
        at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:923)

Here is my code. I try to use the Call to get the response.

Service:

@POST("/login")
    suspend fun postLogin(
            @Body body: RequestBody
    ): Call<User>

Repository:

   suspend fun login(email: String, password: String) {
        val params: MutableMap<String, String> = mutableMapOf(
                "email" to email,
                "password" to password
        )
        router?.let {
            val request = it.postLogin(body = createRequestBody(params))
            request.callback(
                    onSuccess = { user ->
                        handleLoginResponse(user = user)
                    },
                    onError = {}
            )
        }
    }

   suspend fun <T> Call<T>.callback(
            onSuccess: (T?) -> Unit,
            onError: (String) -> Unit
    ) {
        this.request { response ->
            runBlocking {
                response.suspendOnSuccess {
                    onSuccess(data)
                }.suspendOnError {
                    when (statusCode) {
                        StatusCode.Forbidden -> {
                            // do somting...
                        }
                        else -> {
                            onError(message())
                        }
                    }
                }.suspendOnException {
                    onError(message())
                }
            }
        }
    }

Expected Behavior:
If the api is type is ApiResponse then the suspendOnError() method can handle the 403. But Call cannot handle
by onError.
Am I do something wrong? Thanks~

How to get custom error from my server?

When i call api to my server and handle error, i want to get my custom error message like this
}
"statusCode": 400,
"message": "Registered phone number"
}

But when i use sandwich lib, error message convert to [ApiResponse.Failure.Error-BadRequest](errorResponse=Response{protocol=http/1.1, code=400, message=Bad Request
I just want to get my message error "Registered phone number"

deserializeErrorBody() throws internal exceptions

Please complete the following information:

  • Library Version 2.0.5
  • Xiaomi 11T Pro

Describe the Bug:

deserializeErrorBody() extenstion throws all internal exceptions, for example in case of any decoding-specific error - SerializationException if @serializable data class does not match with backend answer.

Expected Behavior:

deserializeErrorBody returns null if the error body is empty. Maybe add try catch on internal json.decodeFromString() to catch serialization exceptions and return null in case of this exception.
I can create pull request with such implementation

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

github-actions
.github/workflows/build.yml
  • actions/checkout v4
  • actions/setup-java v4
  • actions/checkout v4
  • actions/setup-java v4
  • actions/checkout v4
  • actions/setup-java v4
  • actions/cache v4
.github/workflows/publish-docs.yml
  • actions/checkout v4@0ad4b8fadaa221de15dcec353f45205ec38ea70b
  • actions/setup-python v5
  • actions/cache v4
.github/workflows/publish-snapshot.yml
  • actions/checkout v4.1.4@0ad4b8fadaa221de15dcec353f45205ec38ea70b
  • actions/setup-java v4.2.1
.github/workflows/publish.yml
  • actions/checkout v4.1.4@0ad4b8fadaa221de15dcec353f45205ec38ea70b
  • actions/setup-java v4.2.1
gradle
buildSrc/src/main/kotlin/com/github/skydoves/sandwich/Configuration.kt
gradle.properties
settings.gradle.kts
build.gradle.kts
app/build.gradle.kts
  • androidx.multidex:multidex 2.0.1
baselineprofile/build.gradle.kts
buildSrc/build.gradle.kts
gradle/libs.versions.toml
  • androidx.appcompat:appcompat 1.6.1
  • com.google.android.material:material 1.12.0
  • com.squareup.retrofit2:retrofit 2.11.0
  • com.squareup.retrofit2:converter-moshi 2.11.0
  • com.squareup.okhttp3:okhttp 4.12.0
  • com.squareup.okio:okio 3.9.0
  • io.ktor:ktor-client-core 2.3.11
  • io.ktor:ktor-client-content-negotiation 2.3.11
  • io.ktor:ktor-serialization-kotlinx-json 2.3.11
  • io.ktor:ktor-client-okhttp 2.3.11
  • io.ktor:ktor-client-darwin 2.3.11
  • de.jensklingenberg.ktorfit:ktorfit-lib 1.13.0
  • de.jensklingenberg.ktorfit:ktorfit-lib-light 1.13.0
  • de.jensklingenberg.ktorfit:ktorfit-ksp 1.13.0
  • org.jetbrains.kotlinx:kotlinx-coroutines-core 1.8.0
  • org.jetbrains.kotlinx:kotlinx-serialization-json 1.6.3
  • com.squareup.moshi:moshi-kotlin 1.15.1
  • com.squareup.moshi:moshi-kotlin-codegen 1.15.1
  • com.github.bumptech.glide:glide 4.16.0
  • androidx.lifecycle:lifecycle-livedata-ktx 2.7.0
  • androidx.lifecycle:lifecycle-viewmodel-ktx 2.7.0
  • com.jakewharton.timber:timber 5.0.1
  • androidx.arch.core:core-testing 2.2.0
  • org.mockito:mockito-core 5.11.0
  • org.mockito:mockito-inline 5.11.0
  • com.nhaarman.mockitokotlin2:mockito-kotlin 2.2.0
  • com.squareup.okhttp3:mockwebserver 4.12.0
  • org.jetbrains.kotlinx:kotlinx-coroutines-test 1.8.0
  • junit:junit 4.13.2
  • androidx.benchmark:benchmark-macro-junit4 1.2.4
  • androidx.test:runner 1.5.2
  • androidx.test:rules 1.5.2
  • androidx.test.ext:junit-ktx 1.1.5
  • androidx.profileinstaller:profileinstaller 1.3.1
  • androidx.test.uiautomator:uiautomator 2.3.0
  • androidx.core:core-ktx 1.13.1
  • androidx.test.ext:junit 1.1.5
  • androidx.test.espresso:espresso-core 3.5.1
  • com.android.application 8.4.0
  • com.android.library 8.4.0
  • org.jetbrains.kotlin.android 1.9.24
  • org.jetbrains.kotlin.multiplatform 1.9.24
  • org.jetbrains.kotlin.plugin.serialization 1.9.24
  • org.jetbrains.kotlin.kapt 1.9.24
  • com.google.devtools.ksp 1.9.24-1.0.20
  • de.jensklingenberg.ktorfit 1.13.0
  • org.jetbrains.dokka 1.9.20
  • androidx.baselineprofile 1.2.4
  • com.diffplug.spotless 6.25.0
  • org.jetbrains.kotlinx.binary-compatibility-validator 0.14.0
  • com.android.test 8.4.0
sandwich/gradle.properties
sandwich/build.gradle.kts
sandwich-ktor/gradle.properties
sandwich-ktor/build.gradle.kts
sandwich-ktorfit/gradle.properties
sandwich-ktorfit/build.gradle.kts
sandwich-retrofit/build.gradle.kts
sandwich-retrofit-datasource/build.gradle.kts
sandwich-retrofit-serialization/build.gradle.kts
scripts/publish-module.gradle.kts
gradle-wrapper
gradle/wrapper/gradle-wrapper.properties
  • gradle 8.7

  • Check this box to trigger a request for Renovate to run again on this repository

java.lang.RuntimeException: Failed to invoke private com.skydoves.sandwich.ApiResponse() with no args

Please complete the following information:

  • Library Version 1.2.1
  • Affected Device(s) Pixel 4

Describe the Bug:
image
response that used Sandwich always failed, with exception like above

Expected Behavior:
Response should be success because implementation without Sandwich had no problem

Implementation:

@GET("api/v3/rekening/detail")
    suspend fun getRekeningDetail(
        @Header("Authorization") auth: String,
        @Query("id") id: String,
        @Query("type") type: String
    ): ApiResponse<ResponseResult<Rekening>>


data class ResponseResult<T>(
    @field:SerializedName("data")
    val data: T? = null,

    @field:SerializedName("message")
    val message: String? = null,

    @field:SerializedName("status")
    val status: Int? = null
)

fun getRekeningById(rekeningId: String, rekeningType: RekeningType) = flow {
        emit(State.Single.Loading())
        var rekeningData: Rekening? = null
        if (NetworkUtils.isConnected()) {
            val response =
                service.getRekeningDetail(
                    mSecuredPreferences.accessToken,
                    rekeningId,
                    rekeningType.value
                )
            response.suspendOnSuccess {
                dao.insertSyncData(listOf(data.data))
                rekeningData = dao.getRekeningById(rekeningId)
            }.suspendOnError {
                rekeningData = dao.getRekeningById(rekeningId)
            }.suspendOnException {
                rekeningData = dao.getRekeningById(rekeningId)
            }
        } else {
            rekeningData = dao.getRekeningById(rekeningId)
        }
        emit(State.Single.Success(rekeningData))
    }.flowOn(Dispatchers.IO)

Issue with the ApiResponse.Failure<*>

I have the following sample code and have my internet turned off. So an unresolved host is thrown

  val loginResponse = authService.login(loginRequest)

            loginResponse.onFailure {
                Timber.d("This Failed")
            }
            Timber.d("loginResponse is Failure ${loginResponse is ApiResponse.Failure<*>}}")
            Timber.d("loginResponse is Exception ${loginResponse is ApiResponse.Failure.Exception<*>}}")

Based off the comments and the types in onFailure, both these timber log statements should be true and This Failed should print. Since the comments for onFailure say "A function that would be executed for handling error responses if the request failed or get an exception."

Global ApiResponseFailureMapper not returning desired results

First of all, thank you for creating this awesome library :)

Please complete the following information:

  • Library Version 2.0.5

Describe the Bug:
The library allows us to create custom error mappers and assign them globally, but because of the library internals the mapping result is not really returned to the app. Example below

Some dummy classes for failures:

class SandwichException(throwable: Throwable) : ApiResponse.Failure.Exception(throwable)
class SandwichError(payload: Any?) : ApiResponse.Failure.Error(payload)

and a dummy global mapper:

class ApiFailureMapper : ApiResponseFailureMapper {

    override fun map(apiResponse: ApiResponse.Failure<*>): ApiResponse.Failure<*> {
        return when (apiResponse) {
            is ApiResponse.Failure.Exception -> SandwichException(apiResponse.throwable)

            is ApiResponse.Failure.Error -> SandwichError(apiResponse.payload)
        }
    }
}

SandwichInitializer.sandwichFailureMappers.add(ApiFailureMapper())

Now when we face an exception, like IOExceptions, we can expect an instance of SandwitchException with IOException throwable, right? But instead we receive the original ApiResponse.Failure.Exception.

I did some investigation and I belive the error is caused by

public fun exception(ex: Throwable): Failure.Exception =
Failure.Exception(ex).apply { operate().maps() }

because apply will trigger lambda (operators and mappers) as expected, but the result is not assigned anywhere and the original Failure.Exception is returned.

Expected Behavior:

When using global operators and mappers the app should properly return the modified Failure.Exception instances

Change parameter for map in ApiSuccessModelMapper

Sandwich exposes an interfaceApiSuccessModelMapper. The map parameter method should be named apiSuccessResponse instead of apiErrorResponse

package com.skydoves.sandwich

/**
 * @author skydoves (Jaewoong Eum)
 *
 * A mapper interface for mapping [ApiResponse.Success] response as a custom [V] instance model.
 *
 * @see [ApiSuccessModelMapper](https://github.com/skydoves/sandwich#apierrormodelmapper)
 */
public fun interface ApiSuccessModelMapper<T, V> {

  /**
   * maps the [ApiResponse.Success] to the [V] using the mapper.
   *
   * @param apiErrorResponse The [ApiResponse.Success] error response from the network request.
   * @return A custom [V] success response model.
   */
  public fun map(apiErrorResponse: ApiResponse.Success<T>): V
}

Describe the solution you'd like:

Rename map parameter apiErrorResponse into apiSuccessResponse.

Why is this error not intercepted

Please complete the following information:

  •          sandwich:1.0.4
    
  • Affected Device(s) [e.g. Samsung Galaxy s10 with Android 9.0]
    compileSdkVersion: 30,
    buildToolsVersion: "29.0.3",
    Describe the Bug:
    retrofit2.HttpException: HTTP 400 Response.error()
    at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:53)
    at com.skydoves.sandwich.coroutines.ApiResponseCallDelegate$enqueueImpl$1.onResponse(ApiResponseCallDelegate.kt:33)
    at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:161)
    at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
    at java.lang.Thread.run(Thread.java:818)
    Add a clear description about the problem.

Expected Behavior:
net: <-- 400 http://******.com/auth.json (165ms)
net: Server: nginx/1.11.6
net: Date: Fri, 12 Nov 2021 01:20:42 GMT
net: Content-Type: application/json;char
net: Transfer-Encoding: chunked
net: Connection: keep-alive
net: {"code":11015,"message":"无效的客户端"}

Can I use Sandwich with Mongodb Realm API

Is your feature request related to a problem?

I can't find any references and examples on how to use Sandwich with Mongodb Realm API access.

Describe the solution you'd like:

Documentation about if Sandwich is only available for HTTP and REST API. If applicable please provide instruction on how to use sandwich to connect Realm API with samples.

Describe alternatives you've considered:

Manual exception control or backend server with Ktor(which I want to avoid)

java.lang.NullPointerException:::sandwich.ApiResponse$Failure$Error.toString

--sandwich 1.3.5
--okhttp 4.10.0
--okio-jvm 3.0.0
--Xiao Mi Note3 (Android 9,API 28)

java.lang.NullPointerException
at okio.Segment.pop(Segment.kt:95)
at okio.Buffer.readString(Buffer.kt:321)
at okio.Buffer.readString(Buffer.kt:302)
at okhttp3.ResponseBody.string(ResponseBody.kt:187)
at com.skydoves.sandwich.ApiResponse$Failure$Error.toString(ApiResponse.kt:82)
at com.skydoves.sandwich.ResponseTransformer__ResponseTransformerKt.message(ResponseTransformer.kt:759)
at com.skydoves.sandwich.ResponseTransformer.message(ResponseTransformer.kt:1)
at com.robertsi.httpresultcope.mapper.ErrorResponseMapper.map(ErrorResponseMapper.kt:37)
at com.robertsi.httpresultcope.mapper.ErrorResponseMapper.map(ErrorResponseMapper.kt:28)
at com.robertsi.httpresultcope.repository.HttpCommonRepository$getManual$1.invokeSuspend(HttpCommonRepository.kt:197)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.internal.LimitedDispatcher.run(LimitedDispatcher.kt:42)
at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:95)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@7db45e5, Dispatchers.IO]


Thank you for your open source Sandwich Project
I am currently using and found this error. I hope you can capture and handle this exception

Not nullable body on success

In ApiResponse.Success the data property is nullable. And i understand why it is - it's just a wrapper around retrofit response logic. But, when i'm using clean suspend fun something(): Type i will get an exception if the returned type is null. Which is good, i do expect that!

If i wanted null to be possible in my response - i would use a nullable type as a generic parameter. If i don't - my assumption is that in case of success it will always be properly created. The need for another null check does make the whole Success type pointless in my opinion. Because i'm still not sure if it really was a success, i have to do another check..

Have you considered handling that?

[ASK] How can I handle 200 but error case?

Some part of my app use Google Spreadsheet.

And I implemented REST API there with Apps Script.

I think this is rare case but, google server gives me 200 with html code when it has server error.

So, what I am trying is getting the response with ApiResponse. And then try to convert the response to SpreadSheetResponseBody which is a data class when it's successful.

This is my concept.

    suspend fun updateDashboard(
        ssId: String,
        method: String,
        device: String,
        isTest: Boolean,
        request: KioskDashboardReqBody
    ): ApiResponse<SpreadSheetResponseBody>{
        val result = service.updateDashboard(ssId, method, device, isTest, request).onSuccess {
            try {
                val data = Gson().fromJson(this.data, SpreadSheetResponseBody::class.java)
                return ApiResponse.Success<SpreadSheetResponseBody>(data)
            }catch(e: Exception){
                return ApiResponse.Failure<SpreadSheetResponseBody>(e)
            }
        }
        return ApiResponse.Failure<SpreadSheetResponseBody>("Server Error")
    }

This code is invalid.
Since google gives me 200. I'd like to create ApiResponse instead in the Repository.

Is this possible to handling like this?

How to get error message when onError

    data class Error<T>(val response: Response<T>) : ApiResponse<T>() {
      val statusCode: StatusCode = getStatusCodeFromResponse(response)
      val headers: Headers = response.headers()
      val raw: okhttp3.Response = response.raw()
      val errorBody: ResponseBody? = response.errorBody()
      override fun toString(): String = "[ApiResponse.Failure.Error-$statusCode](errorResponse=$response)"
    }

how to get error message? because when i used apiResponse.message() it will return like in toString() function. what i want just message only. thank you

Interface can't be instantiated! Register an InstanceCreator or a TypeAdapter for this type.

  • Library Version: 2.0.0
  • Tested on Samsung galaxy S21 FE 5G and Pixel 7 Pro API 33 emulator

Add a clear description about the problem.
In my retrofit service interface I have a get API call

@GET("/getSomeData/") suspend fun getData(): ApiResponse<List<<MyDataModel>>>
when I run the app after some time the app gets crashed with the below logs:

if I use Moshi Converter Factory -
java.lang.illegalArgumentException: Unable to create converter for com.skydoves.sandwich.ApiResponse<? extends java.util.List<com.packagename.MyDataModel>>

if I use Gson Converter Factory -
com.google.gson.JsonIOException: Interfaces can't be instantiated! Register an InstanceCreator or a TypeAdapter for this type. Interface name: com.skydoves.sandwich.ApiResponse

Expected Behavior:
The app should not crash.

Cannot inline bytecode built with JVM target 11 into bytecode that is being built with JVM target 1.8. Please specify proper '-jvm-target' option

  • Library Version 1.3.9
  • Affected Device(s) [e.g. Samsung Galaxy s10 with Android 9.0]

Describe the Bug:
Cannot inline bytecode built with JVM target 11 into bytecode that is being built with JVM target 1.8. Please specify proper '-jvm-target' option

My gradle.build has the following
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}

the gradle JDK version is 17

Add a clear description about the problem.

Support usage in vanilla JVM projects

Is your feature request related to a problem?

I wanted to experiment with sandwich in a backend project, so a regular Kotlin/JVM project, not an Android one. Using the dependency declaration in the build file described in the readme results in resolve errors of the like

Execution failed for task ':common:compileKotlin'.
Error while evaluating property 'filteredArgumentsMap' of task ':common:compileKotlin'
Could not resolve all files for configuration ':common:compileClasspath'.
> Could not resolve com.github.skydoves:sandwich:1.2.7.
Required by:
project :common
> No matching variant of com.github.skydoves:sandwich:1.2.7 was found. The consumer was configured to find an API of a library compatible with Java 11, preferably in the form of class files, preferably optimized for standard JVMs, and its dependencies declared externally, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'jvm' but:
- Variant 'releaseApiElements-published' capability com.github.skydoves:sandwich:1.2.7 declares an API of a library:
- Incompatible because this component declares a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm' and the consumer needed a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'jvm'

Describe the solution you'd like:

I would like to be able to resolve a variant of sandwich for a vanilla Kotlin/JVM project. I expect that the variant also has only the dependencies that are necessary for that variant - for example no dependency on android libraries or utilities.

Describe alternatives you've considered:

I found a closed issue: #4 where the user chose that moving everything to the android module is an alternative. However, I don't have an android module, so I don't see any alternatives.

Thanks for the great project!
Hannes

Unresolved reference when adding dependency to a Kotlin module

Hi @skydoves, and thanks for this nice library

When adding this dependency to the build.gradle file of an Android module, it works fine.
But doing the same in the build.gradle of a pure Kotlin module, I'm not able to see the library (Unresolved reference). The IDE suggests "Add library 'Gradle: com.github.skydoves:sandwich:1.0.4@aar' to classpath"; and even doing that doesn't solve the problem.
This is my build.gradle file:

apply plugin: 'kotlin'

dependencies {
    // some other dependencies
    implementation "com.github.skydoves:sandwich:1.0.4"
}

Could you explain how to handle this issue?
Thanks

On Failure should return the Failure Sealed class

Since 1.2.2 on failure returns the actual message for example

[ApiResponse.Failure.Error-BadRequest](errorResponse=Response{protocol=h2, code=400, message=, url=urlItriedtodosomethingWith})

It would be nice if this was the actual sealed class instead of the message. This would allow a user to just use onFailure and check the Exception/Error types themselves or do whatever with the actual sealed object. As it stands currently you lose information if it was an error, and logging from this string requires a bunch of parsing.

For example I have an extension function that logs based off the ApiResponse<*> type.

fun ApiResponse<*>.log(type: String) {
    when (this) {
        is ApiResponse.Failure.Exception -> {
            XLog.enableStackTrace(10).e("Exception $type ${this.message}", this.exception)
        }
        is ApiResponse.Failure.Error -> {
            XLog.e("error $type ${this.errorBody?.string()}")
            XLog.e("error response code ${this.statusCode.code}")
        }
        else -> {
            XLog.e("error $type")
        }
    }
}

I would like to do something like

  val loginResponseDto = authService.login(loginRequest).onFailure { 
                this.log("Trying to login")
            }

but currently have to do something like this

  val loginResponseDto = authService.login(loginRequest).onError{ 
                this.log("Trying to login")
            }.onException{ 
                this.log("Trying to login")
            }

ApiResponseCallAdapterFactory is missing in 2.0.4

2.0.4
Android 12

Can't import ApiResponseCallAdapterFactory

import com.skydoves.sandwich.adapters.ApiResponseCallAdapterFactory
...
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) 

image

image

Parse non-standard json

Hello, how should I parse the data format like this [code:Int,data:T,error:String] through ApiResponse

[ASK] How can I get throw exception as onError/onFailure/onException in ViewModel ?

This is related to this question

In the Repository.

    suspend fun reCheckout(
        orderData: OrderData
    ) = flow{
        var jwt: String? = null

        loginAndRegister(orderData.phoneNumber)
            .suspendOnFailure { emit(this) }
            .suspendMapSuccess {
                val authData = this
                jwt = "jwt ${authData.auth.token}"
                Client.saveAuth(authData)
                cancelOrder(
                    orderData.orderId!!,
                    OrderCancelRequestBody("store", "re-checkout")
                ).getOrThrow()
            }
            .suspendOnFailure { emit(this) }
            .suspendMapSuccess { addMultiCartItem(jwt!!, orderData.multiCartBody!!).getOrThrow() }
            .suspendOnFailure { emit(this) }
            .suspendMapSuccess { checkOut(orderData.checkoutBody!!).getOrThrow() }
            .suspendOnFailure { emit(this) }
            .suspendOnSuccess { emit(this) }
    }

And

In the ViewModel that calls the Repository.

        repository.reCheckout(orderData!!).collectLatest { apiResponse ->
            apiResponse.onSuccess {
                try {
                    val checkoutResponse = data as CheckoutResponseBody
                    orderData!!.updateCheckoutResult(checkoutResponse)
                    _uiState.value = PaymentUiState.Success(orderData!!)
                }catch (e: Exception){
                    e.printStackTrace()
                    sendLog(e.stackTraceToString())
                }
            }.onError {
                sendLog("${this.errorBody?.string()}")
            }.onException {
                sendLog("$exception")
            }
        }

I am using in this way. And when one of the API calls gets error, it throws because it usesgetOrThrow().

public fun <T> ApiResponse<T>.getOrThrow(): T {
  when (this) {
    is ApiResponse.Success -> return data
    is ApiResponse.Failure.Error -> throw RuntimeException(message())
    is ApiResponse.Failure.Exception -> throw exception
  }
}

But I think that would be nice if I can get the response because checking the status code is important to check the server error.

I thought I could get the exception in the ViewModel. Is there anyway to approach this..?

So, I tried to do like this. But it's not working.

try {
    cancelOrder(
        orderData.orderId!!,
        OrderCancelRequestBody("store", "re-checkout")
    ).getOrThrow()
}catch (e: Exception){
    val result = Response.error(500, e.message?.toResponseBody(null))
    emit(ApiResponse.of { result })
}

I also need to dismiss like loading animation or something too.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.