0

i'm building a multi-module jetpack-compose login/signup app. My backend is handled using Spring boot and the login/signup function returns a ResponseEntity.

The task: in my frontend i want to display different messages based on type of error. Current solution: i Have a network module where i use Retrofit to call backend. This is the files in netwrok module.

interface AuthApi {
@POST("/auth/register")
suspend fun register(
    @Body request: RegisterRequest,
)

@POST("/auth/login")
suspend fun login(
    @Body request: LoginRequest,
): TokenPair

@GET("authenticate")
suspend fun authenticate(
    @Header("Authorization") token: String,
)
}

@Module
@InstallIn(SingletonComponent::class)
internal object NetworkModule {
    @Provides
    @Singleton
    fun provideAuthApi(
        okhttpCallFactory: dagger.Lazy<Call.Factory>,
     ): AuthApi {
        return Retrofit.Builder()
           .baseUrl(SPOTIFY_BASE_URL)
           .callFactory { okhttpCallFactory.get().newCall(it) }
           .addConverterFactory(GsonConverterFactory.create())
           .build()
           .create(AuthApi::class.java)
     }

Then this is my data module.

class AuthRepositoryImpl
@Inject
constructor(
    private val api: AuthApi,
) : AuthRepository {


override suspend fun signIn(email: String, password: String): Result<TokenPair, DataError.Network> {
    return try {
        val user = api.login(LoginRequest(email, password))
        Result.Success(user)
    } catch (e: HttpException) {
        when (e.code()) {
            400 -> Result.Error(DataError.Network.BAD_REQUEST)
            401 -> Result.Error(DataError.Network.UNAUTHORIZED)
            409 -> Result.Error(DataError.Network.CONFLICT)
            else -> Result.Error(DataError.Network.INTERNAL_SERVER_ERROR)
        }
    }
}

sealed interface DataError : Error {
    enum class Network : DataError {
       BAD_REQUEST,
       CONFLICT,
       UNAUTHORIZED,
       INTERNAL_SERVER_ERROR,
    }
}

sealed interface Result<out D, out E : RootError> {
    data class Success<out D, out E : RootError>(val data: D) : Result<D, E>
    data class Error<out D, out E : RootError>(val error: E) : Result<D, E>
}

My problem is that in data layer i'm using HttpException which come from Retrofit (i add retrofit core dependency in data module too as well as network module).which means i'm not benefiting the idea of multi-module.

1 Answer 1

0

I use Ktor in my app, but the solution should apply similarly to Retrofit too.

In a common module, I define a dedicated exception to represent HTTP-based failures:

class HttpException(val errorCode: Int, val description: String? = null) : NetworkException("HTTP error: $errorCode - Description:$description")

NetworkException is just a wrapper over IOException, allowing to group all network-related issues:

abstract class NetworkException(message: String? = null, cause: Throwable? = null) : IOException(message, cause)

Then, I model network responses using a sealed interface (similar to your Result):

sealed interface NetworkResponse<out T> {
    data class Success<T>(val data: T) : NetworkResponse<T>
    data class Error(val throwable: NetworkException) : NetworkResponse<Nothing>
}

Here's the helper to safely execute network calls and map exceptions:

internal suspend inline fun <reified T> safeNetworkCallNetworkResponse(
    crossinline call: suspend () -> T
): NetworkResponse<T> {
    return runCatchingNonCancellation { call() }
        .fold(
            onSuccess = { data ->
                NetworkResponse.Success(data)
            },
            onFailure = { error ->
                AppLog.error("Network call failure", error)
                NetworkResponse.Error(error.toNetworkException())
            }
        )
}

The toNetworkException function maps different Throwable types into your domain-specific exceptions:

fun Throwable.toNetworkException(): NetworkException {
    return when (this) {
        // ... Here is your mapping where map the api HttpException to your core module HttpException (and other mapping if required)
    }
}

Tip: I use runCatchingNonCancellation, which skips catching CancellationException. This ensures coroutines are properly cancelled when for cooperative cancellation behavior.
This method can be implemented like this:


inline fun <R> runCatchingNonCancellation(block: () -> R): Result<R> {
    return try {
        Result.success(block())
    } catch (e: Throwable) {
        if (e is CancellationException) throw e
        Result.failure(e)
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

thanks for you time, but from where i get runCatchingNonCancellation. and in which package i should put safeNetworkCallNetworkResponse and oNetworkException()
safeNetworkCallNetworkResponse and toNetworkException should be at the network module. And both can have internal visibility modifier. The runCatchingNonCancellable is own method. And it should be in the common module

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.