fl_smart_http 1.0.1
fl_smart_http: ^1.0.1 copied to clipboard
Flutter HTTP client with AES-256-GCM encrypted caching, offline support, priority request queue, interceptors, retry back-off, rate limiting, and DI support via IFlSmartHttp.
// ignore_for_file: avoid_print
import 'package:flutter/material.dart';
import 'package:fl_smart_http/fl_smart_http.dart';
// ══════════════════════════════════════════════════════════════════════════════
// Model definitions — your own classes, zero package coupling
// ══════════════════════════════════════════════════════════════════════════════
class User {
final int id;
final String name;
final String email;
const User({required this.id, required this.name, required this.email});
factory User.fromJson(dynamic json) => User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
@override
String toString() => 'User(id: $id, name: $name, email: $email)';
}
class Post {
final int id;
final String title;
final String body;
const Post({required this.id, required this.title, required this.body});
factory Post.fromJson(dynamic json) => Post(
id: json['id'] as int,
title: json['title'] as String,
body: json['body'] as String,
);
@override
String toString() => 'Post(id: $id, title: $title)';
}
// ══════════════════════════════════════════════════════════════════════════════
// main() — initialise once, register models, run app
// ══════════════════════════════════════════════════════════════════════════════
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// ── 1. Initialise with baseUrl + interceptors ────────────────────────────
await FlSmartHttp.init(
baseUrl: 'https://jsonplaceholder.typicode.com',
config: FlHttpConfig(
defaultCacheDuration: const Duration(hours: 1),
defaultRetryAttempts: 3,
defaultStrategy: CacheStrategy.cacheFirst,
serveStaleOnOffline: true,
logLevel: FlLogLevel.verbose,
// Interceptors are executed in order for every request/response
interceptors: [
LoggingInterceptor(), // pretty-prints every request & response
// AuthRefreshInterceptor(
// onRefresh: () async => await authService.refreshToken(),
// ),
],
// Optional rate limiting
maxRequestsPerSecond: 10,
// Optional encryption — uncomment + store key securely in production:
// enableEncryption: true,
// encryptionKey: 'your-32-character-secret-key!!!!',
),
);
// ── 2. Register models once — no fromJson at call sites ──────────────────
FlSmartHttp.registerModel<User>(User.fromJson);
FlSmartHttp.registerModel<List<User>>(
(json) => (json as List).map((e) => User.fromJson(e)).toList(),
);
FlSmartHttp.registerModel<Post>(Post.fromJson);
FlSmartHttp.registerModel<List<Post>>(
(json) => (json as List).map((e) => Post.fromJson(e)).toList(),
);
runApp(const MyApp());
}
// ══════════════════════════════════════════════════════════════════════════════
// DI examples (pick ONE pattern for your app — they all work)
// ══════════════════════════════════════════════════════════════════════════════
// ── Provider ─────────────────────────────────────────────────────────────────
// MultiProvider(providers: [
// Provider<IFlSmartHttp>.value(value: FlSmartHttp.instance),
// ]);
// Usage: context.read<IFlSmartHttp>().get<User>(...)
// ── Riverpod ──────────────────────────────────────────────────────────────────
// final httpProvider = FutureProvider<IFlSmartHttp>((ref) async {
// final http = await FlSmartHttp.init(baseUrl: 'https://api.example.com');
// ref.onDispose(() => http.dispose());
// return http;
// });
// ── GetIt ─────────────────────────────────────────────────────────────────────
// GetIt.I.registerSingletonAsync<IFlSmartHttp>(
// () => FlSmartHttp.init(baseUrl: 'https://api.example.com'),
// );
// await GetIt.I.allReady();
// ── Bloc ──────────────────────────────────────────────────────────────────────
// BlocProvider(create: (_) => UserBloc(context.read<IFlSmartHttp>()))
// ── Multiple isolated clients (multi-baseUrl) ─────────────────────────────────
// final authClient = await FlSmartHttp.create(
// baseUrl: 'https://auth.example.com',
// config: FlHttpConfig(hiveBoxName: 'auth_cache'),
// );
// GetIt.I.registerSingleton<IFlSmartHttp>(authClient, instanceName: 'auth');
// ══════════════════════════════════════════════════════════════════════════════
// App
// ══════════════════════════════════════════════════════════════════════════════
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'FlSmartHttp Demo',
theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
home: const DemoScreen(),
);
}
}
class DemoScreen extends StatefulWidget {
const DemoScreen({super.key});
@override
State<DemoScreen> createState() => _DemoScreenState();
}
class _DemoScreenState extends State<DemoScreen> {
final _http = FlSmartHttp.instance;
String _output = 'Tap a button to try a request';
bool _loading = false;
CancellationToken? _activeToken;
// ── Example 1: GET with registered model ─────────────────────────────────
Future<void> _getUser() async {
_activeToken = CancellationToken();
final res = await _http.get<User>(
endpoint: '/users/1',
// token: 'your_access_token', // uncomment when using a real API
cacheResponse: true,
retryPolicy: 3,
cacheDuration: const Duration(minutes: 30),
cancelToken: _activeToken,
);
setState(() {
_output = res.isSuccess
? '✅ GET User\n'
'Source: ${res.source.name}\n'
'Elapsed: ${res.elapsed.inMilliseconds}ms\n'
'Expires: ${res.expiresAt?.toLocal()}\n\n'
'data → ${res.data}'
: '❌ ${res.errorMessage}';
});
}
// ── Example 2: GET list with registered model ─────────────────────────────
Future<void> _getUsers() async {
final res = await _http.get<List<User>>(
endpoint: '/users',
cacheResponse: true,
retryPolicy: 2,
cacheDuration: const Duration(hours: 1),
tag: 'user-requests', // used with cancelGroup('user-requests')
priority: RequestPriority.high, // processed before normal/low requests
);
setState(() {
_output = res.isSuccess
? '✅ GET Users (${res.data?.length} items)\n'
'Source: ${res.source.name}\n'
'${res.data?.take(3).join('\n')}'
: '❌ ${res.errorMessage}';
});
}
// ── Example 3: GET with query params ──────────────────────────────────────
Future<void> _getPostsByUser() async {
final res = await _http.get<List<Post>>(
endpoint: '/posts',
queryParams: {'userId': '1', '_limit': '5'},
cacheResponse: true,
cacheDuration: const Duration(minutes: 10),
);
setState(() {
_output = res.isSuccess
? '✅ GET Posts (userId=1)\n'
'Count: ${res.data?.length}\n'
'${res.data?.map((p) => p.title).join('\n')}'
: '❌ ${res.errorMessage}';
});
}
// ── Example 4: GET WITHOUT model — returns raw body ───────────────────────
// Use this when T is omitted or dynamic; data will be null but rawBody has content.
Future<void> _getRaw() async {
final res = await _http.get(
endpoint: '/posts/1',
cacheResponse: true,
);
setState(() {
_output = res.isSuccess
? '✅ Raw GET\nSource: ${res.source.name}\n\n${res.rawBody}'
: '❌ ${res.errorMessage}';
});
}
// ── Example 5: GET with inline fromJson (no registration needed) ──────────
Future<void> _getInlineFactory() async {
final res = await _http.get<Post>(
endpoint: '/posts/2',
fromJson: Post.fromJson, // one-off override — skips the registry
cacheResponse: true,
);
setState(() {
_output = res.isSuccess
? '✅ Inline factory\n${res.data}'
: '❌ ${res.errorMessage}';
});
}
// ── Example 6: POST with body ─────────────────────────────────────────────
Future<void> _createPost() async {
final res = await _http.post<Post>(
endpoint: '/posts',
body: {
'title': 'Flutter caching is easy',
'body': 'Using fl_smart_http',
'userId': 1,
},
);
setState(() {
_output = res.isSuccess
? '✅ POST Created\nStatus: ${res.statusCode}\n${res.data}'
: '❌ ${res.errorMessage}';
});
}
// ── Example 7: PUT (full update) ──────────────────────────────────────────
Future<void> _updatePost() async {
final res = await _http.put<Post>(
endpoint: '/posts/1',
body: {'title': 'Updated title', 'body': 'Updated body', 'userId': 1},
);
setState(() {
_output = res.isSuccess
? '✅ PUT Updated\n${res.data}'
: '❌ ${res.errorMessage}';
});
}
// ── Example 8: PATCH (partial update) ────────────────────────────────────
Future<void> _patchPost() async {
final res = await _http.patch<Post>(
endpoint: '/posts/1',
body: {'title': 'Patched title only'},
);
setState(() {
_output = res.isSuccess
? '✅ PATCH\nStatus: ${res.statusCode}\n${res.data}'
: '❌ ${res.errorMessage}';
});
}
// ── Example 9: DELETE + invalidate cache ─────────────────────────────────
Future<void> _deletePost() async {
final res = await _http.delete(endpoint: '/posts/1');
await _http.invalidate('/posts/1'); // remove stale cache entry
setState(() {
_output = res.isSuccess
? '✅ DELETE ${res.statusCode} — cache invalidated'
: '❌ ${res.errorMessage}';
});
}
// ── Example 10: Stale-while-revalidate per request ────────────────────────
// Returns cached data immediately (even if stale) and refreshes in background.
Future<void> _swr() async {
final res = await _http.get<List<Post>>(
endpoint: '/posts',
strategy: CacheStrategy.staleWhileRevalidate,
cacheDuration: const Duration(seconds: 30),
cacheResponse: true,
);
setState(() {
_output = '✅ Stale-While-Revalidate\n'
'Source: ${res.source.name}\n'
'Is stale: ${res.isStale}\n'
'Hit count: ${res.cacheHitCount}';
});
}
// ── Example 11: Cancel active request ────────────────────────────────────
void _cancelActive() {
_activeToken?.cancel('User cancelled');
setState(() => _output = '🚫 Request cancelled');
}
// ── Example 12: Cancel group ──────────────────────────────────────────────
// Cancels all pending/active requests that were tagged 'user-requests'.
void _cancelGroup() {
_http.cancelGroup('user-requests');
setState(() => _output = '🚫 Group "user-requests" cancelled');
}
// ── Example 13: Pause / resume queue ──────────────────────────────────────
// Paused queue keeps requests pending; they run when resumed.
void _pauseQueue() {
_http.pauseQueue();
setState(() => _output = '⏸ Queue paused — new requests will wait');
}
void _resumeQueue() {
_http.resumeQueue();
setState(() => _output = '▶ Queue resumed');
}
// ── Example 14: Cache prewarming ──────────────────────────────────────────
// Seeds the cache with a known response before the first network call.
Future<void> _prewarm() async {
await _http.prewarm(
'/config',
'{"theme":"dark","version":"1.0","maintenance":false}',
duration: const Duration(days: 7),
);
// Next get() for /config will be served instantly from cache
final res = await _http.get(endpoint: '/config');
setState(() {
_output = '✅ Prewarmed + served from cache\n'
'Source: ${res.source.name}\n'
'${res.rawBody}';
});
}
// ── Example 15: Cache invalidation ───────────────────────────────────────
Future<void> _invalidate() async {
await _http.invalidate('/users/1'); // single endpoint
await _http.invalidateByPrefix('/posts'); // all /posts/* entries
setState(() => _output = '✅ Invalidated /users/1 and all /posts/* entries');
}
// ── Example 16: Diagnostics ───────────────────────────────────────────────
Future<void> _diagnostics() async {
final d = _http.diagnostics();
setState(() {
_output = 'Diagnostics:\n'
'${d.entries.map((e) => ' ${e.key}: ${e.value}').join('\n')}';
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('FlSmartHttp Demo'),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
actions: [
// Real-time connectivity indicator
StreamBuilder<bool>(
stream: _http.connectivityStream,
initialData: _http.isOnline,
builder: (_, snap) => Padding(
padding: const EdgeInsets.only(right: 12),
child: Icon(
snap.data! ? Icons.wifi : Icons.wifi_off,
color: snap.data! ? Colors.greenAccent : Colors.redAccent,
),
),
),
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Real-time queue status bar
StreamBuilder<QueueStatus>(
stream: _http.queueStatus,
builder: (_, snap) {
final s = snap.data;
if (s == null) return const SizedBox.shrink();
return Container(
width: double.infinity,
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: Colors.indigo.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Queue — pending: ${s.pending} | active: ${s.active} '
'| done: ${s.completed} | cancelled: ${s.cancelled}',
style: const TextStyle(fontSize: 11),
),
);
},
),
// Output panel
Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF1E1E2E),
borderRadius: BorderRadius.circular(12),
),
child: _loading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
child: Text(
_output,
style: const TextStyle(
color: Color(0xFFCDD6F4),
fontFamily: 'monospace',
fontSize: 12.5,
),
),
),
),
),
const SizedBox(height: 12),
// Button grid
Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.center,
children: [
_asyncBtn('GET User<T>', _getUser, Colors.green),
_asyncBtn('GET List<T>', _getUsers, Colors.green),
_asyncBtn('GET + QueryParams', _getPostsByUser, Colors.teal),
_asyncBtn('GET Raw (no T)', _getRaw, Colors.blueGrey),
_asyncBtn('GET Inline Factory', _getInlineFactory, Colors.cyan),
_asyncBtn('POST + Body', _createPost, Colors.orange),
_asyncBtn('PUT', _updatePost, Colors.deepOrange),
_asyncBtn('PATCH', _patchPost, Colors.deepPurple),
_asyncBtn('DELETE', _deletePost, Colors.red),
_asyncBtn('Stale-While-Revalidate', _swr, Colors.purple),
_btn('Cancel Active', _cancelActive, Colors.redAccent),
_btn('Cancel Group', _cancelGroup, Colors.red.shade800),
_btn('Pause Queue', _pauseQueue, Colors.amber),
_btn('Resume Queue', _resumeQueue, Colors.lightGreen),
_asyncBtn('Prewarm Cache', _prewarm, Colors.indigo),
_asyncBtn('Invalidate Cache', _invalidate, Colors.brown),
_asyncBtn('Diagnostics', _diagnostics, Colors.grey),
],
),
],
),
),
);
}
/// Async button: shows loading spinner while the callback is running.
Widget _asyncBtn(String label, Future<void> Function() onTap, Color color) =>
ElevatedButton(
onPressed: _loading
? null
: () {
setState(() {
_loading = true;
_output = '⏳ Loading...';
});
onTap().catchError((Object e) {
setState(() => _output = '❌ Error: $e');
}).whenComplete(() => setState(() => _loading = false));
},
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
textStyle: const TextStyle(fontSize: 12),
),
child: Text(label),
);
/// Sync button: for non-async operations (cancel, pause, resume).
Widget _btn(String label, VoidCallback onTap, Color color) =>
ElevatedButton(
onPressed: onTap,
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
textStyle: const TextStyle(fontSize: 12),
),
child: Text(label),
);
}