Httply is a modern, lightweight HTTP networking library for Android that supports both Retra-style (annotation-based) and Voltra-style (request queue-based) APIs. Built with zero external dependencies, it's designed to be simple, efficient, and easy to use.
🔌 | Zero External Dependencies - Built entirely with Java standard libraries |
🧩 |
Dual API Styles • Retra API - Declarative, annotation-based API for defining HTTP endpoints • Voltra API - Imperative, queue-based API for making requests |
☕ | Full Java and Kotlin Support - Works seamlessly with both languages |
🪶 | Lightweight - Minimal footprint in your app |
🔄 | Connection Pooling - Reuses connections for better performance |
🧠 | Configurable Caching - Control whether requests should be cached |
⏱️ | Configurable Timeouts - Set connect and read timeouts |
🧵 | Customizable Thread Pools - Control the number of threads used for requests |
📋 | JSON Support - Built-in JSON parsing with standard org.json library |
Httply is designed with a strong focus on minimizing dependencies in your Android projects:
- No third-party libraries - Reduces risk of dependency conflicts and security vulnerabilities
- Better long-term stability - No risk of external dependencies becoming deprecated or unmaintained
- Consistent behavior - No unexpected changes from third-party library updates
- Smaller APK size - No additional libraries means your app's size stays smaller
- Faster build times - Fewer dependencies lead to quicker compilation and build processes
- Reduced method count - Helps stay under the 65K method limit without requiring multidex
Gradle |
---|
Step 1: Add JitPack repository to your project-level build.gradle: dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
} Step 2: Add the dependency to your app-level build.gradle: dependencies {
implementation 'com.github.tuhinx:httply:1.0.5'
} |
Kotlin DSL |
Step 1: Add JitPack repository to your settings.gradle.kts: dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
} Step 2: Add the dependency to your app-level build.gradle.kts: dependencies {
implementation("com.github.tuhinx:httply:1.0.5")
} |
Annotation-based, declarative style |
Queue-based, imperative style |
The Retra API provides a clean, annotation-based approach for defining HTTP endpoints.
// Define model classes for your API responses
public class FoodItem {
private String name;
private String category;
private String price;
// Getters
public String getName() { return name; }
public String getCategory() { return category; }
public String getPrice() { return price; }
}
// Define your API interface
public interface FoodApi {
@GET("v1/861a8605-a6e0-408d-8feb-ab303b15f59f")
Call<List<FoodItem>> getFoodItems();
}
// Create a Retra instance using the builder pattern
Retra retra = new Retra.Builder()
.baseUrl("https://mocki.io/")
.addConverterFactory(GsonConverterFactory.create())
.build();
// Create an implementation of the API interface
FoodApi api = retra.create(FoodApi.class);
// Asynchronous requests with callbacks
Call<List<FoodItem>> call = api.getFoodItems();
call.enqueue(new Callback<List<FoodItem>>() {
@Override
public void onResponse(Call<List<FoodItem>> call, RetraResponse<List<FoodItem>> response) {
if (response.isSuccessful() && response.body() != null) {
// Process the response on the UI thread
for (FoodItem item : response.body()) {
HashMap<String, String> map = new HashMap<>();
map.put("name", item.getName());
map.put("category", item.getCategory());
map.put("price", "$" + item.getPrice());
foodList.add(map);
}
// Update the UI
FoodAdapter adapter = new FoodAdapter(context, foodList);
listView.setAdapter(adapter);
adapter.notifyDataSetChanged();
} else {
Toast.makeText(context, "Retra: Empty response", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<List<FoodItem>> call, Throwable t) {
Toast.makeText(context, "Retra Error: " + t.getMessage(), Toast.LENGTH_LONG).show();
// Fallback to Voltra API if Retra fails
callVoltraApi();
}
});
The Voltra API provides a request queue-based approach, perfect for making multiple HTTP requests.
// Initialize a RequestQueue with the default configuration
RequestQueue queue = Httply.newRequestQueue(context);
📊 JsonObjectRequest Example
// GET request for a JSON object (lambda style)
String url = "https://mocki.io/v1/861a8605-a6e0-408d-8feb-ab303b15f59f";
JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(
Request.Method.GET,
url,
null, // No request body for GET
response -> {
try {
// Parse the JSON response
String name = response.getString("name");
String category = response.getString("category");
// Use the data
System.out.println("Food: " + name + ", Category: " + category);
} catch (JSONException e) {
e.printStackTrace();
}
},
error -> {
// Handle any errors
error.printStackTrace();
}
);
// Disable caching for this request (optional)
jsonObjectRequest.setShouldCache(false);
// Add the request to the queue to execute it
queue.add(jsonObjectRequest);
// GET request for a JSON object (anonymous inner class style)
String url = "https://api.example.com/food";
JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(
Request.Method.GET,
url,
null,
new VoltraResponse.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try {
int id = response.getInt("id");
String name = response.getString("name");
String category = response.getString("category");
double price = response.getDouble("price");
Toast.makeText(context,
name + " (" + category + ") - $" + price,
Toast.LENGTH_SHORT).show();
} catch (JSONException e) {
e.printStackTrace();
}
}
},
new VoltraResponse.ErrorListener() {
@Override
public void onErrorResponse(VoltraError error) {
error.printStackTrace();
Toast.makeText(context, "Error loading object", Toast.LENGTH_SHORT).show();
}
}
);
jsonObjectRequest.setShouldCache(false);
requestQueue.add(jsonObjectRequest);
📋 JsonArrayRequest Example
// GET request for a JSON array (lambda style)
String urlArray = "https://mocki.io/v1/861a8605-a6e0-408d-8feb-ab303b15f59f";
JsonArrayRequest jsonArrayRequest = new JsonArrayRequest(
Request.Method.GET,
urlArray,
null,
response -> {
try {
// Iterate through the JSON array
for (int i = 0; i < response.length(); i++) {
JSONObject food = response.getJSONObject(i);
// Extract data from each object
String name = food.getString("name");
String category = food.getString("category");
// Use the data
System.out.println("Food " + i + ": " + name + ", " + category);
}
} catch (JSONException e) {
e.printStackTrace();
}
},
error -> {
// Handle any errors
error.printStackTrace();
}
);
jsonArrayRequest.setShouldCache(false);
// Add the request to the queue to execute it
queue.add(jsonArrayRequest);
// GET request for a JSON array (anonymous inner class style)
String url = "https://api.example.com/foods";
JsonArrayRequest jsonArrayRequest = new JsonArrayRequest(
Request.Method.GET,
url,
null,
new VoltraResponse.Listener<JSONArray>() {
@Override
public void onResponse(JSONArray response) {
try {
for (int i = 0; i < response.length(); i++) {
JSONObject item = response.getJSONObject(i);
String name = item.getString("name");
double price = item.getDouble("price");
int finalI = i;
Toast.makeText(context,
(finalI + 1) + ". " + name + " - $" + price,
Toast.LENGTH_SHORT).show();
}
} catch (JSONException e) {
e.printStackTrace();
}
}
},
new VoltraResponse.ErrorListener() {
@Override
public void onErrorResponse(VoltraError error) {
error.printStackTrace();
Toast.makeText(context, "Error loading array", Toast.LENGTH_SHORT).show();
}
}
);
jsonArrayRequest.setShouldCache(false);
requestQueue.add(jsonArrayRequest);
📝 StringRequest Example
// GET request for a string response (lambda style)
String url = "https://mocki.io/v1/861a8605-a6e0-408d-8feb-ab303b15f59f";
StringRequest stringRequest = new StringRequest(
Request.Method.GET,
url,
response -> {
Log.d("TAG", "Raw response: " + response);
if (response != null && !response.trim().isEmpty()) {
try {
JSONArray jsonArray = new JSONArray(response);
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject item = jsonArray.getJSONObject(i);
HashMap<String, String> map = new HashMap<>();
map.put("name", item.getString("name"));
map.put("category", item.getString("category"));
map.put("price", "$" + item.getString("price"));
foodList.add(map);
}
Toast.makeText(context, "Loaded " + foodList.size() + " items.", Toast.LENGTH_SHORT).show();
} catch (JSONException e) {
Log.e("TAG", "JSON parsing error: " + e.getMessage());
Toast.makeText(context, "Invalid JSON format", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(context, "Empty response", Toast.LENGTH_SHORT).show();
}
},
error -> {
Log.e("TAG", "Voltra Error: " + error.getMessage(), error);
Toast.makeText(context, "Voltra Error: " + error.getMessage(), Toast.LENGTH_SHORT).show();
}
);
// Disable caching for this request
stringRequest.setShouldCache(false);
// Add the request to the queue
queue.add(stringRequest);
// GET request for a string response (anonymous inner class style)
String url = "https://mocki.io/v1/861a8605-a6e0-408d-8feb-ab303b15f59f";
StringRequest stringRequest = new StringRequest(
Request.Method.GET,
url,
new VoltraResponse.Listener<String>() {
@Override
public void onResponse(String response) {
Log.d("TAG", "Raw response: " + response);
if (response != null && !response.trim().isEmpty()) {
try {
JSONArray jsonArray = new JSONArray(response);
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject item = jsonArray.getJSONObject(i);
HashMap<String, String> map = new HashMap<>();
map.put("name", item.getString("name"));
map.put("category", item.getString("category"));
map.put("price", "$" + item.getString("price"));
// Add to your data list
foodList.add(map);
}
Toast.makeText(context, "Loaded " + foodList.size() + " items.", Toast.LENGTH_SHORT).show();
} catch (JSONException e) {
Log.e("TAG", "JSON parsing error: " + e.getMessage());
Toast.makeText(context, "Invalid JSON format", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(context, "Empty response", Toast.LENGTH_SHORT).show();
}
}
},
new VoltraResponse.ErrorListener() {
@Override
public void onErrorResponse(VoltraError error) {
Log.e("TAG", "Voltra Error: " + error.getMessage(), error);
Toast.makeText(context, "Voltra Error: " + error.getMessage(), Toast.LENGTH_SHORT).show();
}
}
);
// Disable caching for this request
stringRequest.setShouldCache(false);
// Add the request to the queue
requestQueue.add(stringRequest);
Httply is designed with performance in mind, offering several features to optimize your network operations:
⚡ | Efficient Resource Usage - Minimizes memory and CPU consumption |
🔄 | Connection Reuse - Reduces connection establishment overhead |
📦 | Minimal Footprint - Zero dependencies means smaller APK size |
⏱️ | Configurable Timeouts - Fine-tune network behavior for your use case |
Httply allows you to control whether individual requests should be cached:
// Create a request
StringRequest stringRequest = new StringRequest(
Request.Method.GET,
"https://mocki.io/v1/861a8605-a6e0-408d-8feb-ab303b15f59f",
new VoltraResponse.Listener<String>() {
@Override
public void onResponse(String response) {
// Process the response
Log.d("TAG", "Raw response: " + response);
// Parse JSON and update UI
}
},
new VoltraResponse.ErrorListener() {
@Override
public void onErrorResponse(VoltraError error) {
// Handle errors
Log.e("TAG", "Voltra Error: " + error.getMessage(), error);
}
}
);
// Disable caching for this request
stringRequest.setShouldCache(false);
// Add the request to the queue
requestQueue.add(stringRequest);
By default, all requests are cached. Setting setShouldCache(false)
will:
- Prevent the connection from being reused for future requests
- Force a new connection to be established for this request
- Useful for requests that need fresh data or when troubleshooting network issues
Httply provides a powerful, customizable HTTP client that forms the foundation of both the Retra and Voltra APIs. The client is built on Java's standard HttpURLConnection
but adds several advanced features:
⏱️ | Configurable Timeouts - Set custom connect and read timeouts to handle slow networks |
🔄 | Connection Pooling - Reuse connections to improve performance and reduce latency |
🧵 | Custom Thread Pools - Configure the executor used for asynchronous requests |
🔀 | Redirect Control - Enable or disable following HTTP redirects |
🧠 | Caching Control - Fine-grained control over which requests should be cached |
// Create a custom HTTP client with advanced configuration
HttpClient client = Httply.newHttpClient()
.connectTimeout(15, TimeUnit.SECONDS) // Set connection timeout
.readTimeout(30, TimeUnit.SECONDS) // Set read timeout
.followRedirects(true) // Enable following redirects
.build();
// Create a custom connection pool
ConnectionPool connectionPool = new ConnectionPool(
10, // Maximum idle connections
5, TimeUnit.MINUTES // Keep-alive duration
);
// Create a custom executor for async requests
Executor executor = Executors.newFixedThreadPool(4);
// Create a fully customized HTTP client
HttpClient client = Httply.newHttpClient()
.connectionPool(connectionPool)
.executor(executor)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.followRedirects(false) // Disable following redirects
.build();
Use with Retra API |
---|
// Configure a Retra instance with the custom client
Retra retra = new Retra.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(JsonConverterFactory.create())
.build();
// Use Retra to create API interfaces
ApiService api = retra.create(ApiService.class); |
Use with Voltra API |
// Configure a RequestQueue with the custom client
RequestQueue queue = Httply.newRequestQueue(
context,
client
);
// Use the queue to make requests
queue.add(new StringRequest(...)); |
Httply's HTTP client includes a built-in connection pooling mechanism that significantly improves performance by reusing existing connections:
⚡ | Reduced Latency - Eliminates the overhead of establishing new connections |
🔋 | Lower Resource Usage - Reduces CPU, memory, and battery consumption |
📈 | Improved Throughput - Handles more requests with fewer resources |
⚙️ | Configurable - Customize pool size and connection keep-alive duration |
// Create a custom connection pool with 20 max idle connections
// and a 10-minute keep-alive duration
ConnectionPool connectionPool = new ConnectionPool(
20, // Maximum idle connections
10, TimeUnit.MINUTES // Keep-alive duration
);
// Use the custom connection pool with your HTTP client
HttpClient client = Httply.newHttpClient()
.connectionPool(connectionPool)
.build();
Httply allows you to customize the thread pool used for asynchronous requests, giving you control over resource usage and concurrency:
🧵 | Fixed Thread Pool - Limit the number of concurrent requests |
⏱️ | Scheduled Executor - Schedule requests to run at specific times |
🔄 | Single Thread - Ensure requests are processed sequentially |
🚀 | Cached Thread Pool - Dynamically adjust thread count based on load |
// Create a fixed thread pool with 4 threads
Executor fixedPool = Executors.newFixedThreadPool(4);
// Create a single-threaded executor for sequential processing
Executor singleThread = Executors.newSingleThreadExecutor();
// Create a cached thread pool that adjusts based on load
Executor cachedPool = Executors.newCachedThreadPool();
// Use a custom executor with your HTTP client
HttpClient client = Httply.newHttpClient()
.executor(fixedPool) // Choose the appropriate executor for your needs
.build();
For advanced use cases, you can also use the HTTP client directly:
// Create an HTTP client
HttpClient client = Httply.newHttpClient().build();
// Create a request
HttpRequest request = new HttpRequest.Builder()
.url("https://mocki.io/v1/861a8605-a6e0-408d-8feb-ab303b15f59f")
.method(HttpMethod.GET)
.shouldCache(true) // Enable caching (default)
.build();
// Execute synchronously
try {
HttpResponse response = client.execute(request);
if (response.isSuccessful()) {
String body = response.body().string();
// Process the response
}
} catch (IOException e) {
e.printStackTrace();
}
// Or execute asynchronously
client.executeAsync(request, new HttpClient.Callback() {
@Override
public void onResponse(HttpResponse response) {
// Process the response
}
@Override
public void onFailure(Exception e) {
// Handle the error
}
});
🔌 |
Connection Timeout If you're experiencing connection timeouts, try increasing the timeout values: HttpClient client = Httply.newHttpClient() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); |
🔄 |
Stale Connections If you suspect stale connections in the pool, disable caching for critical requests: request.setShouldCache(false); |
🧵 |
Thread Pool Exhaustion If you're making many concurrent requests, consider using a larger thread pool: Executor executor = Executors.newFixedThreadPool(8); HttpClient client = Httply.newHttpClient() .executor(executor) .build(); |
🔒 |
SSL/TLS Issues For HTTPS connections, ensure your server's certificates are valid and trusted. |
View MIT License Text
MIT License
Copyright (c) 2025 TuhinX
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Made with ❤️ by Tuhinx