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
-
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
-
Integrate this library as a dependency in your project:
-
Clone the project and publish it to your local Maven repository:
-
(In Android Studio top menu) Build → Clean Build
-
Build → Make Module 'Hoodies-Network.Hoodies-Network'
-
Gradle → Tasks → publishing → publishAarPublicationToMavenLocal
-
-
Use Gradle to add it as a dependency in your project:
-
implementation "com.gap.androidlibraries:hoodies-networkandroid:latest"
-
-
-
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()
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
}
}
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
-
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()
By default, all cookies are ignored. Cookie retention and manipulation can be performed as follows:
-
Pass a
CookieJar
to the.enableCookiesWithCookieJar()
method of theHoodiesNetworkClient.Builder()
:-
(For most use-cases) Use the
CookieJar()
-
(If cookies must persist across app launches) Use the
PersistentCookieJar("myPersistentCookieJar", context)
- Cookies are securely encrypted while in storage
-
-
Manipulate the contents of the
CookieJar
using the following methods:-
getCookiesForHost(host: URI) : List<HttpCookie>
gets all the cookies for a specified host -
getAllCookies() : List<HttpCookie>
gets all the cookies stored in theCookieJar
-
getAllHosts() : List<URI>
gets a list of all hosts that have stored cookies in theCookieJar
-
setCookiesForHost(host: URI, cookies: List<HttpCookie>)
overwrites all the cookies for the specified host with those in the passed list -
addCookieForHost(host: URI, cookie: HttpCookie)
adds the passed cookie for the specified host -
removeAllCookies()
deleted all cookies in theCookieJar
-
By default, no data is cached. Caching can be configured and enabled as follows:
-
Create a
CacheEnabled
object-
If the data in the cache needs to be encrypted, set
encryptionEnabled = true
-
Decide what the stale data threshold should be and set it:
staleDataThreshold = Duration.ofSeconds(60)
-
Instantiate the object:
val cacheConfiguration = CacheEnabled(encryptionEnabled = true, staleDataThreshold = Duration.ofSeconds(60), context)
-
-
Pass the
CacheEnabled
object to thecacheConfiguration
parameter when making a network request:
return@withContext client.getUrlQueryParam<LocationAttribute>(
queryParams = queryParams,
api = pathParams,
cacheConfiguration = cacheConfiguration
)
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)
}
}
The MockWebServer can replicate your API endpoints for unit testing purposes.
-
Create a
MockWebServerManager.Builder()
and set the port:val serverBuilder = MockWebServerManager.Builder().usePort(5000)
-
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 aWebServerHandler()
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)
-
Start the MockWebServer:
val server = serverBuilder.start()
-
Run your tests
-
Stop the MockWebServer:
server.stop()
There are many more usage examples in the examples folder.
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. |
-
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.
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.
-
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.