autopilot_api 1.0.1 copy "autopilot_api: ^1.0.1" to clipboard
autopilot_api: ^1.0.1 copied to clipboard

Zero-boilerplate smart API engine for Flutter. Only uses http + shared_preferences.

๐Ÿš€ AutoPilot API #

Zero-boilerplate smart API engine for Flutter #

Pub Version Flutter Dart License: MIT Null Safety Dependencies

Only 2 dependencies ยท No Dio ยท Production Ready ยท Works with every state manager

Features โ€ข Installation โ€ข Quick Start โ€ข Usage โ€ข State Managers โ€ข API Reference


๐ŸŽฏ Why AutoPilot? #

Every Flutter project has this problem โ€” you write the same boilerplate for every single API call:

// โŒ The OLD painful way โ€” 40+ lines, copy-pasted everywhere
Future<UserModel?> getUser(int id) async {
  try {
    final prefs = await SharedPreferences.getInstance();
    final token = prefs.getString('token');

    final response = await http.get(
      Uri.parse('$baseUrl/users/$id'),
      headers: {
        'Authorization': 'Bearer $token',
        'Content-Type' : 'application/json',
        'Accept'       : 'application/json',
      },
    ).timeout(const Duration(seconds: 30));

    if (response.statusCode == 200) {
      final json = jsonDecode(response.body);
      if (json['status'] == true) {
        return UserModel.fromJson(json['data']);
      } else {
        throw Exception(json['message']);
      }
    } else if (response.statusCode == 401) {
      // handle unauthorized...
    } else if (response.statusCode == 422) {
      // handle validation...
    }
  } on SocketException {
    throw Exception('No internet');
  } on TimeoutException {
    throw Exception('Timeout');
  } catch (e) {
    rethrow;
  }
  return null;
}
// โœ… The AutoPilot way โ€” 4 lines, everything automated
final res = await api.get(
  endpoint : '/users/1',
  parser   : UserModel.fromJson,
);
if (res.isSuccess) print(res.data);
else print(res.message);

AutoPilot automatically handles:

What How
๐Ÿ”‘ Token injection Auto reads from secure storage, injects into every request
๐Ÿ”„ Token refresh On 401 โ†’ refresh โ†’ retry original request automatically
๐ŸŒ Internet check Detects offline before request, returns clean error
โฑ Timeout Configurable, returns typed exception
๐Ÿ” Retry Exponential backoff on network failures
๐Ÿ’พ Caching Memory + disk, configurable duration per-request
โšก Deduplication Same concurrent requests โ†’ 1 network call
๐Ÿ“ฆ Parsing Auto parse JSON โ†’ your model via parser function
๐ŸŽจ Logging Beautiful colored logs with full payload in debug
๐Ÿ”ด Error handling Typed exceptions, global error callback
โณ Loader Global loading state via callback

โœจ Features #

  • ๐Ÿš€ GET, POST, PUT, PATCH, DELETE โ€” all HTTP methods
  • ๐Ÿ“ Multipart upload โ€” single file, multiple files, with fields
  • โฌ‡๏ธ File download โ€” with progress callback
  • ๐Ÿ” Auto token injection โ€” SharedPreferences + memory cache
  • ๐Ÿ”„ Auto token refresh โ€” on 401, retry original request
  • ๐Ÿ’พ Two-layer cache โ€” memory (instant) + disk (persistent)
  • โšก Request deduplication โ€” prevent duplicate concurrent requests
  • ๐Ÿ” Retry with exponential backoff โ€” configurable attempts
  • ๐ŸŒ Connectivity check โ€” pure Dart, no extra package
  • ๐Ÿ“ฆ Smart response parser โ€” envelope + list + raw support
  • ๐ŸŽจ Colored ANSI logs โ€” full request/response payload
  • ๐Ÿ”Œ Works with all state managers โ€” GetX, Riverpod, Bloc, Provider, MobX
  • ๐ŸŒ Runtime environment switching โ€” staging โ†” production
  • ๐ŸŽฃ Extension methods โ€” .handle(), .mapData()
  • ๐Ÿงช 25+ unit tests included
  • ๐Ÿ“„ MIT License โ€” free for personal & commercial use

