█████╗ ██╗   ██╗████████╗ ██████╗ ██████╗ ██╗██╗      ██████╗ ████████╗
██╔══██╗██║   ██║╚══██╔══╝██╔═══██╗██╔══██╗██║██║     ██╔═══██╗╚══██╔══╝
███████║██║   ██║   ██║   ██║   ██║██████╔╝██║██║     ██║   ██║   ██║
██╔══██║██║   ██║   ██║   ██║   ██║██╔═══╝ ██║██║     ██║   ██║   ██║
██║  ██║╚██████╔╝   ██║   ╚██████╔╝██║     ██║███████╗╚██████╔╝   ██║
╚═╝  ╚═╝ ╚═════╝    ╚═╝    ╚═════╝ ╚═╝     ╚═╝╚══════╝ ╚═════╝    ╚═╝

⚡ Zero-Dependency Smart API Engine for Flutter

Pub Version Flutter Dart License Dependencies

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

  • x v1.0.0 — Core engine (http + shared_preferences)
  • x 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 ⭐