autopilot_api 2.0.0
autopilot_api: ^2.0.0 copied to clipboard
Zero dependency pure Dart API engine for Flutter with smart caching, retry, token refresh, multipart upload, download support and automatic parsing.
example/lib/main.dart
import 'package:autopilot_api/core/autopilot_core.dart';
import 'package:flutter/material.dart';
// ─── Models ───────────────────────────────────────────────────────────────────
class UserModel {
final int id;
final String name;
final String email;
final String username;
const UserModel({
required this.id,
required this.name,
required this.email,
required this.username,
});
factory UserModel.fromJson(dynamic json) => UserModel(
id: json['id'] as int,
name: json['name'].toString(),
email: json['email'].toString(),
username: json['username'].toString(),
);
@override
String toString() =>
'User #$id\nName : $name\nEmail : $email\nUsername : $username';
}
class PostModel {
final int id;
final String title;
final String body;
const PostModel({required this.id, required this.title, required this.body});
factory PostModel.fromJson(dynamic json) => PostModel(
id: json['id'] as int,
title: json['title'].toString(),
body: json['body'].toString(),
);
@override
String toString() => 'Post #$id\nTitle : $title';
}
// ─────────────────────────────────────────────────────────────────────────────
// VANILLA FLUTTER STATE MANAGER (ChangeNotifier)
// Works exactly the same with GetX, Riverpod, Bloc, Provider, MobX
// ─────────────────────────────────────────────────────────────────────────────
class ApiDemoController extends ChangeNotifier {
final _api = AutoPilotApi.instance;
String output = 'Press a button to test 🚀';
bool isLoading = false;
void _setLoading(bool v) {
isLoading = v;
notifyListeners();
}
void _setOutput(String s) {
output = s;
notifyListeners();
}
// GET list
Future<void> getUsers() async {
_setLoading(true);
final res = await _api.get<List<UserModel>>(
endpoint: '/users',
parser: (json) => (json as List).map(UserModel.fromJson).toList(),
useCache: true,
);
_setLoading(false);
_setOutput(
res.isSuccess
? '✅ ${res.data?.length} users loaded\n\n'
'${res.data?.take(3).map((u) => u.toString()).join('\n\n')}\n\n'
'⏱ ${res.responseTime?.inMilliseconds}ms [${res.requestId}]'
: '❌ ${res.message}',
);
}
// GET single
Future<void> getUser() async {
_setLoading(true);
final res = await _api.get<UserModel>(
endpoint: '/users/1',
parser: UserModel.fromJson,
);
_setLoading(false);
_setOutput(
res.isSuccess
? '✅ User fetched!\n\n${res.data}\n\n'
'⏱ ${res.responseTime?.inMilliseconds}ms'
: '❌ ${res.message}',
);
}
// POST
Future<void> createPost() async {
_setLoading(true);
final res = await _api.post<PostModel>(
endpoint: '/posts',
body: {
'title': 'AutoPilot Zero — Zero Dependencies!',
'body': 'Pure Dart. No http package. No shared_preferences.',
'userId': 1,
},
parser: PostModel.fromJson,
);
_setLoading(false);
_setOutput(
res.isSuccess
? '✅ Post created!\n\n${res.data}\n\nStatus: ${res.statusCode}\n'
'⏱ ${res.responseTime?.inMilliseconds}ms'
: '❌ ${res.message}',
);
}
// PUT
Future<void> updatePost() async {
_setLoading(true);
final res = await _api.put<PostModel>(
endpoint: '/posts/1',
body: {
'title': 'Updated via AutoPilot Zero',
'body': 'Pure Dart HttpClient',
'userId': 1,
},
parser: PostModel.fromJson,
);
_setLoading(false);
_setOutput(
res.isSuccess ? '✅ Post updated!\n\n${res.data}' : '❌ ${res.message}',
);
}
// PATCH
Future<void> patchPost() async {
_setLoading(true);
final res = await _api.patch(
endpoint: '/posts/1',
body: {'title': 'Patched title only'},
);
_setLoading(false);
_setOutput(
res.isSuccess ? '✅ Patched!\nRaw: ${res.raw}' : '❌ ${res.message}',
);
}
// DELETE
Future<void> deletePost() async {
_setLoading(true);
final res = await _api.delete(endpoint: '/posts/1');
_setLoading(false);
_setOutput(
res.isSuccess
? '✅ Deleted!\nStatus: ${res.statusCode}'
: '❌ ${res.message}',
);
}
// Query params
Future<void> queryParams() async {
_setLoading(true);
final res = await _api.get<List<PostModel>>(
endpoint: '/posts',
queryParams: {'userId': 1, '_limit': 3},
parser: (json) => (json as List).map(PostModel.fromJson).toList(),
);
_setLoading(false);
_setOutput(
res.isSuccess
? '✅ Posts (userId=1, limit=3)\n\n'
'${res.data?.map((p) => '• ${p.title}').join('\n')}'
: '❌ ${res.message}',
);
}
// .handle() extension
Future<void> handleExtension() async {
_setLoading(true);
await _api
.get<UserModel>(endpoint: '/users/3', parser: UserModel.fromJson)
.handle(
onSuccess: (data, msg) =>
_setOutput('✅ .handle() extension!\n\n$data'),
onFailure: (msg, code) => _setOutput('❌ Failed [$code]: $msg'),
);
_setLoading(false);
}
// .onSuccess() + .onFailure() chaining
Future<void> chainExtensions() async {
_setLoading(true);
final res = await _api
.get<UserModel>(endpoint: '/users/4', parser: UserModel.fromJson)
.onSuccess((data) => debugPrint('Side effect: got ${data?.name}'))
.onFailure((msg, code) => debugPrint('Side effect error: $msg'));
_setLoading(false);
_setOutput(
res.isSuccess ? '✅ Chain extensions!\n\n${res.data}' : '❌ ${res.message}',
);
}
// Cache + Deduplication demo
Future<void> cacheDedup() async {
_setLoading(true);
_setOutput(
'🔄 Firing 3 identical GETs simultaneously...\n'
'Should deduplicate → 1 real network call',
);
final sw = Stopwatch()..start();
final results = await Future.wait([
_api.get<UserModel>(
endpoint: '/users/2',
parser: UserModel.fromJson,
useCache: true,
),
_api.get<UserModel>(
endpoint: '/users/2',
parser: UserModel.fromJson,
useCache: true,
),
_api.get<UserModel>(
endpoint: '/users/2',
parser: UserModel.fromJson,
useCache: true,
),
]);
sw.stop();
_setLoading(false);
final names = results.map((r) => r.data?.name ?? '?').toSet();
_setOutput(
'✅ 3 calls → 1 network request!\n\n'
'All same user: $names\n\n'
'Times: ${results.map((r) => '${r.responseTime?.inMilliseconds}ms').join(', ')}\n'
'Wall time: ${sw.elapsedMilliseconds}ms',
);
}
// mapData extension
Future<void> mapDataExtension() async {
_setLoading(true);
// Get user then map name to uppercase
final res = await _api
.get<UserModel>(endpoint: '/users/5', parser: UserModel.fromJson)
.mapData((user) => user.name.toUpperCase());
_setLoading(false);
_setOutput(
res.isSuccess
? '✅ .mapData() — name uppercased:\n\n${res.data}'
: '❌ ${res.message}',
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// MAIN
// ─────────────────────────────────────────────────────────────────────────────
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await AutoPilotApi.init(
baseUrl: 'https://jsonplaceholder.typicode.com',
enableLogs: true,
printPayload: true,
prettyPrint: true,
enableCache: true,
cacheDuration: const Duration(minutes: 3),
maxRetries: 2,
tokenType: 'Bearer',
enableGlobalLoader: true,
onLoadingChanged: (v) => debugPrint('⏳ Loader: $v'),
onError: (msg, code) => debugPrint('🔴 Error [$code]: $msg'),
onRequestSent: (url, m) => debugPrint('📤 $m → $url'),
onResponseReceived: (url, c, t) =>
debugPrint('📥 $c ← $url (${t.inMilliseconds}ms)'),
// match your backend's envelope
successKey: 'status',
successValue: true,
messageKey: 'message',
dataKey: 'data',
);
runApp(const MyApp());
}
// ─────────────────────────────────────────────────────────────────────────────
// APP
// ─────────────────────────────────────────────────────────────────────────────
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'AutoPilot Zero',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: ChangeNotifierProvider(
create: (_) => ApiDemoController(),
child: const DemoPage(),
),
);
}
// simple ChangeNotifierProvider inline
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget {
final T Function(BuildContext) create;
final Widget child;
const ChangeNotifierProvider({
super.key,
required this.create,
required this.child,
});
static T of<T extends ChangeNotifier>(BuildContext context) {
final scope = context.dependOnInheritedWidgetOfExactType<_Scope<T>>();
return scope!.notifier;
}
@override
State<ChangeNotifierProvider<T>> createState() =>
_ChangeNotifierProviderState<T>();
}
class _ChangeNotifierProviderState<T extends ChangeNotifier>
extends State<ChangeNotifierProvider<T>> {
late T _notifier;
@override
void initState() {
super.initState();
_notifier = widget.create(context);
_notifier.addListener(_rebuild);
}
void _rebuild() => setState(() {});
@override
void dispose() {
_notifier.removeListener(_rebuild);
_notifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) =>
_Scope<T>(notifier: _notifier, child: widget.child);
}
class _Scope<T extends ChangeNotifier> extends InheritedWidget {
final T notifier;
const _Scope({required this.notifier, required super.child});
@override
bool updateShouldNotify(_Scope<T> old) => true;
}
// ─────────────────────────────────────────────────────────────────────────────
// DEMO PAGE
// ─────────────────────────────────────────────────────────────────────────────
class DemoPage extends StatelessWidget {
const DemoPage({super.key});
@override
Widget build(BuildContext context) {
final ctrl = ChangeNotifierProvider.of<ApiDemoController>(context);
return Scaffold(
backgroundColor: const Color(0xFF090D1A),
appBar: AppBar(
backgroundColor: const Color(0xFF141928),
title: Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: Colors.indigo.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.indigo.withValues(alpha: 0.5)),
),
child: const Text(
'ZERO DEPS',
style: TextStyle(
color: Colors.indigoAccent,
fontSize: 10,
fontWeight: FontWeight.bold,
fontFamily: 'monospace',
),
),
),
const SizedBox(width: 8),
const Text(
'AutoPilot Zero',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 17,
),
),
],
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(18),
child: Padding(
padding: const EdgeInsets.only(bottom: 5),
child: Text(
'Pure Dart + Flutter SDK only • dart:io HttpClient',
style: TextStyle(color: Colors.white38, fontSize: 10),
),
),
),
),
body: Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
// ── buttons ───────────────────────────────────────────────────
Wrap(
spacing: 7,
runSpacing: 7,
children: [
_btn(
'GET List',
Icons.people,
ctrl.getUsers,
Colors.green,
ctrl.isLoading,
),
_btn(
'GET Single',
Icons.person,
ctrl.getUser,
Colors.blue,
ctrl.isLoading,
),
_btn(
'POST',
Icons.add_circle,
ctrl.createPost,
Colors.purple,
ctrl.isLoading,
),
_btn(
'PUT',
Icons.edit,
ctrl.updatePost,
Colors.orange,
ctrl.isLoading,
),
_btn(
'PATCH',
Icons.edit_note,
ctrl.patchPost,
Colors.teal,
ctrl.isLoading,
),
_btn(
'DELETE',
Icons.delete,
ctrl.deletePost,
Colors.red,
ctrl.isLoading,
),
_btn(
'?query',
Icons.filter_list,
ctrl.queryParams,
Colors.cyan,
ctrl.isLoading,
),
_btn(
'.handle()',
Icons.extension,
ctrl.handleExtension,
Colors.pink,
ctrl.isLoading,
),
_btn(
'.chain()',
Icons.link,
ctrl.chainExtensions,
Colors.lime,
ctrl.isLoading,
),
_btn(
'.mapData()',
Icons.transform,
ctrl.mapDataExtension,
Colors.amber,
ctrl.isLoading,
),
_btn(
'Cache/Dedup',
Icons.cached,
ctrl.cacheDedup,
Colors.indigo,
ctrl.isLoading,
),
],
),
const SizedBox(height: 10),
// loader
AnimatedContainer(
duration: const Duration(milliseconds: 150),
height: ctrl.isLoading ? 2 : 0,
child: LinearProgressIndicator(
backgroundColor: Colors.transparent,
valueColor: const AlwaysStoppedAnimation<Color>(
Colors.indigoAccent,
),
),
),
const SizedBox(height: 10),
// ── terminal output ────────────────────────────────────────────
Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// mac-style dots
Row(
children: [
_dot(Colors.red),
const SizedBox(width: 5),
_dot(Colors.orange),
const SizedBox(width: 5),
_dot(Colors.green),
const SizedBox(width: 10),
Text(
'autopilot_zero • pure dart:io',
style: TextStyle(
color: Colors.white24,
fontSize: 10,
fontFamily: 'monospace',
),
),
],
),
const SizedBox(height: 10),
Expanded(
child: SingleChildScrollView(
child: Text(
ctrl.output,
style: const TextStyle(
color: Colors.greenAccent,
fontFamily: 'monospace',
fontSize: 12,
height: 1.6,
),
),
),
),
],
),
),
),
const SizedBox(height: 6),
Text(
'👆 Check debug console for 🎨 colored request/response logs',
style: TextStyle(color: Colors.white24, fontSize: 10),
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _btn(
String label,
IconData icon,
VoidCallback? fn,
Color c,
bool loading,
) => ElevatedButton.icon(
onPressed: loading ? null : fn,
icon: Icon(icon, size: 12),
label: Text(label, style: const TextStyle(fontSize: 10)),
style: ElevatedButton.styleFrom(
backgroundColor: c.withValues(alpha: 0.12),
foregroundColor: c,
side: BorderSide(color: c.withValues(alpha: 0.35)),
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 6),
minimumSize: const Size(0, 0),
),
);
Widget _dot(Color c) => Container(
width: 10,
height: 10,
decoration: BoxDecoration(color: c, shape: BoxShape.circle),
);
}