๐Ÿ“ฆ Installation #

Add to your pubspec.yaml:

dependencies:
  autopilot_api: ^1.0.0

Or if using as local package:

dependencies:
  autopilot_api:
    path: ../autopilot_api
flutter pub get

Only 2 transitive dependencies: http + shared_preferences


โšก Quick Start #

Step 1 โ€” Initialize once in main.dart #

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await AutoPilotApi.init(
    baseUrl      : 'https://api.example.com/v1',
    enableLogs   : true,   // ๐ŸŽจ beautiful colored logs
    printPayload : true,   // ๐Ÿ“‹ full JSON in debug console
    enableCache  : true,   // ๐Ÿ’พ auto cache GET responses
    maxRetries   : 3,      // ๐Ÿ” retry on network failure
  );

  runApp(const MyApp());
}

Step 2 โ€” Call APIs anywhere in your app #

final api = AutoPilotApi.instance;

final res = await api.get(
  endpoint : '/users/1',
  parser   : UserModel.fromJson,
);

if (res.isSuccess) {
  print(res.data);     // โœ… UserModel
} else {
  print(res.message);  // โŒ error message
}

That's it. No repositories. No try-catch. No headers. No token injection. No JSON decoding.


๐Ÿ”ง Full Configuration #

await AutoPilotApi.init(
  // โ”€โ”€ Required โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  baseUrl : 'https://api.example.com/v1',

  // โ”€โ”€ Auth โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  tokenType          : 'Bearer',            // default
  enableTokenRefresh : true,
  onRefreshToken     : () async {
    // your refresh logic โ€” return new access token
    final res = await AuthService.refreshToken();
    return res.accessToken;
  },

  // โ”€โ”€ Network โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  timeoutSeconds : 30,                      // default
  maxRetries     : 3,                       // retry attempts
  retryDelay     : Duration(seconds: 1),    // exponential: 1s, 2s, 3s

  // โ”€โ”€ Cache โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  enableCache   : true,
  cacheDuration : Duration(minutes: 5),     // default

  // โ”€โ”€ Debug Logs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  enableLogs   : true,                      // auto false in release
  printPayload : true,                      // print full JSON body
  prettyPrint  : true,                      // formatted vs minified

  // โ”€โ”€ Global Loader โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  enableGlobalLoader : true,
  onLoadingChanged   : (loading) {
    // hook into your state manager
    Get.find<AppController>().isLoading(loading);    // GetX
    // or: ref.read(loadingProvider.notifier).state = loading; // Riverpod
  },

  // โ”€โ”€ Global Error Handler โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  onError : (message, statusCode) {
    Get.snackbar('Error', message);    // GetX
    // or: ScaffoldMessenger.of(context).showSnackBar(...);
  },

  // โ”€โ”€ Analytics / Hooks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  onRequestSent      : (url, method) => Analytics.log('$method $url'),
  onResponseReceived : (url, code, time) => Analytics.log('$code ${time.inMilliseconds}ms'),

  // โ”€โ”€ Global Headers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  globalHeaders : {
    'X-App-Version' : '1.0.0',
    'X-Platform'    : Platform.isAndroid ? 'android' : 'ios',
    'X-Locale'      : 'en',
  },

  // โ”€โ”€ Response Envelope โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  // Match your backend's JSON structure
  successKey   : 'status',    // your API's success field name
  successValue : true,        // value that means success
  messageKey   : 'message',   // your API's message field name
  dataKey      : 'data',      // your API's data field name

  // โ”€โ”€ Performance โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  enableDeduplication : true, // prevent duplicate concurrent GETs
);

๐ŸŒŸ All Requests #

GET #

