/hoodies-network

A modern, type-safe, and high-performance Android HTTP library made by Gap Inc.

Primary LanguageKotlinMIT LicenseMIT

Hoodies-Network

Introduction

Hoodies-Network is a modern, type-safe and high-performance Android HTTP library. Though its feature set has expanded over time to match and sometimes exceed other libraries with respect to performance. It was originally designed to make network calls in Gap’s Instant Apps.

  • Is designed using a philosophy of simple and clean architecture

  • Offers a highly competitive feature set while providing a smaller binary size

  • Is Kotlin-native

Features

  • URL parameter replacement and query parameter support

  • Object conversion to request body (e.g., JSON, protocol buffers)

  • Object conversion from response body (e.g., JSON, protocol buffers, images)

  • Custom header support

  • Multiple and multipart file upload

  • Support for POST, GET, PATCH, PUT, and DELETE methods

  • Success and failure callbacks

  • Request, error, network, response, request body encryption, and response body decryption interceptors

  • Automatic retry on failure

  • Use of SSLSocketFactory

  • Advanced mock web server

  • Advanced optionally encrypted cache system

  • Built-in cookie handling including encrypted persistent local storage

Usage Guide

Getting Started

  1. Integrate this library as a dependency in your project:

    1. Clone the project and publish it to your local Maven repository:

      1. (In Android Studio top menu) Build → Clean Build

      2. BuildMake Module 'Hoodies-Network.Hoodies-Network'

      3. GradleTaskspublishingpublishAarPublicationToMavenLocal

    2. Use Gradle to add it as a dependency in your project:

      1. implementation "com.gap.androidlibraries:hoodies-networkandroid:latest"

  2. Build your HttpClient:

val defaultHeaders = hashMapOf(
        CONTENT_TYPE_KEY to APPLICATION_JSON, MULTIDB_ENABLED to MULTIDB_ENABLED_VALUE,
        CLIENT_ID to CLIENT_ID_VALUE,
        CLIENT_OS to CLIENT_OS_VALUE
    )

val client = HoodiesNetworkClient.Builder()
    .baseUrl(baseUrl)
    .addHeaders(defaultHeaders)
    .addInterceptor(sessionInterceptor)
    .retryOnConnectionFailure(true, HoodiesNetworkClient.RetryCount.RETRY_MAX)
    .build()

Using Interceptors

Optionally, you can create an Interceptor Class which inherits from com.gap.network.interceptor.Interceptor. Interceptors allow you read/modify all properties (headers, body, etc.) of requests and responses before they are executed/delivered.

CancellableMutableRequests can be cancelled by calling cancellableMutableRequest.cancelRequest(Success(object to return)) or cancellableMutableRequest.cancelRequest(Failure(message, code, throwable)) - based on your use case.

RetryableCancellableMutableRequests can be cancelled as well as retried. If the request has its body or headers changed, the retry attempt will execute the request with the changes intact.

class SessionInterceptor(context: Context) : Interceptor(context) {

    override fun interceptNetwork(isOnline: Boolean, cancellableMutableRequest: CancellableMutableRequest) {
	//First thing that is called before the request is made
	//Here, you can define what happens if the device knows that it is offline
	//For example:
        //If you don't want the request to be executed if there is no network connection:
	if (!isOnline) { cancellableMutableRequest.cancelRequest(Failure(HoodiesNetworkError("No connection!", 0, SocketTimeoutException("No connection!")))) }
    }

    override fun interceptRequest(identifier: String, cancellableMutableRequest: CancellableMutableRequest) {
        //Second thing that is called before the request is made
	//Here, you can define some universal behaviors for all network requests
	//For example:
	//Append an Authorization header
	val headers = cancellableMutableRequest.request.getHeaders().toMutableMap()
	headers["Authorization"] = "Something"
        cancellableMutableRequest.request.setRequestHeaders(headers)
    }

    override fun interceptError(error: HoodiesNetworkError, retryableCancellableMutableRequest: RetryableCancellableMutableRequest, autoRetryAttempts: Int) {
        //This is invoked before the failure callback is called
	//Here, you can define some universal behaviors for error handling
	//For example:
	//You can retry the request if it fails because of expired authorization data
	if (error.code == 403) {
		val headers = retryableCancellableMutableRequest.request.getHeaders().toMutableMap()
		headers["Authorization"] = getNewAuthorization()
        	retryableCancellableMutableRequest.request.setRequestHeaders(headers)

	  	retryableCancellableMutableRequest.retryRequest()
	}
    }

