Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fe62a0a1 authored by Nishant D's avatar Nishant D
Browse files

Retrofit generic implementation

parent a11ec944
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -9,6 +9,7 @@ ext {
    core_version = '1.10.1'
    gson_version = '2.9.0'
    kotlin_reflection = '1.8.10'
    retrofit_version = '2.9.0'
}

android {
@@ -44,6 +45,12 @@ dependencies {
    implementation "com.google.code.gson:gson:$gson_version"
    implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_reflection"

    // region Retrofit
    implementation("com.squareup.retrofit2:retrofit:$retrofit_version")
    implementation("com.squareup.retrofit2:converter-gson:$retrofit_version")
    implementation("com.squareup.retrofit2:converter-scalars:$retrofit_version")
    // endregion

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
+50 −0
Original line number Diff line number Diff line
package app.lounge.networking


//region Generic Network Error Types

sealed interface FetchError {
    data class Network(val underlyingError: AnyFetchError) : FetchError
}

/** Supertype for network error types. */
sealed interface AnyFetchError {

    var description: String

    enum class BadRequest(override var description: String) : AnyFetchError {
        Encode("FIXME: Error encoding request! $dumpKeyWord"),
        Decode("FIXME: Error decoding request! $dumpKeyWord"),
    }

    enum class NotFound(override var description: String) : AnyFetchError {
        MissingData("No data found! $dumpKeyWord"),
        MissingNetwork("No network! $dumpKeyWord")
    }

    data class BadStatusCode (val statusCode: Int, val rawResponse: Any) : AnyFetchError {
        override var description: String =
            "Bad status code: $statusCode. Raw response: $rawResponse"
    }

    /**
     * Represents a vague error case typically caused by `UnknownHostException`.
     * This error case is encountered if and only if network status cannot be determined
     * while the `UnknownHostException` is received.
     */
    data class Unknown(
        override var description: String = "Unknown Error! $dumpKeyWord"
    ) : AnyFetchError

    companion object {
        const val dumpKeyWord: String = "dump:-"

        fun make(error: AnyFetchError, addingDump: String) : AnyFetchError {
            error.description = error.description + addingDump
            return error
        }
    }

}

//endregion
+29 −0
Original line number Diff line number Diff line
package app.lounge.networking

import okhttp3.OkHttpClient
import retrofit2.Retrofit
import java.util.concurrent.TimeUnit

/**
 * Implement retrofit configuration
 * 1. Try to use single instance of configuration
 * 2. Generic way to handle success or failure
 * 3. API parsing can be clean and testable
 *
 * NOTE: Try to use naming which define the action for the logic.
 * */

internal fun Retrofit.Builder.appLounge(
    baseURL: String,
    shouldFollowRedirects: Boolean,
    callTimeoutInSeconds: Long,
) : Retrofit {
    return this.baseUrl(baseURL)
        .client(
            OkHttpClient.Builder()
                .callTimeout(callTimeoutInSeconds, TimeUnit.SECONDS)
                .followRedirects(shouldFollowRedirects)
                .build()
        )
        .build()
}
 No newline at end of file
+201 −0
Original line number Diff line number Diff line
package app.lounge.networking

import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.lang.IllegalStateException
import java.net.UnknownHostException


//region Retrofit Asynchronous Networking
interface RetrofitFetching {
    val executor: Executor get() = callEnqueue

    val checkNetwork: (() -> Boolean)? get() = null

    interface Executor {
        fun <R> fetchAndCallback(endpoint: Call<R>, callback: Callback<R>)
    }

    /**
     * An object that receives Retrofit `Callback<T>` arguments and determines whether or not
     * an error should be returned. Use to return specific error cases for a given network request.
     */
    interface ResultProcessing<R, E> {
        /**
         * Return `R` if possible to cast given object to `R` or return `null`.
         * This lambda is required due to jvm's type-erasing for generic types.
         */
        val tryCastResponseBody: (Any?) -> R?

        /** Return result, success/failure, for the received request response */
        val resultFromResponse: (call: Call<R>, response: Response<R>) -> RetrofitResult<R, E>

        /** Return error object `E` for the given failed request. */
        val errorFromFailureResponse: (call: Call<*>, t: Throwable) -> E

        /** Return error object `E` that contains/represents the given `AnyFetchError` */
        val errorFromNetworkFailure: (AnyFetchError) -> E

    }

    companion object {
        /** Creates and returns a network request executor using Retrofit `Call<T>` enqueue. */
        val callEnqueue: Executor
            get() {
                return object : Executor {
                    override fun <R> fetchAndCallback(endpoint: Call<R>, callback: Callback<R>) {
                        endpoint.enqueue(callback)
                    }
                }
            }
    }
}

/**
 * Fetch for response type `R` and callback in `success` callback. Invokes failure with
 * an error subtype of `AnyFetchError` upon failure.
 *
 * @param usingExecutor Network request executor (set to `this.executor` by default)
 * @param endpoint The API endpoint that should be fetched
 * @param success Success callback with the response `R`
 * @param failure Failure callback with an error case from `AnyFetchError` subtypes
 */
inline fun <reified R> RetrofitFetching.fetch(
    usingExecutor: RetrofitFetching.Executor = executor,
    endpoint: Call<R>,
    noinline success: (R) -> Unit,
    noinline failure: (FetchError) -> Unit
) {
    val resultProcessing = RetrofitResultProcessing<R, FetchError>(
        errorFromNetworkFailure = { FetchError.Network(it) },
        hasNetwork = checkNetwork
    )
    fetch(usingExecutor, endpoint, resultProcessing, success, failure)
}

/**
 * Fetch for response type `R` and callback in `success` callback. Invokes failure with
 * an error subtype of `E : AnyFetchError` upon failure.
 *
 * @param usingExecutor Network request executor (set to `this.executor` by default)
 * @param endpoint The API endpoint that should be fetched
 * @param resultProcessing Processes response and finds corresponding error case (if needed)
 * @param success Success callback with the response `R`
 * @param failure Failure callback with an error case from given error subtype `E`
 */
fun <R, E> RetrofitFetching.fetch(
    usingExecutor: RetrofitFetching.Executor = executor,
    endpoint: Call<R>,
    resultProcessing: RetrofitFetching.ResultProcessing<R, E>,
    success: (R) -> Unit,
    failure: (E) -> Unit
) {
    fetch(usingExecutor, endpoint, resultProcessing) { it.invoke(success, failure) }
}

inline fun <R, E> RetrofitResult<R, E>.invoke(success: (R) -> Unit, failure: (E) -> Unit) {
    return when (this) {
        is RetrofitResult.Success -> success(this.result)
        is RetrofitResult.Failure -> failure(this.error)
    }
}

sealed class RetrofitResult<R, E> {
    data class Success<R, E>(val result: R) : RetrofitResult<R, E>()
    data class Failure<R, E>(val error: E) : RetrofitResult<R, E>()
}

private fun <R, E> fetch(
    usingExecutor: RetrofitFetching.Executor,
    endpoint: Call<R>,
    resultProcessing: RetrofitFetching.ResultProcessing<R, E>,
    callback: (RetrofitResult<R, E>) -> Unit,
) {
    usingExecutor.fetchAndCallback(endpoint, object : Callback<R> {
        override fun onFailure(call: Call<R>, t: Throwable) {
            callback(RetrofitResult.Failure(resultProcessing.errorFromFailureResponse(call, t)))
        }

        override fun onResponse(call: Call<R>, response: Response<R>) {
            callback(resultProcessing.resultFromResponse(call, response))
        }
    })
}

//endregion

//region Retrofit Standard Result Processing

/** Returns result processing object for given response type `R` and error type `E` */
open class RetrofitResultProcessing<R, E>(
    override val tryCastResponseBody: (Any?) -> R?,
    override val errorFromNetworkFailure: (AnyFetchError) -> E,
    hasNetwork: (() -> Boolean)? = null,
) : RetrofitFetching.ResultProcessing<R, E> {

    companion object {
        inline operator fun <reified R, E> invoke(
            noinline errorFromNetworkFailure: (AnyFetchError) -> E,
            noinline hasNetwork: (() -> Boolean)? = null
        ) : RetrofitResultProcessing<R, E> {
            return RetrofitResultProcessing<R, E>(
                tryCastResponseBody = {
                    if (it == null && R::class.java == Unit::class.java) { Unit as R }
                    else { it as? R }
                },
                errorFromNetworkFailure = errorFromNetworkFailure, hasNetwork = hasNetwork
            )
        }
    }

    override var errorFromFailureResponse: (call: Call<*>, t: Throwable) -> E = { call, t ->
        val error: AnyFetchError = when(t) {
            is UnknownHostException -> errorForUnknownHostException
            is IllegalStateException -> errorForIllegalStateException

            //TODO: Check other cases
            else -> AnyFetchError.Unknown()
        }
        errorFromNetworkFailure(
            AnyFetchError.make(error = error, addingDump = "(Throwable): $t\n(Call): $call")
        )
    }

    override var resultFromResponse:
                (Call<R>, Response<R>) -> RetrofitResult<R, E> = { call, response ->
        if (response.isSuccessful) {
            tryCastResponseBody(response.body())?.let { body ->
                RetrofitResult.Success(body)
            } ?: RetrofitResult.Failure(
                errorFromNetworkFailure(
                    AnyFetchError.make(
                        error = AnyFetchError.NotFound.MissingData,
                        addingDump = "(Response): $response\n(Call): $call"
                    )
                )
            )
        } else {
            RetrofitResult.Failure(
                errorFromNetworkFailure(AnyFetchError.BadStatusCode(response.code(), response))
            )
        }
    }

    //region Exception to AnyFetchError mapping

    var errorForUnknownHostException: AnyFetchError = if (hasNetwork != null) {
        if (hasNetwork()) AnyFetchError.BadRequest.Encode
        else AnyFetchError.NotFound.MissingNetwork
    } else {
        // Cannot distinguish the error case from `MissingNetwork` and `Encode` error.
        AnyFetchError.Unknown()
    }

    var errorForIllegalStateException: AnyFetchError = AnyFetchError.BadRequest.Decode

    //endregion

}

//endregion
 No newline at end of file
+75 −0
Original line number Diff line number Diff line
package app.lounge.networking

import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

interface RawResponseProcessing<R, E, T> {
    val onResponse: (Call<T>, Response<T>) -> RetrofitResult<R, E>
}


fun <R, E, T, Processing> RetrofitFetching.fetch(
    usingExecutor: RetrofitFetching.Executor = executor,
    endpoint: Call<T>,
    processing: Processing,
    success: (R) -> Unit,
    failure: (E) -> Unit
) where Processing : RawResponseProcessing<R, E, T>,
        Processing : RetrofitFetching.ResultProcessing<R, E> {
    fetch(usingExecutor, endpoint, processing) { it.invoke(success, failure) }
}


private fun <R, E, T, Processing> fetch(
    usingExecutor: RetrofitFetching.Executor,
    endpoint: Call<T>,
    processing: Processing,
    callback: (RetrofitResult<R, E>) -> Unit,
) where Processing : RawResponseProcessing<R, E, T>,
        Processing : RetrofitFetching.ResultProcessing<R, E> {
    usingExecutor.fetchAndCallback(endpoint, object : Callback<T> {
        override fun onFailure(call: Call<T>, t: Throwable) {
            callback(RetrofitResult.Failure(processing.errorFromFailureResponse(call, t)))
        }

        override fun onResponse(call: Call<T>, response: Response<T>) {
            callback(processing.onResponse(call, response))
        }
    })
}

class RetrofitRawResultProcessing<R, E, T>(
    override val onResponse: (Call<*>, Response<*>) -> RetrofitResult<R, E>,
    override val tryCastResponseBody: (Any?) -> R?,
    override val errorFromNetworkFailure: (AnyFetchError) -> E,
    hasNetwork: (() -> Boolean)? = null,
) : RetrofitResultProcessing<R, E>(tryCastResponseBody, errorFromNetworkFailure, hasNetwork),
    RawResponseProcessing<R, E, T> {

    override var resultFromResponse: (Call<R>, Response<R>) -> RetrofitResult<R, E> = {
            call, response ->
        when(val customResult = onResponse(call, response)) {
            is RetrofitResult.Success -> customResult
            is RetrofitResult.Failure -> super.resultFromResponse(call, response)
        }
    }

    companion object {
        inline operator fun <reified R, E, T> invoke(
            noinline onResponse: (Call<*>, Response<*>) -> RetrofitResult<R, E>,
            noinline errorFromNetworkFailure: (AnyFetchError) -> E,
            noinline hasNetwork: (() -> Boolean)? = null
        ) : RetrofitRawResultProcessing<R, E, T> {
            return RetrofitRawResultProcessing(
                onResponse = onResponse,
                tryCastResponseBody = {
                    if (it == null && R::class.java == Unit::class.java) { Unit as R }
                    else { it as? R }
                },
                errorFromNetworkFailure = errorFromNetworkFailure, hasNetwork = hasNetwork
            )
        }
    }

}