autopilot_api 2.0.1
autopilot_api: ^2.0.1 copied to clipboard
Zero dependency pure Dart API engine for Flutter with smart caching, retry, token refresh, multipart upload, download support and automatic parsing.
█████╗ ██╗ ██╗████████╗ ██████╗ ██████╗ ██╗██╗ ██████╗ ████████╗
██╔══██╗██║ ██║╚══██╔══╝██╔═══██╗██╔══██╗██║██║ ██╔═══██╗╚══██╔══╝
███████║██║ ██║ ██║ ██║ ██║██████╔╝██║██║ ██║ ██║ ██║
██╔══██║██║ ██║ ██║ ██║ ██║██╔═══╝ ██║██║ ██║ ██║ ██║
██║ ██║╚██████╔╝ ██║ ╚██████╔╝██║ ██║███████╗╚██████╔╝ ██║
╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝
⚡ Zero-Dependency Smart API Engine for Flutter #
Pure Dart • dart:io HttpClient • Zero Dependencies • Production Ready
Features · Install · Quick Start · API Ref · State Managers
🎯 The Problem #
Every Flutter developer has written this at least once:
// ❌ The painful reality — 50+ lines for a single API call
Future<UserModel?> getUser(int id) async {
try {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString('token') ?? '';
final request = await HttpClient().getUrl(Uri.parse('$baseUrl/users/$id'));
request.headers.set('Authorization', 'Bearer $token');
request.headers.set('Content-Type', 'application/json');
final response = await request.close().timeout(const Duration(seconds: 30));
final body = await response.transform(utf8.decoder).join();
final json = jsonDecode(body);
if (response.statusCode == 401) { /* refresh token... */ }
if (response.statusCode == 422) { /* validation errors... */ }
if (json['status'] == true) return UserModel.fromJson(json['data']);
throw Exception(json['message']);
} on SocketException { throw Exception('No internet'); }
on TimeoutException { throw Exception('Timed out'); }
catch (e) { rethrow; }
}
Multiply this by every endpoint in your app. Thousands of lines of identical, bug-prone boilerplate.
✅ The AutoPilot Solution #
// ✅ The only code you need to write
final res = await api.get(
endpoint : '/users/1',
parser : UserModel.fromJson,
);
if (res.isSuccess) print(res.data);
else print(res.message);
AutoPilot handles everything automatically:
| Feature | What it does |
|---|---|
| 🔑 Token injection | Reads token from storage, adds to every request |
| 🔄 Token refresh | On 401 → refresh → retry silently |
| 🌐 Internet check | Detects offline before hitting network |
| ⏱ Timeouts | Configurable, typed exception on expiry |
| 🔁 Retry | Exponential backoff, configurable attempts |
| 💾 Caching | Memory + disk, auto-expiry, per-request |
| ⚡ Deduplication | Same concurrent GETs → 1 network call |
| 📦 Parsing | JSON → your model via parser function |
| 🎨 Debug logs | Colored logs with full payload, auto-off in release |
✨ Features #
┌─────────────────────────────────────────────────────────────────────┐
│ AUTOPILOT ZERO v2.0.0 │
├─────────────────────────┬───────────────────────────────────────────┤
│ HTTP Methods │ GET · POST · PUT · PATCH · DELETE │
│ File Operations │ Multipart Upload · File Download │
│ Auth │ Auto Token Inject · Auto Token Refresh │
│ Caching │ Memory Layer · Disk Layer · Auto Expiry │
│ Reliability │ Retry · Exponential Backoff · Dedup │
│ Networking │ Pure dart:io · Zero dependencies │
│ Storage │ Pure Dart local storage (no plugins) │
│ Connectivity │ Native socket lookup │
│ Logging │ Colored ANSI · Full payload · Auto-off │
│ State Managers │ GetX · Bloc · Riverpod · Provider · MobX│
│ Extensions │ .handle() · .mapData() · .onSuccess() │
│ Dependencies │ ZERO ← this is the headline │
└─────────────────────────┴───────────────────────────────────────────┘
🔥 v2.0.0 — Zero Dependencies #
| Removed Package | Replaced With | Benefit |
|---|---|---|
http |
dart:io HttpClient |
No external dep, full control |
shared_preferences |
Pure Dart JSON file storage | No platform channels |
connectivity_plus |
InternetAddress.lookup() |
Works everywhere, zero config |
path |
Internal string utils | Smaller binary |
mime |
Internal MIME resolver | No conflicts |
# Before — 5 external dependencies
dependencies:
http: ^1.2.2
shared_preferences: ^2.3.3
connectivity_plus: ^6.0.0
path: ^1.9.0
mime: ^1.0.6
# After — ZERO
dependencies:
flutter:
sdk: flutter
Smaller APK. Faster builds. Zero version conflicts. Zero native setup.
📦 Installation #
dependencies:
autopilot_api: ^2.0.0
flutter pub get
No
pod install. No gradle changes. No native setup. Just add and go.
🚀 60-Second Quickstart #
// Step 1 — Initialize once in main()
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await AutoPilotApi.init(
baseUrl : 'https://api.example.com/v1',
enableLogs : true, // 🎨 colored logs in debug
printPayload : true, // 📋 full JSON in console
enableCache : true, // 💾 auto cache GET responses
maxRetries : 3, // 🔁 auto retry on failure
);
runApp(const MyApp());
}
// Step 2 — Use anywhere, nothing else needed
final api = AutoPilotApi.instance;
final res = await api.get(
endpoint : '/users/1',
parser : UserModel.fromJson,
);
if (res.isSuccess) {
print(res.data); // ✅ UserModel
print(res.message); // "Success"
print(res.statusCode); // 200
print(res.responseTime?.inMilliseconds); // 245 (ms)
} else {
print(res.message); // ❌ error description
print(res.statusCode); // 404
}
// Step 3 — Set token after login (auto-injected from now on)
await AutoPilotApi.setToken(loginRes.data!.token);
🌟 All Requests #
// ── GET single ─────────────────────────────────────────────────────────────
final res = await api.get<UserModel>(
endpoint : '/users/1',
parser : UserModel.fromJson,
);
// ── GET list ───────────────────────────────────────────────────────────────
final res = await api.get<List<UserModel>>(
endpoint : '/users',
parser : (json) => (json as List).map(UserModel.fromJson).toList(),
);
// ── GET with query params → /posts?userId=1&page=2 ────────────────────────
final res = await api.get<List<PostModel>>(
endpoint : '/posts',
queryParams : {'userId': 1, 'page': 2, 'limit': 10},
parser : (json) => (json as List).map(PostModel.fromJson).toList(),
);
// ── GET with cache ─────────────────────────────────────────────────────────
final res = await api.get<ConfigModel>(
endpoint : '/app/config',
parser : ConfigModel.fromJson,
useCache : true,
cacheDuration : const Duration(hours: 24),
);
// ── POST ───────────────────────────────────────────────────────────────────
final res = await api.post<LoginModel>(
endpoint : '/auth/login',
body : {'email': email, 'password': password},
parser : LoginModel.fromJson,
);
// ── PUT / PATCH / DELETE ───────────────────────────────────────────────────
await api.put(endpoint: '/posts/1', body: {'title': 'New'});
await api.patch(endpoint: '/users/1', body: {'name': 'Updated'});
await api.delete(endpoint: '/posts/1');
// ── MULTIPART — single file ────────────────────────────────────────────────
await api.multipart(
endpoint : '/profile/photo',
fileKey : 'image',
filePath : pickedFile.path,
);
// ── MULTIPART — multiple files + fields + progress ────────────────────────
await api.multipart<UploadModel>(
endpoint : '/documents',
files : [
MultipartFileModel(key: 'doc', path: file.path,
fileName: 'report.pdf', mimeType: 'application/pdf'),
],
fields : {'title': 'Q4 Report', 'year': '2025'},
onProgress : (sent, total) =>
setState(() => progress = sent / total),
parser : UploadModel.fromJson,
);
// ── MULTIPART — from bytes (no file path needed) ──────────────────────────
await api.multipart(
endpoint : '/upload',
files : [
MultipartFileModel.fromBytes(
key: 'avatar', bytes: imageBytes, fileName: 'avatar.png'),
],
);
// ── DOWNLOAD with progress ─────────────────────────────────────────────────
final res = await api.download(
endpoint : '/reports/jan.pdf',
savePath : '/storage/Download/jan.pdf',
onProgress : (recv, total) => print('${(recv/total*100).toInt()}%'),
);
if (res.isSuccess) print('Saved: ${res.data}');
📦 ApiResponse<T> #
res.isSuccess // bool — did it succeed?
res.data // T? — parsed model
res.message // String — server or error message
res.statusCode // int — 200, 201, 400, 401, 422, 500...
res.raw // dynamic — raw JSON body
res.errors // Map? — validation errors map
res.requestId // String? — tracing ID e.g. "a3f2b1c9"
res.responseTime // Duration? — round-trip time
res.timestamp // DateTime — when received
// Status helpers
res.isClientError // 400–499
res.isServerError // 500+
res.isUnauthorized // 401
res.isForbidden // 403
res.isNotFound // 404
res.isValidationError // 422
res.isTimeout // 408
res.isNoInternet // 0
// Data helpers
res.dataOr(fallback) // safe access with fallback
res.dataOrThrow // throws if null
res.validationError('email') // first error for field
res.allErrors // all validation errors as string
res.cast<R>(newData) // cast to different type
🎣 Extension Methods #
// .handle() — no if/else needed
await api.get(endpoint: '/me', parser: UserModel.fromJson)
.handle(
onSuccess : (data, msg) => setState(() => user = data),
onFailure : (msg, code) => showSnackbar(msg),
);
// .mapData() — transform data type in chain
final res = await api
.get<String>(endpoint: '/stats/total')
.mapData((s) => int.parse(s)); // ApiResponse<int>
// .onSuccess() / .onFailure() — side effects, returns original
await api.post(endpoint: '/order', body: data)
.onSuccess((_) => Analytics.log('order_placed'))
.onFailure((msg, __) => Crashlytics.record(msg))
.handle(
onSuccess : (data, _) => navigateTo(SuccessPage(data)),
onFailure : (msg, _) => showErrorDialog(msg),
);
🔐 Token Management #
await AutoPilotApi.setToken(token); // set access token
await AutoPilotApi.setTokens(accessToken: a, refreshToken: r); // set both
await AutoPilotApi.clearTokens(); // logout
// Auto refresh on 401
await AutoPilotApi.init(
enableTokenRefresh : true,
onRefreshToken : () async {
final r = await TokenManager.getRefreshToken();
final res = await refreshEndpoint(r);
return res.newToken; // returned → saved → request retried
},
);
💾 Caching #
// Global
await AutoPilotApi.init(enableCache: true, cacheDuration: Duration(minutes: 5));
// Per-request override
await api.get(endpoint: '/config', useCache: true, cacheDuration: Duration(hours: 1));
// Invalidate
await api.invalidateCache('/config');
await api.clearCache();
⚙️ Full Configuration #
await AutoPilotApi.init(
baseUrl : 'https://api.example.com/v1', // required
tokenType : 'Bearer',
timeoutSeconds : 30,
maxRetries : 3,
retryDelay : Duration(seconds: 1),
enableCache : true,
cacheDuration : Duration(minutes: 5),
enableLogs : true, // auto false in release
printPayload : true, // print full JSON
prettyPrint : true,
enableTokenRefresh : true,
onRefreshToken : () async => newToken,
enableGlobalLoader : true,
onLoadingChanged : (v) => AppController.setLoading(v),
onError : (msg, code) => showSnackbar(msg),
onRequestSent : (url, method) => Analytics.log(url, method),
onResponseReceived : (url, code, time) => Analytics.log(url, code),
globalHeaders : {
'X-App-Version' : '2.0.0',
'X-Platform' : Platform.isAndroid ? 'android' : 'ios',
},
// match your backend's JSON envelope:
successKey : 'status', // default
successValue : true, // default
messageKey : 'message', // default
dataKey : 'data', // default
enableDeduplication : true,
);
🔌 State Manager Integration #
GetX #
class UserController extends GetxController {
final user = Rx<UserModel?>(null);
final isLoading = false.obs;
Future<void> fetchUser() async {
isLoading.value = true;
await AutoPilotApi.instance
.get<UserModel>(endpoint: '/users/me', parser: UserModel.fromJson)
.handle(
onSuccess : (data, _) => user.value = data,
onFailure : (msg, _) => Get.snackbar('Error', msg),
);
isLoading.value = false;
}
}
Riverpod #
class UserNotifier extends AsyncNotifier<UserModel?> {
@override Future<UserModel?> build() async => null;
Future<void> fetch() async {
state = const AsyncLoading();
final res = await AutoPilotApi.instance
.get(endpoint: '/users/me', parser: UserModel.fromJson);
state = res.isSuccess
? AsyncData(res.data)
: AsyncError(res.message, StackTrace.current);
}
}
final userProvider = AsyncNotifierProvider<UserNotifier, UserModel?>(UserNotifier.new);
Bloc / Cubit #
class UserCubit extends Cubit<UserState> {
UserCubit() : super(const UserInitial());
Future<void> fetchUser() async {
emit(const UserLoading());
final res = await AutoPilotApi.instance
.get(endpoint: '/users/me', parser: UserModel.fromJson);
emit(res.isSuccess ? UserLoaded(res.data!) : UserError(res.message));
}
}
Provider #
class UserProvider extends ChangeNotifier {
UserModel? user;
bool isLoading = false;
Future<void> fetchUser() async {
isLoading = true; notifyListeners();
final res = await AutoPilotApi.instance
.get(endpoint: '/users/me', parser: UserModel.fromJson);
user = res.data;
isLoading = false; notifyListeners();
}
}
MobX #
abstract class _UserStore with Store {
@observable UserModel? user;
@observable bool isLoading = false;
@action
Future<void> fetchUser() async {
isLoading = true;
final res = await AutoPilotApi.instance
.get(endpoint: '/users/me', parser: UserModel.fromJson);
if (res.isSuccess) user = res.data;
isLoading = false;
}
}
🌍 Runtime Environment Switch #
api.reconfigure((c) => c.copyWith(baseUrl: 'https://staging.api.com'));
api.reconfigure((c) => c.copyWith(baseUrl: 'https://api.example.com'));
api.reconfigure((c) => c.copyWith(timeoutSeconds: 60)); // slow network
🎨 Debug Logs #
┌────────────────────────────────────────────────────
│ 🚀 REQUEST [a3f2b1c9] 14:32:01.432
│ POST https://api.example.com/v1/auth/login
│ ⊳ Headers
│ Authorization: ***masked***
│ ⊳ Body
│ {
│ "email": "john@example.com",
│ "password": "••••••••"
│ }
└────────────────────────────────────────────────────
┌────────────────────────────────────────────────────
│ ✅ RESPONSE [a3f2b1c9] 14:32:01.687
│ Status: 200 ⏱ 255ms
│ ⊳ Payload
│ {
│ "status": true,
│ "message": "Login successful",
│ "data": { "token": "eyJ...", "user": { "id": 1 } }
│ }
└────────────────────────────────────────────────────
┌────────────────────────────────────────────────────
│ 💥 ERROR [b7d3e2f1] 14:35:22.901
│ Status: 422
│ Validation failed
│ ⊳ Error Body
│ { "errors": { "email": ["already taken"] } }
└────────────────────────────────────────────────────
Auto-disabled in release builds. Zero configuration needed.
📐 Architecture #
autopilot_zero/
├── lib/
│ ├── autopilot_zero.dart ← single import
│ ├── core/autopilot_core.dart ← 🧠 main engine (dart:io)
│ ├── models/ ← ApiResponse, Config, FileModel
│ ├── storage/ap_storage.dart ← 💾 pure Dart file storage
│ ├── auth/token_manager.dart ← memory + disk token
│ ├── cache/cache_service.dart ← two-layer cache
│ ├── connectivity/ ← socket lookup
│ ├── exceptions/ ← typed exception hierarchy
│ ├── extensions/ ← .handle() .mapData() .onSuccess()
│ ├── logger/ ← colored ANSI logger
│ ├── parsers/ ← smart JSON parser
│ ├── queue/ ← deduplication
│ └── retry/ ← exponential backoff
├── example/lib/main.dart ← full demo app
├── test/autopilot_zero_test.dart ← 30+ unit tests
└── pubspec.yaml ← ZERO external deps
📋 API Reference #
| Method | Description |
|---|---|
AutoPilotApi.init(...) |
Initialize — call once in main() |
AutoPilotApi.instance |
Get singleton |
AutoPilotApi.setToken(t) |
Save access token |
AutoPilotApi.setTokens(...) |
Save access + refresh token |
AutoPilotApi.clearTokens() |
Logout |
api.get(...) |
GET request |
api.post(...) |
POST request |
api.put(...) |
PUT request |
api.patch(...) |
PATCH request |
api.delete(...) |
DELETE request |
api.multipart(...) |
File upload (single/multiple) |
api.download(...) |
File download with progress |
api.invalidateCache(e) |
Clear cache for endpoint |
api.clearCache() |
Clear all cache |
api.reconfigure(fn) |
Update config at runtime |
api.dispose() |
Close HTTP client |
⚡ Performance #
┌──────────────────┬─────────────┬────────────────┐
│ Metric │ Traditional │ AutoPilot Zero │
├──────────────────┼─────────────┼────────────────┤
│ Boilerplate LOC │ 50+/endpoint│ 4/endpoint │
│ Dependencies │ 5–10 │ 0 │
│ Token read time │ ~10ms (disk)│ ~0ms (memory) │
│ Duplicate GETs │ All hit net │ 1 hits net │
│ APK size impact │ +200–500KB │ +0KB │
└──────────────────┴─────────────┴────────────────┘
🧪 Testing #
flutter test
flutter test --coverage
🗺 Roadmap #
- ✅ v1.0.0 — Core engine (
http+shared_preferences) - ✅ v2.0.0 — Zero dependencies (pure
dart:io) - ❌ v2.1.0 — WebSocket support
- ❌ v2.2.0 — GraphQL support
- ❌ v2.3.0 — Offline request queue
- ❌ v3.0.0 — Code generator (
@GET('/users')annotations)
📄 License #
MIT License © 2025 AutoPilot API
Built with ❤️ for the Flutter community
⭐ Star on GitHub · 📦 pub.dev · 🐛 Issues
If AutoPilot saved you hours of boilerplate, consider starring the repo ⭐