    override fun interceptResponse(result: Result<*, HoodiesNetworkError>, request: Request<Any>?) {
        //This is invoked upon the successful completion of a request
	//Here, you can define some universal behaviors for all responses
    }
}

Using Automatic Retry

If a request fails due to a SocketTimeoutException or IOException, Hoodies-Network can automatically retry the request a specific number of times.

Retry is configured in the HoodiesNetworkClient.Builder() with the .retryOnConnectionFailure(true, HoodiesNetworkClient.RetryCount.RETRY_MAX) method.

The following options are available:

  • HoodiesNetworkClient.RetryCount.RETRY_NEVER

  • HoodiesNetworkClient.RetryCount.RETRY_ONCE

  • HoodiesNetworkClient.RetryCount.RETRY_TWICE

  • HoodiesNetworkClient.RetryCount.RETRY_THRICE

  • HoodiesNetworkClient.RetryCount.RETRY_MAX

Configuring Timeouts

  • Connect timeout can be configured using HttpClientConfig.setConnectTimeOut(Duration.ofSeconds(seconds))

  • Read timeout can be configured using HttpClientConfig.setReadTimeOut(Duration.ofSeconds(seconds))

  • Setting the duration to 0 will make the timeout infinite

  • Changes apply to all HttpClients

  • Defaults can be restored using HttpClientConfig.setFactoryDefaultConfiguration()

Handling Cookies

By default, all cookies are ignored. Cookie retention and manipulation can be performed as follows:

  1. Pass a CookieJar to the .enableCookiesWithCookieJar() method of the HoodiesNetworkClient.Builder():

    1. (For most use-cases) Use the CookieJar()

    2. (If cookies must persist across app launches) Use the PersistentCookieJar("myPersistentCookieJar", context) - Cookies are securely encrypted while in storage

  2. Manipulate the contents of the CookieJar using the following methods:

    1. getCookiesForHost(host: URI) : List<HttpCookie> gets all the cookies for a specified host

    2. getAllCookies() : List<HttpCookie> gets all the cookies stored in the CookieJar

    3. getAllHosts() : List<URI> gets a list of all hosts that have stored cookies in the CookieJar

    4. setCookiesForHost(host: URI, cookies: List<HttpCookie>) overwrites all the cookies for the specified host with those in the passed list

    5. addCookieForHost(host: URI, cookie: HttpCookie) adds the passed cookie for the specified host

    6. removeAllCookies() deleted all cookies in the CookieJar

Using the Cache

By default, no data is cached. Caching can be configured and enabled as follows:

  1. Create a CacheEnabled object

    1. If the data in the cache needs to be encrypted, set encryptionEnabled = true

    2. Decide what the stale data threshold should be and set it: staleDataThreshold = Duration.ofSeconds(60)

    3. Instantiate the object: val cacheConfiguration = CacheEnabled(encryptionEnabled = true, staleDataThreshold = Duration.ofSeconds(60), context)

  2. Pass the CacheEnabled object to the cacheConfiguration parameter when making a network request:

return@withContext client.getUrlQueryParam<LocationAttribute>(
        queryParams = queryParams,
        api = pathParams,
        cacheConfiguration = cacheConfiguration
    )

Using Encryption/Decryption Interceptors

Encryption/decryption of the request and response bodies can be implemented by passing an EncryptionDecryptionInterceptor to the .addEncryptionDecryptionInterceptor(encDecInterceptor) method of the HoodiesNetworkClient.Builder().

val encDecInterceptor = EncDecInterceptor(this.context)

class EncDecInterceptor(override val context: Context) : EncryptionDecryptionInterceptor {

    override fun decryptResponse(response: ByteArray): ByteArray {
        // add your decryption logic here
        return  ByteArray(1)
    }

    override fun encryptAdditionalHeaders(additionalHeaderValue: ByteArray): ByteArray {
        // add your encryption logic here
        return  ByteArray(1)
    }