// Single object
final res = await api.get<UserModel>(
  endpoint : '/users/1',
  parser   : UserModel.fromJson,
);

// List
final res = await api.get<List<UserModel>>(
  endpoint : '/users',
  parser   : (json) => (json as List).map(UserModel.fromJson).toList(),
);

// With query params โ†’ /posts?userId=1&page=2&limit=10
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(),
);

// With cache
final res = await api.get(
  endpoint      : '/app/config',
  parser        : ConfigModel.fromJson,
  useCache      : true,
  cacheDuration : Duration(hours: 1),   // override global duration
);

// With custom headers
final res = await api.get(
  endpoint : '/premium/content',
  headers  : {'X-Plan': 'premium'},
  parser   : ContentModel.fromJson,
);

POST #

// Login
final res = await api.post<LoginModel>(
  endpoint : '/auth/login',
  body     : {'email': email, 'password': password},
  parser   : LoginModel.fromJson,
);

// Create resource
final res = await api.post<PostModel>(
  endpoint : '/posts',
  body     : {
    'title'   : 'My Post',
    'content' : 'Hello World',
    'tags'    : ['flutter', 'dart'],
  },
  parser: PostModel.fromJson,
);

// Without parser (just check success)
final res = await api.post(
  endpoint : '/auth/logout',
  body     : {'device_id': deviceId},
);
if (res.isSuccess) navigateToLogin();

PUT / PATCH #

// Full update
final res = await api.put<UserModel>(
  endpoint : '/users/1',
  body     : {'name': 'John', 'email': 'john@example.com', 'age': 25},
  parser   : UserModel.fromJson,
);

// Partial update
final res = await api.patch<UserModel>(
  endpoint : '/users/1',
  body     : {'name': 'New Name'},   // only changed fields
  parser   : UserModel.fromJson,
);

DELETE #

final res = await api.delete(endpoint: '/posts/1');
if (res.isSuccess) print('Deleted! ${res.message}');

MULTIPART โ€” File Upload #

// Single file โ€” shorthand
final res = await api.multipart<UploadModel>(
  endpoint : '/profile/photo',
  fileKey  : 'image',
  filePath : pickedFile.path,
  parser   : UploadModel.fromJson,
);

// Multiple files
final res = await api.multipart(
  endpoint : '/gallery/upload',
  files    : [
    MultipartFileModel(key: 'photos', path: img1.path),
    MultipartFileModel(key: 'photos', path: img2.path),
    MultipartFileModel(key: 'photos', path: img3.path),
  ],
);

// Files + form fields
final res = await api.multipart<UploadModel>(
  endpoint : '/documents/upload',
  files    : [
    MultipartFileModel(
      key      : 'document',
      path     : file.path,
      fileName : 'report_2025.pdf',  // custom filename
      mimeType : 'application/pdf',  // custom mime type
    ),
  ],
  fields : {
    'title'    : 'Q4 Report',
    'category' : 'finance',
    'year'     : '2025',
  },
  parser : UploadModel.fromJson,
);

// Video upload
final res = await api.multipart(
  endpoint : '/videos/upload',
  files    : [
    MultipartFileModel(
      key      : 'video',
      path     : videoFile.path,
      mimeType : 'video/mp4',
    ),
  ],
);

DOWNLOAD #

final res = await api.download(
  endpoint   : '/reports/january-2025.pdf',
  savePath   : '/storage/emulated/0/Download/report.pdf',
  onProgress : (received, total) {
    final percent = (received / total * 100).toInt();
    print('Downloading: $percent%');
    setState(() => downloadProgress = percent / 100);
  },
);

if (res.isSuccess) {
  print('Saved to: ${res.data}');   // res.data = file path
}

๐Ÿ“ฆ ApiResponse<T> #

Every request returns a standardized ApiResponse<T>:

final res = await api.get(endpoint: '/users/1', parser: UserModel.fromJson);

