juice_network
A reactive HTTP client bloc for Juice applications with Dio integration, intelligent caching, request coalescing, and automatic retry.
Features
- Request Coalescing - Automatically deduplicates concurrent identical requests, reducing network traffic and server load
- Intelligent Caching - Multiple cache policies (networkFirst, cacheFirst, staleWhileRevalidate, cacheOnly, networkOnly)
- Automatic Retry - Configurable retry with exponential backoff for failed requests
- Request Tracking - Real-time visibility into inflight requests and their status
- Statistics - Built-in metrics for cache hits, success rates, response times, and more
- Dio Integration - Full access to Dio's powerful HTTP features
Installation
dependencies:
juice_network: ^0.7.1
Quick Start
1. Initialize the Blocs
import 'package:juice/juice.dart';
import 'package:juice_network/juice_network.dart';
import 'package:juice_storage/juice_storage.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Register StorageBloc (required for caching)
BlocScope.register<StorageBloc>(
() => StorageBloc(config: const StorageConfig(
hiveBoxesToOpen: [CacheManager.cacheBoxName],
)),
lifecycle: BlocLifecycle.permanent,
);
final storageBloc = BlocScope.get<StorageBloc>();
await storageBloc.initialize();
// Register FetchBloc
BlocScope.register<FetchBloc>(
() => FetchBloc(storageBloc: storageBloc),
lifecycle: BlocLifecycle.permanent,
);
// Initialize with configuration
final fetchBloc = BlocScope.get<FetchBloc>();
await fetchBloc.send(InitializeFetchEvent(
config: FetchConfig(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
defaultTtl: const Duration(minutes: 5),
),
));
runApp(MyApp());
}
2. Make Requests
// Simple GET request
fetchBloc.send(GetEvent(
url: '/users/1',
decode: (json) => User.fromJson(json),
));
// With cache policy
fetchBloc.send(GetEvent(
url: '/posts',
cachePolicy: CachePolicy.cacheFirst,
ttl: const Duration(minutes: 10),
decode: (json) => (json as List).map((e) => Post.fromJson(e)).toList(),
));
// POST request
fetchBloc.send(PostEvent(
url: '/posts',
body: {'title': 'Hello', 'body': 'World'},
decode: (json) => Post.fromJson(json),
));
3. React to State Changes
class PostsWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final fetchBloc = BlocScope.get<FetchBloc>();
return JuiceAsyncBuilder<StreamStatus<FetchState>>(
stream: fetchBloc.stream,
initial: StreamStatus.updating(fetchBloc.state, fetchBloc.state, null),
builder: (context, status) {
if (status is WaitingStatus) {
return CircularProgressIndicator();
}
if (status is FailureStatus) {
return Text('Error: ${fetchBloc.state.lastError}');
}
return Text('Loaded successfully');
},
);
}
}
Request Coalescing
One of juice_network's key features is request coalescing - automatic deduplication of concurrent identical requests.
How It Works
When multiple parts of your app request the same resource simultaneously:
- The first request goes to the network
- Subsequent identical requests attach to the existing inflight request
- All callers receive the same response when it arrives
- Only one network call is made
Example
// User rapidly taps a button, or multiple widgets request the same data
for (var i = 0; i < 10; i++) {
fetchBloc.send(GetEvent(
url: '/posts/1',
cachePolicy: CachePolicy.networkOnly,
));
}
// Result: 1 network call, 9 coalesced requests
// All 10 callers get the same response
Benefits
- Reduced server load - Prevents duplicate requests from hammering your API
- Lower bandwidth usage - Only one request travels over the network
- Consistent data - All consumers receive the same response
- No code changes needed - Coalescing happens automatically
Statistics
Track coalescing effectiveness via FetchState.stats:
final stats = fetchBloc.state.stats;
print('Total requests: ${stats.totalRequests}');
print('Coalesced: ${stats.coalescedCount}');
Cache Policies
| Policy | Behavior |
|---|---|
networkFirst |
Try network, fall back to cache on failure |
cacheFirst |
Use cache if available, otherwise fetch from network |
staleWhileRevalidate |
Return cached data immediately, refresh in background |
cacheOnly |
Only use cached data, fail if not available |
networkOnly |
Always fetch from network, never cache |
fetchBloc.send(GetEvent(
url: '/data',
cachePolicy: CachePolicy.staleWhileRevalidate,
ttl: const Duration(hours: 1),
));
Configuration
FetchConfig(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
sendTimeout: const Duration(seconds: 30),
defaultTtl: const Duration(minutes: 5),
defaultCachePolicy: CachePolicy.networkFirst,
maxRetries: 3,
headers: {'Authorization': 'Bearer token'},
)
Events
| Event | Description |
|---|---|
InitializeFetchEvent |
Initialize with configuration |
GetEvent |
HTTP GET request |
PostEvent |
HTTP POST request |
PutEvent |
HTTP PUT request |
PatchEvent |
HTTP PATCH request |
DeleteEvent |
HTTP DELETE request |
ClearCacheEvent |
Clear all cached responses |
ResetStatsEvent |
Reset statistics counters |
Rebuild Groups
Subscribe to specific state changes:
fetch:inflight- Inflight request count changesfetch:stats- Statistics updatesfetch:cache- Cache state changesfetch:request:{METHOD}:{path}- Specific request status
Statistics
Access detailed metrics:
final stats = fetchBloc.state.stats;
// Request metrics
stats.totalRequests
stats.successCount
stats.failureCount
stats.successRate
stats.retryCount
stats.coalescedCount
// Cache metrics
stats.cacheHits
stats.cacheMisses
stats.hitRate
// Performance
stats.avgResponseTimeMs
stats.bytesReceived
stats.bytesSent
Example App
See the example directory for a complete demo app showcasing:
- Different cache policies
- Request coalescing demonstration
- Real-time statistics dashboard
License
MIT License - see LICENSE for details.
Libraries
- juice_network
- Unified network BLoC for Flutter - request coalescing, caching, retry, and interceptors built on the Juice framework.