    override fun encryptRequest(requestBodyOrUrlQueryParamKeyValue: ByteArray): ByteArray {
        // add your encryption logic here
        return  ByteArray(1)
    }
}

Using the MockWebServer

The MockWebServer can replicate your API endpoints for unit testing purposes.

  1. Create a MockWebServerManager.Builder() and set the port: val serverBuilder = MockWebServerManager.Builder().usePort(5000)

  2. Mock your API endpoints (For simple use-cases) Using the MockServerMaker DSL:

    //Make request body
    val body = JSONObject()
    body.put("name", "test_1")
    body.put("salary", "1234")
    body.put("age", "123")
    
    //Make request headers
    val reqHeaders: MutableMap<String, String> = HashMap()
    reqHeaders["key"] = "value"
    
    //Mock response
    val response = "{\"status\":\"success\",\"data\":{\"name\":\"test_1\",\"salary\":\"1234\",\"age\":\"123\",\"id\":9221},\"message\":\"Successfully! Record has been added.\"}"
    
    //Set up MockWebServer builder with port
    val serverBuilder = MockWebServerManager.Builder().usePort(5000)
    
    //Set up handler on MockWebServer to accept the request body and headers from above
    MockServerMaker.Builder()
        .acceptMethod(HoodiesNetworkClient.HttpMethod.POST)
        .expect(body) //Can also be a HashMap<String, String> to validate URL-encoded params
        .expectHeaders(reqHeaders)
        .returnThisJsonIfInputMatches(JSONObject(response))
        .applyToMockWebServerBuilder("/test", serverBuilder)
    (For advanced behavior) By making a WebServerHandler() for your endpoint:
    val handler = object : WebServerHandler() {
        override fun handleRequest(call: HttpCall) {
            when (method) {
                //KTor-like syntax
                get {
                    val delayLength = call.httpExchange.requestURI.toString().split("/").last()
                    Thread.sleep(delayLength.toLong() * 1000L)
                    call.respond(200, "{\"delay\":\"$delayLength\"}")
                }
                post {
                    val delayLength = call.httpExchange.requestURI.toString().split("/").last()
                    Thread.sleep(delayLength.toLong() * 1000L)
                    call.respond(200, "{\"delay\":\"$delayLength\"}")
                }
            }
        }
    }
    
    serverBuilder.addContext("/echodelay", handler)
  3. Start the MockWebServer: val server = serverBuilder.start()

  4. Run your tests

  5. Stop the MockWebServer: server.stop()

More usage examples

There are many more usage examples in the examples folder.

Running Tests

The test classes package path is at com.gap.hoodies_network(androidTest). The test classes use test libraries Mockito and Junit, and run on an Android device. The MockWebServer is used to host the endpoints for the tests. The test classes are as follows:

  • CachingAndCryptographyTest

  • FormUrlEncodedRequestTest

  • EncryptionDecryptionTest

  • FileUploadRequestTests

  • HoodiesNetworkClientTest

  • HeaderTest

  • ImageRequestMockTest

  • ImageTests

  • JsonRequestTest

  • MultiRequestTest

  • NetworkConnectionTest

  • ResponseDeliveryInstant

  • ResponseTest

  • RetryTest

  • SocketTimeOutTest

  • StringRequestTest

  • UrlResolverTest

  • CookieTests

Tests can be run by right-clicking on the androidTest folder and selecting "Run Tests" from the dropdown menu.

Note
A physical device or Android emulator is required to run the tests.

Environment Setup

  • Since this is a Gradle project, any Android and Gradle-compatible IDE can be used. The recommendation is Android Studio.

  • Android Studio Bumblebee and above are supported.

Conduct

This is a professional environment, and you are expected to conduct yourself in a professional and courteous manner. If you fail to exhibit appropriate conduct, your contributions and interactions will no longer be welcome here.

Contributing

  • Everyone is welcome and encouraged to contribute. If you are looking for a place to start, try working on an unassigned issue with the good-first-issue tag.

  • All contributions are expected to conform to standard Kotlin code style and be covered by unit tests.

  • PRs will not be merged if there are failing tests.

  • If you would like to contribute code you can do so through GitHub by forking the repository and sending a pull request.

  • When submitting code, please follow the existing conventions and style in order to keep the code readable.