// Core fields
res.isSuccess    // bool โ€” true if request succeeded
res.data         // T?  โ€” your parsed model (null on failure)
res.message      // String โ€” success msg or error msg from server
res.statusCode   // int โ€” 200, 201, 400, 401, 404, 422, 500...
res.raw          // dynamic โ€” raw JSON response body
res.errors       // Map<String, dynamic>? โ€” validation errors
res.requestId    // String? โ€” short tracing ID e.g. "a3f2b1c9"
res.responseTime // Duration? โ€” how long the request took
res.timestamp    // DateTime โ€” when response was received

// Status helpers
res.isClientError     // statusCode 400โ€“499
res.isServerError     // statusCode 500+
res.isUnauthorized    // statusCode == 401
res.isForbidden       // statusCode == 403
res.isNotFound        // statusCode == 404
res.isValidationError // statusCode == 422
res.isTimeout         // statusCode == 408

// Data helpers
res.dataOr(UserModel.guest)   // returns fallback if data is null
res.dataOrThrow               // throws StateError if data is null

// Validation error helpers
res.validationError('email')  // โ†’ "Email is required"
res.validationError('phone')  // โ†’ "Invalid phone number"
res.allErrors                 // all validation errors as single string

๐Ÿ” Token Management #

// After login โ€” token auto-injected into every subsequent request
await AutoPilotApi.setToken(loginRes.data!.accessToken);

// Save both tokens
await AutoPilotApi.setTokens(
  accessToken  : loginRes.data!.accessToken,
  refreshToken : loginRes.data!.refreshToken,
);

// Logout โ€” clear all tokens
await AutoPilotApi.clearTokens();

Tokens stored in SharedPreferences with in-memory cache for fast access. Auto-injected as Authorization: Bearer <token> header.


๐Ÿ’พ Caching #

Two-layer cache: memory (instant, lost on restart) + disk (SharedPreferences, survives restart).

// Enable globally in init
await AutoPilotApi.init(
  baseUrl       : '...',
  enableCache   : true,
  cacheDuration : Duration(minutes: 5),
);

// Override per-request
final res = await api.get(
  endpoint      : '/app/config',
  useCache      : true,
  cacheDuration : Duration(hours: 24),  // cache for 1 day
);

// Invalidate specific endpoint
await api.invalidateCache('/app/config');

// Wipe all cache
await api.clearCache();

Cache key is built from URL + sorted query params โ€” same request always hits same cache entry.


๐Ÿ”„ Retry with Exponential Backoff #

await AutoPilotApi.init(
  baseUrl    : '...',
  maxRetries : 3,                         // retry 3 times
  retryDelay : Duration(seconds: 1),      // 1s โ†’ 2s โ†’ 3s (exponential)
);

Retries on: network errors, socket exceptions, connection drops, timeouts.
Does NOT retry on: 4xx client errors (400, 401, 403, 404, 422).


๐Ÿ”‘ Auto Token Refresh #

await AutoPilotApi.init(
  baseUrl            : '...',
  enableTokenRefresh : true,
  onRefreshToken     : () async {
    // Called automatically on 401
    final refreshToken = await TokenManager.getRefreshToken();
    final response = await http.post(
      Uri.parse('https://api.example.com/auth/refresh'),
      body: jsonEncode({'refresh_token': refreshToken}),
      headers: {'Content-Type': 'application/json'},
    );
    final json = jsonDecode(response.body);
    return json['access_token'] as String?; // return new token
  },
);

// Now ALL your requests auto-handle 401:
// Request โ†’ 401 โ†’ refresh token โ†’ retry original request โ†’ โœ…
// Developer writes NOTHING extra.

โšก Request Deduplication #

Prevents the same GET request from hitting the server twice when called simultaneously:

// Both widgets call this at the same time
// Result: only ONE network request is made
final results = await Future.wait([
  api.get(endpoint: '/user/me', parser: UserModel.fromJson),
  api.get(endpoint: '/user/me', parser: UserModel.fromJson),
  api.get(endpoint: '/user/me', parser: UserModel.fromJson),
]);
// All 3 get the same result from 1 request โšก

