netra_flutter 1.0.4
netra_flutter: ^1.0.4 copied to clipboard
A Flutter networking plugin with offline queueing, slow network strategies, smart caching, circuit breaker, multipart uploads, and event-driven observability. Built on top of OkHttp via a native Andro [...]
Netra Flutter Plugin #
โ ๏ธ Platform Support: Android only. iOS is not currently supported.
Built on top of a native Kotlin networking engine. A powerful Flutter networking plugin with built-in caching, offline queueing, slow network strategies, circuit breaker support, multipart uploads, and event-driven observability.
๐ฑ Check out the example app for a full working demo.
โ๏ธ Installation #
flutter pub add netra_flutter
Why Netra? #
Netra focuses on real-world mobile networking problems:
- Unreliable connections
- Offline-first architecture
- Slow networks
- Cache consistency
- Request recovery
- Developer observability
Instead of only being an HTTP client, Netra aims to provide a resilient networking layer for modern Flutter applications.
Netra vs Dio Ecosystem #
Most Netra features can be implemented with Dio and additional packages.
The difference is that Netra provides them as a unified networking layer out of the box.
| Capability | Dio Ecosystem | Netra |
|---|---|---|
| HTTP Requests | Dio | Built-in |
| Multipart Upload | Dio | Built-in |
| Request Cancellation | Dio | Built-in |
| Caching | Dio + dio_cache_interceptor | Built-in |
| Offline Cache Fallback | Dio + dio_cache_interceptor configuration | Built-in |
| Offline Queue | Custom implementation | Built-in |
| Retry On Reconnect | offline_retry_interceptor + custom persistence | Built-in |
| Slow Network Policies | Custom implementation | Built-in |
| Circuit Breaker | Custom implementation | Built-in |
| Request Lifecycle Events | Custom interceptors | Built-in |
| Cache Events | Custom implementation | Built-in |
| Queue Events | Custom implementation | Built-in |
| Unified API | Multiple packages | Single SDK |
Quick Example #
final client = NetraClient(
baseUrl: "https://api.example.com",
);
client.on(RequestEvent.requestExecuted((request) {
print("executing: ${request.url}");
}));
client.on(RequestEvent.requestSuccess((request, response) {
print("success: ${response.statusCode} data: ${response.data}");
}));
client.on(RequestEvent.requestFailed((request, response, exception) {
print("failed: ${response?.statusCode}");
}));
final response = await client.get(
requestOptions: RequestOptions(
url: "/users",
cacheOptions: CacheOptions(ttl: 60000),
offlinePolicyAction: OfflinePolicyAction.queue,
slowNetworkPolicyAction: SlowNetworkPolicyAction.useCache,
),
);
Features #
- โก Simple async/await API
- ๐ฆ GET, POST, PUT, PATCH, DELETE support
- ๐ง Smart cache system (memory + disk)
- ๐ก Offline request handling (queue / retry / cache fallback)
- ๐ข Slow network strategies (wait / timeout / cache fallback)
- ๐ Event-driven observers (request, cache, queue, network)
- ๐ผ Multipart file/image upload
- ๐ฅ Streaming support for large responses
- ๐ Automatic queued request restoration
- ๐ฅ Circuit breaker support
- ๐งฉ Converter support (Gson, KotlinX, Moshi)
- ๐ Request cancellation on dispose
- ๐งต Fully async Dart API
- ๐ก๏ธ Guaranteed background execution for long-running requests
Create Client #
final netraClient = NetraClient(
baseUrl: "https://api.example.com",
headers: {
"Authorization": "Bearer token",
},
converterType: ConverterType.gson,
);
With circuit breaker:
final netraClient = NetraClient(
baseUrl: "https://api.example.com",
converterType: ConverterType.gson,
circuitBreakerOptions: CircuitBreakerOptions(
failureThreshold: 5,
retryDelayMs: 1000,
),
);
RequestOptions #
All requests are configured via RequestOptions:
RequestOptions(
url: "/users", // required
body: null, // RequestBody?
headers: {"X-Custom": "value"}, // per-request headers
offlinePolicyAction: ..., // OfflinePolicyAction?
slowNetworkPolicyAction: ..., // SlowNetworkPolicyAction?
cacheOptions: CacheOptions(), // cache config
cancelOnDispose: true, // auto-cancel on widget dispose
executionMode: ExecutionMode.direct, // Execution Mode Direct or Guaranteed
)
GET Request #
final result = await netraClient.get(
requestOptions: RequestOptions(
url: "/users",
),
);
print(result?.statusCode);
print(result?.data);
POST Request #
final result = await netraClient.post(
requestOptions: RequestOptions(
url: "/users",
body: RequestBody.createJson(
jsonEncode({"name": "Sinem", "job": "developer"}),
),
),
);
PUT Request #
final result = await netraClient.put(
requestOptions: RequestOptions(
url: "/users/1",
body: RequestBody.createJson(
jsonEncode({"name": "Sinem", "job": "mobile developer"}),
),
),
);
PATCH Request #
final result = await netraClient.patch(
requestOptions: RequestOptions(
url: "/users/1",
body: RequestBody.createJson(
jsonEncode({"name": "Sinem"}),
),
),
);
DELETE Request #
final result = await netraClient.delete(
requestOptions: RequestOptions(
url: "/users/1",
),
);
Example Response #
result?.statusCode // int
result?.statusMessage // String?
result?.headers // Map<String, String?>?
result?.data // dynamic
result?.isCache // bool โ true if served from cache
Response Types #
The examples above assume the simple, common case โ the request ran and you got real data back. But not every request finishes immediately: an offline-queued or background request hasn't run yet when the call returns. To make that explicit, Netra returns a sealed NetraResponse<T> with two variants:
sealed class NetraResponse<T> {}
class ResponseReceived<T> extends NetraResponse<T> {
final T? data;
final int statusCode;
final String? statusMessage;
final Map<String, String?>? headers;
final bool isCache;
}
class ResponseQueued<T> extends NetraResponse<T> {
final int queueOrder;
}
| Variant | When you get it |
|---|---|
ResponseReceived |
The normal case โ the request actually ran and you have a real response (online, or served from cache) |
ResponseQueued |
The request was deferred instead of run immediately โ either offlinePolicyAction: OfflinePolicyAction.queue was set and the device is offline, or backgroundOptions was set (see Background Execution) |
Handle both explicitly with a switch:
final result = await netraClient.get(
requestOptions: RequestOptions(
url: "/users",
offlinePolicyAction: OfflinePolicyAction.queue,
),
);
switch (result) {
case ResponseReceived(:final data, :final statusCode):
print("received: $statusCode data: $data");
case ResponseQueued(:final queuePosition):
print("queued position $queuePosition");
}
โ ๏ธ When
Execution Guaranteed Modeis set,ResponseQueuedis always returned โ regardless of whether the device is online โ because the request is handed off to a guaranteed background executor from the start rather than run inline. Listen to Queue Events to find out when it actually finishes.
Request Body Types #
JSON #
RequestBody.createJson(jsonEncode({"name": "Sinem"}))
Bytes #
RequestBody.createBytes(
Uint8List.fromList(utf8.encode(jsonEncode({"name": "Sinem"}))),
)
Multipart #
final part = RequestBodyPart.file(
name: "image",
fileName: "photo.jpg",
bytes: bytes,
contentType: "image/jpeg",
);
RequestBody.multipart([part])
Multipart Upload #
final XFile? file = await picker.pickImage(source: ImageSource.gallery);
if (file != null) {
final bytes = await file.readAsBytes();
final result = await netraClient.post(
requestOptions: RequestOptions(
url: "/upload",
body: RequestBody.multipart([
RequestBodyPart.file(
name: "image",
fileName: file.name,
bytes: bytes,
contentType: "image/jpeg",
),
]),
),
);
print(result?.statusCode);
}
Streaming #
For large responses (images, files, audio), use getStream to consume data chunk by chunk.
final stream = await netraClient.getStream(
requestOptions: RequestOptions(
url: "/image",
cancelOnDispose: true,
),
);
final List<int> imageBytes = [];
stream.listen(
(data) {
imageBytes.addAll(data);
},
onDone: () {
final bytes = Uint8List.fromList(imageBytes);
// use bytes
},
onError: (e) {
print("stream error: $e");
},
);
Cache Support #
final result = await netraClient.get(
requestOptions: RequestOptions(
url: "/users",
cacheOptions: CacheOptions(),
),
);
print(result?.isCache); // true if served from cache
Custom TTL:
cacheOptions: CacheOptions(ttl: 60000) // 1 minute in ms
Offline Support #
final result = await netraClient.get(
requestOptions: RequestOptions(
url: "/users",
offlinePolicyAction: OfflinePolicyAction.queue,
),
);
Available offline actions:
| Action | Description |
|---|---|
OfflinePolicyAction.queue |
Stores request, retries automatically when connection is restored |
OfflinePolicyAction.useCache |
Returns cached response if available |
OfflinePolicyAction.retry(retries, retryInterval) |
Retries N times with given interval |
OfflinePolicyAction.throwError |
Immediately throws a network error |
RETRY example: #
offlinePolicyAction: OfflinePolicyAction.retry(
retries: 3,
retryInterval: Duration(seconds: 2),
),
Guaranteed Execution #
For requests that should survive the app being backgrounded or killed โ large uploads, big downloads, anything you don't want lost if the user leaves mid-request โ set backgroundOptions. The request is handed off to a guaranteed background executor immediately; get/post/etc. return ResponseQueued right away, and the real result arrives later through Queue Events.
final result = await netraClient.post(
requestOptions: RequestOptions(
url: "/uploads/photo",
body: RequestBody.multipart([...]),
executionMode: ExecutionMode.guaranteed
),
);
// result is always ResponseQueued here
Listen for the eventual result the same way you'd listen for offline queue events:
netraClient.on(QueueEvent.queuedRequestExecuted((url, response) {
print("background upload finished: ${response.statusCode}");
}));
netraClient.on(QueueEvent.queuedRequestFailed((url) {
print("background upload failed: $url");
}));
Combining
backgroundOptionswithofflinePolicyActionis allowed but redundant โbackgroundOptionsalready defers the request unconditionally, so the offline policy is never evaluated in that case.
Slow Network Strategies #
final result = await netraClient.get(
requestOptions: RequestOptions(
url: "/users",
slowNetworkPolicyAction: SlowNetworkPolicyAction.useCache,
),
);
Available actions:
| Action | Description |
|---|---|
SlowNetworkPolicyAction.useCache |
Returns cached response immediately |
SlowNetworkPolicyAction.wait(delay) |
Waits given duration before sending request |
SlowNetworkPolicyAction.timeout(timeout) |
Throws error if request exceeds duration |
WAIT example: #
slowNetworkPolicyAction: SlowNetworkPolicyAction.wait(
delay: Duration(seconds: 2),
),
TIMEOUT example: #
slowNetworkPolicyAction: SlowNetworkPolicyAction.timeout(
timeout: Duration(milliseconds: 2000),
),
Per-Request Headers #
requestOptions: RequestOptions(
url: "/users",
headers: {
"X-Custom-Header": "value",
},
),
Cancel on Dispose #
Requests marked with cancelOnDispose: true are automatically cancelled when the widget is disposed.
requestOptions: RequestOptions(
url: "/users",
cancelOnDispose: true,
),
Converter Support #
| Converter | Enum |
|---|---|
| Gson | ConverterType.gson |
| Kotlinx Serialization | ConverterType.kotlinX |
| Moshi | ConverterType.moshi |
final netraClient = NetraClient(
baseUrl: "https://api.example.com",
converterType: ConverterType.kotlinX,
);
Observers #
Netra uses an event-driven system. Attach listeners with on() and detach with off().
// attach โ returns an eventId
final eventId = netraClient.on(
CacheEvent.cacheStaleUsed((request, ageMs, ttlMs, expiredByMs) {
print("stale cache used: ${request.url}");
}),
);
// detach
netraClient.off(eventId);
Request Events #
netraClient.on(RequestEvent.requestExecuted((request) {
print("executing: ${request.url}");
}));
netraClient.on(RequestEvent.requestSuccess((request, response) {
print("success: ${response.statusCode} data: ${response.data}");
}));
netraClient.on(RequestEvent.requestFailed((request, response, exception) {
print("failed: ${response?.statusCode}");
}));
Cache Events #
netraClient.on(CacheEvent.cacheHit((request, ageMs, ttlMs) {
print("cache hit: ${request.url}");
}));
netraClient.on(CacheEvent.cacheMiss((request) {
print("cache miss: ${request.url}");
}));
netraClient.on(CacheEvent.cacheStored((request, ageMs, ttlMs) {
print("cache stored: ${request.url}");
}));
netraClient.on(CacheEvent.cacheExpired((request, ageMs, ttlMs, expiredByMs) {
print("cache expired: ${request.url}");
}));
netraClient.on(CacheEvent.cacheStaleUsed((request, ageMs, ttlMs, expiredByMs) {
print("stale cache used: ${request.url}");
}));
| Event | Description |
|---|---|
cacheHit |
Valid cached data found and returned |
cacheMiss |
No cache entry exists |
cacheStored |
Response saved to cache |
cacheExpired |
Cache exists but TTL has passed |
cacheStaleUsed |
Expired cache returned as fallback |
Queue Events #
netraClient.on(QueueEvent.requestQueued((url, queueOrder, createdAt) {
print("queued: $url order: $queueOrder");
}));
netraClient.on(QueueEvent.queuedRequestRestored((url) {
print("restored: $url");
}));
netraClient.on(QueueEvent.queuedRequestExecuted((url, response) {
print("executed: $url status: ${response.statusCode}");
}));
netraClient.on(QueueEvent.queuedRequestFailed((url) {
print("queue failed: $url");
}));
| Event | Description |
|---|---|
requestQueued |
Request stored in offline queue |
queuedRequestRestored |
Connection restored, queue processing started |
queuedRequestExecuted |
Queued request succeeded |
queuedRequestFailed |
Queued request failed on retry |
These same events also fire for
backgroundOptionsrequests โ a background request is a queued request under the hood, just deferred unconditionally instead of only when offline.
Network Events #
netraClient.on(NetworkEvent.offline(() {
print("offline");
}));
netraClient.on(NetworkEvent.slowNetwork(() {
print("slow network");
}));
netraClient.on(NetworkEvent.connectionRestored(() {
print("connection restored");
}));
| Event | Description |
|---|---|
offline |
Device lost internet connection |
slowNetwork |
Network is congested or degraded |
connectionRestored |
Internet connection is back |
Circuit Breaker #
Automatically stops sending requests after a threshold of consecutive failures.
final netraClient = NetraClient(
baseUrl: "https://api.example.com",
circuitBreakerOptions: CircuitBreakerOptions(
failureThreshold: 5,
retryDelayMs: 1000,
),
);
Roadmap #
- โ iOS support
- โ WebSocket support
- โ Request deduplication
- โ Logging interceptor
License #
MIT License