๐ŸŽจ Debug Logs #

AutoPilot prints beautiful colored logs in debug mode โ€” automatically disabled in release builds:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
โ”‚ ๐Ÿš€ REQUEST [a3f2b1c9]  14:32:01.432
โ”‚  POST  https://api.example.com/v1/auth/login
โ”‚  โŠณ Headers
โ”‚    Content-Type: application/json
โ”‚    Authorization: ***masked***
โ”‚  โŠณ Body
โ”‚    {
โ”‚      "email": "john@example.com",
โ”‚      "password": "***"
โ”‚    }
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
โ”‚ โœ… RESPONSE [a3f2b1c9]  14:32:01.687
โ”‚  Status: 200  โฑ 255ms
โ”‚  https://api.example.com/v1/auth/login
โ”‚  โŠณ Payload
โ”‚    {
โ”‚      "status": true,
โ”‚      "message": "Login successful",
โ”‚      "data": {
โ”‚        "token": "eyJhbGci...",
โ”‚        "user": { "id": 1, "name": "John" }
โ”‚      }
โ”‚    }
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
โ”‚ ๐Ÿ’ฅ ERROR [b7d3e2f1]  14:35:22.901
โ”‚  https://api.example.com/v1/posts/999
โ”‚  Status: 404
โ”‚  Resource not found.
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
await AutoPilotApi.init(
  enableLogs   : true,   // default: kDebugMode (auto false in release)
  printPayload : true,   // show full request/response JSON
  prettyPrint  : true,   // formatted JSON (false = minified)
);

๐ŸŽฃ Extension Methods #

// .handle() โ€” clean success/failure without if/else
await api.get(endpoint: '/me', parser: UserModel.fromJson)
  .handle(
    onSuccess : (data, message) => setState(() => user = data),
    onFailure : (message, code) => showSnackbar(message),
  );

// .mapData() โ€” transform data type in the chain
final countRes = await api
    .get<String>(endpoint: '/stats/count')
    .mapData((s) => int.parse(s));   // ApiResponse<int>

print(countRes.data);   // int

๐ŸŒ Runtime Environment Switching #

// Switch between environments without restarting
api.reconfigure(
  (config) => config.copyWith(baseUrl: 'https://staging.api.example.com'),
);

// Switch back to production
api.reconfigure(
  (config) => config.copyWith(baseUrl: 'https://api.example.com'),
);

๐Ÿ”Œ State Manager Integration #

AutoPilot works with every Flutter state manager โ€” zero conflicts, zero setup.

GetX #

class UserController extends GetxController {
  final _api = AutoPilotApi.instance;

  final user      = Rx<UserModel?>(null);
  final isLoading = false.obs;
  final error     = RxString('');

  Future<void> fetchUser() async {
    isLoading.value = true;
    final res = await _api.get(
      endpoint : '/users/me',
      parser   : UserModel.fromJson,
    );
    isLoading.value = false;
    if (res.isSuccess) user.value = res.data;
    else error.value = res.message;
  }

  Future<void> updateProfile(Map<String, dynamic> data) async {
    final res = await _api.patch(
      endpoint : '/users/me',
      body     : data,
      parser   : UserModel.fromJson,
    );
    if (res.isSuccess) {
      user.value = res.data;
      Get.snackbar('โœ…', res.message);
    }
  }
}

Provider #

class UserProvider extends ChangeNotifier {
  final _api = AutoPilotApi.instance;

  UserModel? user;
  bool isLoading = false;
  String error   = '';

  Future<void> fetchUser() async {
    isLoading = true; notifyListeners();

    final res = await _api.get(
      endpoint : '/users/me',
      parser   : UserModel.fromJson,
    );

    isLoading = false;
    if (res.isSuccess) user = res.data;
    else error = res.message;
    notifyListeners();
  }
}

Riverpod #

// FutureProvider
final userProvider = FutureProvider<UserModel?>((ref) async {
  final res = await AutoPilotApi.instance.get(
    endpoint : '/users/me',
    parser   : UserModel.fromJson,
  );
  if (!res.isSuccess) throw res.message;
  return res.data;
});

// AsyncNotifier (recommended for mutations)
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 userNotifierProvider =
    AsyncNotifierProvider<UserNotifier, UserModel?>(() => UserNotifier());

Bloc / Cubit #

// Cubit (simpler)
class UserCubit extends Cubit<UserState> {
  final _api = AutoPilotApi.instance;
  UserCubit() : super(UserInitial());

  Future<void> fetchUser() async {
    emit(UserLoading());
    final res = await _api.get(
      endpoint : '/users/me',
      parser   : UserModel.fromJson,
    );
    if (res.isSuccess) emit(UserLoaded(res.data!));
    else emit(UserError(res.message));
  }

  Future<void> uploadPhoto(String path) async {
    emit(UserLoading());
    final res = await _api.multipart(
      endpoint : '/users/photo',
      fileKey  : 'image',
      filePath : path,
      parser   : UserModel.fromJson,
    );
    if (res.isSuccess) emit(UserLoaded(res.data!));
    else emit(UserError(res.message));
  }
}

// Bloc (event-based)
class UserBloc extends Bloc<UserEvent, UserState> {
  final _api = AutoPilotApi.instance;
  UserBloc() : super(UserInitial()) {
    on<FetchUserEvent>(_onFetch);
    on<UpdateUserEvent>(_onUpdate);
  }

  Future<void> _onFetch(FetchUserEvent e, Emitter<UserState> emit) async {
    emit(UserLoading());
    final res = await _api.get(endpoint: '/users/me', parser: UserModel.fromJson);
    res.isSuccess ? emit(UserLoaded(res.data!)) : emit(UserError(res.message));
  }

  Future<void> _onUpdate(UpdateUserEvent e, Emitter<UserState> emit) async {
    emit(UserLoading());
    final res = await _api.patch(endpoint: '/users/me', body: e.data, parser: UserModel.fromJson);
    res.isSuccess ? emit(UserLoaded(res.data!)) : emit(UserError(res.message));
  }
}

MobX #

part 'user_store.g.dart';

class UserStore = _UserStore with _$UserStore;

abstract class _UserStore with Store {
  final _api = AutoPilotApi.instance;

  @observable UserModel? user;
  @observable bool isLoading = false;
  @observable String error   = '';

  @action
  Future<void> fetchUser() async {
    isLoading = true;
    final res = await _api.get(endpoint: '/users/me', parser: UserModel.fromJson);
    isLoading = false;
    if (res.isSuccess) user = res.data;
    else error = res.message;
  }
}

๐Ÿ“ Architecture #

autopilot_api/
โ”œโ”€โ”€ lib/
โ”‚   โ”œโ”€โ”€ autopilot_api.dart              โ† barrel export (import this)
โ”‚   โ”œโ”€โ”€ core/
โ”‚   โ”‚   โ””โ”€โ”€ autopilot_core.dart         โ† ๐Ÿง  main engine (AutoPilotApi class)
โ”‚   โ”œโ”€โ”€ models/
โ”‚   โ”‚   โ”œโ”€โ”€ api_response.dart           โ† ApiResponse<T> wrapper
โ”‚   โ”‚   โ”œโ”€โ”€ autopilot_config.dart       โ† AutoPilotConfig with copyWith
โ”‚   โ”‚   โ””โ”€โ”€ multipart_file_model.dart   โ† MultipartFileModel
โ”‚   โ”œโ”€โ”€ auth/
โ”‚   โ”‚   โ””โ”€โ”€ token_manager.dart          โ† SharedPrefs + memory token storage
โ”‚   โ”œโ”€โ”€ cache/
โ”‚   โ”‚   โ””โ”€โ”€ cache_service.dart          โ† memory + disk two-layer cache
โ”‚   โ”œโ”€โ”€ connectivity/
โ”‚   โ”‚   โ””โ”€โ”€ connectivity_service.dart   โ† pure Dart internet check
โ”‚   โ”œโ”€โ”€ exceptions/
โ”‚   โ”‚   โ””โ”€โ”€ api_exception.dart          โ† typed exceptions hierarchy
โ”‚   โ”œโ”€โ”€ extensions/
โ”‚   โ”‚   โ””โ”€โ”€ response_extensions.dart    โ† .handle(), .mapData()
โ”‚   โ”œโ”€โ”€ logger/
โ”‚   โ”‚   โ””โ”€โ”€ autopilot_logger.dart       โ† colored ANSI debug logger
โ”‚   โ”œโ”€โ”€ parsers/
โ”‚   โ”‚   โ””โ”€โ”€ response_parser.dart        โ† smart JSON โ†’ model parser
โ”‚   โ”œโ”€โ”€ queue/
โ”‚   โ”‚   โ””โ”€โ”€ request_queue.dart          โ† request deduplication
โ”‚   โ””โ”€โ”€ retry/
โ”‚       โ””โ”€โ”€ retry_service.dart          โ† exponential backoff retry
โ”œโ”€โ”€ example/
โ”‚   โ””โ”€โ”€ lib/main.dart                   โ† full working demo app
โ”œโ”€โ”€ test/
โ”‚   โ””โ”€โ”€ autopilot_test.dart             โ† 25+ unit tests
โ”œโ”€โ”€ LICENSE
โ”œโ”€โ”€ CHANGELOG.md
โ””โ”€โ”€ pubspec.yaml

๐Ÿ“‹ API Reference #

AutoPilotApi #

Method Description
AutoPilotApi.init(...) Initialize โ€” call once in main()
AutoPilotApi.instance Get singleton instance
AutoPilotApi.setToken(token) Save access token
AutoPilotApi.setTokens(...) Save access + refresh token
AutoPilotApi.clearTokens() Clear all tokens (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(endpoint) Clear cache for endpoint
api.clearCache() Clear all cached responses
api.reconfigure(fn) Update config at runtime

ApiResponse<T> #

Property Type Description
isSuccess bool Request succeeded
data T? Parsed model
message String Server message
statusCode int HTTP status code
raw dynamic Raw JSON body
errors Map? Validation errors
requestId String? Tracing ID
responseTime Duration? Request duration
isUnauthorized bool statusCode == 401
isValidationError bool statusCode == 422
dataOr(fallback) T Safe data with fallback
validationError(field) String? First error for field
allErrors String All errors as string

MultipartFileModel #

Property Type Description
key String Form field name
path String Absolute file path
fileName String? Custom filename
mimeType String? Custom MIME type

๐Ÿงช Testing #

# Run all tests
flutter test

# Run with coverage
flutter test --coverage

๐Ÿ“ Response Envelope Support #

AutoPilot supports any backend response structure. Configure the keys to match your API:

// Your API returns:
// { "success": true, "msg": "OK", "result": { ... } }
await AutoPilotApi.init(
  baseUrl      : '...',
  successKey   : 'success',   // default: 'status'
  successValue : true,        // default: true
  messageKey   : 'msg',       // default: 'message'
  dataKey      : 'result',    // default: 'data'
);

// Flat response (no envelope) โ€” also works automatically
// { "id": 1, "name": "John" }

๐Ÿค Contributing #

Contributions are welcome! Please open an issue or submit a PR.


๐Ÿ“„ License #

MIT License

Copyright (c) 2025 AutoPilot API

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Made with โค๏ธ for the Flutter community

โญ Star on GitHub โ€ข ๐Ÿ“ฆ View on pub.dev โ€ข ๐Ÿ› Report Bug

3
likes
120
points
200
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Zero-boilerplate smart API engine for Flutter. Only uses http + shared_preferences.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, http, shared_preferences

More

Packages that depend on autopilot_api