Nexio

The networking runtime for Flutter.

Nexio is a production-grade networking package for Flutter applications on Android and iOS. It uses Dio as its HTTP engine and adds the coordination layer that large applications repeatedly rebuild: runtime environments, interceptor-driven context, encryption, typed parsing, isolate-aware JSON decoding, retries, caching, offline replay, cancellation, uploads, downloads, request scheduling, logging, and monitoring.

Nexio is not a custom HTTP implementation and it does not hide Dio from advanced use cases. It provides one predictable request lifecycle while keeping backend-specific policy in your application.

Use Nexio When

  • your app has development, QA, staging, regional, tenant, or production APIs;
  • headers and session context must be evaluated before every request;
  • large JSON responses can cause dropped Flutter frames;
  • payment, telecom, self-care, marketplace, or enterprise flows need explicit retry, cache, cancellation, priority, and idempotency decisions;
  • you need CBC/GCM payload envelopes that match a cooperating backend;
  • multiple callers should share an identical in-flight request;
  • you want request timing, lifecycle events, network state, and optional Chucker inspection from one runtime.

What Remains App-Owned

Nexio deliberately does not define your token format, refresh endpoint, secure storage, certificate-pinning policy, analytics backend, route navigation, or UI design. It provides hooks so those decisions remain testable and specific to your backend.

Features

Area Included
Transport Dio-powered GET, POST, PUT, PATCH, DELETE, multipart upload, and file download
Environments Any number of named environments, runtime switching, per-request base URL override, stable Dio pool per environment
Interceptors Built-in context, encryption, logging, and conditional Chucker interceptors plus app-owned factories
Encryption AES-CBC, AES-GCM, no encryption, custom ciphers, and backend wire-format adapters
Parsing JSON, string, bytes, XML, custom parsers, registered typed models, automatic background JSON decoding
Resilience Fixed/exponential retries, cancellation, priorities, concurrency limits, in-flight deduplication
Caching Memory and disk cache, TTL, network-only, cache-only, cache-first, and network-first policies
Connectivity Online/offline streams and optional persisted offline request replay
Transfers Multipart progress and cancellation; download progress, pause, resume, and cancel
UI hooks Optional global/per-request loaders without imposing an app design
Observability Lifecycle events, console/in-memory logs, Chucker, response timing, and aggregate health outcomes

Request Pipeline

Nexio call
  -> priority scheduler and in-flight deduplication
  -> environment URL and context headers
  -> app interceptors
  -> optional request encryption
  -> Dio asynchronous network I/O
  -> optional response decryption
  -> automatic main/background JSON decoding
  -> typed parser
  -> cache, events, health counters, and timing metrics

Installation

dependencies:
  nexio: ^0.2.0
flutter pub get

Import the single public library:

import 'package:nexio/nexio.dart';

60-Second Start

Initialize once before the app starts making requests:

void main() {
  Nexio.initialize(
    environments: const {
      'dev': NexioEnvironment(baseUrl: 'https://dev.example.com'),
      'production': NexioEnvironment(baseUrl: 'https://api.example.com'),
    },
    initialEnvironment: 'dev',
  );

  runApp(const MyApp());
}

Make a typed request:

final response = await Nexio.get<List<User>>(
  '/users',
  parser: parseUsers,
);

final users = response.data;
print('Loaded ${users.length} users');
print('Total time: ${response.metrics.totalDuration}');

Future<List<User>> parseUsers(Object? input) async {
  final items = input! as List<Object?>;
  return items
      .cast<Map<String, Object?>>()
      .map(User.fromJson)
      .toList();
}

NexioResponse<T> also exposes status, headers, cache origin, Dio request options, and network/decrypt/parse/total timing metrics.

Complete Initialization

All settings have conservative defaults. Enable only the capabilities your app uses:

Nexio.initialize(
  environments: const {
    'local': NexioEnvironment(baseUrl: 'http://10.0.2.2:8080'),
    'qa-east': NexioEnvironment(
      baseUrl: 'https://qa-east.example.com',
      headers: {'X-Region': 'east'},
    ),
    'uat': NexioEnvironment(baseUrl: 'https://uat.example.com'),
    'production': NexioEnvironment(baseUrl: 'https://api.example.com'),
  },
  initialEnvironment: 'qa-east',
  defaultHeaders: const {'Accept': Headers.jsonContentType},
  loggerEnabled: true,
  enableChucker: true,
  defaultLogInChucker: false,
  defaultThreadMode: ThreadMode.auto,
  parseThresholdKb: 64,
  retryPolicy: const RetryPolicy(retries: 2),
  cacheConfig: const CacheConfig(
    defaultTtl: Duration(minutes: 5),
  ),
  maxConcurrentRequests: 6,
  offlineQueueEnabled: true,
  networkConfig: NexioNetworkConfig(
    connectivityProbe: checkBackendReachability,
  ),
  interceptorFactories: [
    () => CorrelationIdInterceptor(),
  ],
);

Environments

Environment names belong to your application. Switch without reinitializing:

Nexio.switchEnvironment('production');

print(Nexio.currentEnvironment);
print(Nexio.currentEnvironmentConfig.baseUrl);

Override one request without creating another Dio instance:

final response = await Nexio.get<Map<String, Object?>>(
  '/status',
  baseUrlOverride: 'https://status.example.com',
  parser: parseMap,
);

Nexio captures the selected environment when a request enters the scheduler. Later environment changes affect future calls, not already queued calls.

Request Configuration

final response = await Nexio.post<PaymentReceipt>(
  '/payments',
  data: payment.toJson(),
  headers: {'X-Idempotency-Key': payment.idempotencyKey},
  encryptionMode: EncryptionMode.aesGcm,
  threadMode: ThreadMode.auto,
  retryPolicy: const RetryPolicy(retries: 0),
  cachePolicy: CachePolicy.networkOnly,
  priority: RequestPriority.high,
  deduplicate: false,
  cancelGroup: 'payments',
  showLoader: true,
  logInChucker: false,
  parser: PaymentReceipt.parse,
);

Use Nexio.request<T> when the HTTP method or advanced Dio options are chosen dynamically.

Registered Parsers

Register parsers used across many endpoints:

Nexio.registerParser<User>((input) async {
  return User.fromJson(Map<String, Object?>.from(input! as Map));
});

final user = (await Nexio.get<User>('/me')).data;

The parser receives already decrypted and JSON-decoded data.

Await and Isolates

await Nexio.get(...) does not block Flutter's UI isolate. Dio performs asynchronous network I/O; await suspends only the current async function while the event loop continues rendering frames and processing input.

Isolates solve a different problem: CPU work. Nexio's ThreadMode.auto measures raw JSON size and uses Flutter's compute for decoding when the configured threshold is crossed. For very large typed responses, isolateParser moves JSON decoding and model construction together. Its callback must be top-level or static so Dart can send it to an isolate.

final catalog = await Nexio.get<List<Item>>(
  '/large-catalog',
  threadMode: ThreadMode.auto,
  parseThresholdKb: 32,
  isolateParser: Item.parseListFromSource,
);

The network wait and the isolate result are both Futures, so callers correctly use await for both. See the threading guide.

Encryption

Configure backend-compatible keys centrally and select a mode per request:

Nexio.initialize(
  environments: environments,
  initialEnvironment: 'production',
  encryptionConfig: EncryptionConfig(
    aesCbcKey: secureConfig.cbcKey,
    aesCbcIv: secureConfig.cbcIv,
    aesGcmKey: secureConfig.gcmKey,
  ),
);

final receipt = await Nexio.post<PaymentReceipt>(
  '/payments',
  data: payment.toJson(),
  encryptionMode: EncryptionMode.aesGcm,
  logInChucker: false,
  parser: PaymentReceipt.parse,
);

Request flow is serialize then encrypt then send. Response flow is receive then decrypt then parse. Local payload encryption complements TLS; it does not replace TLS. Never hardcode production secrets in source code. See the encryption guide and security guide.

Authentication Hooks

Nexio can coordinate one app-owned refresh operation while other protected requests wait:

authConfig: NexioAuthConfig(
  headersProvider: () => {
    'Authorization': 'Bearer ${session.accessToken}',
    'X-Gateway-Token': session.gatewayToken,
  },
  decide: (signal) {
    if (signal.statusCode == 401) {
      return NexioAuthDecision.refreshAndRetry;
    }
    return NexioAuthDecision.proceed;
  },
  refresh: (_) => session.refreshTokens(),
  onSessionExpired: (_) => session.expire(),
),

Your callback owns credentials, storage, endpoint selection, and token parsing. See authentication hooks.

Mark sign-in, registration, public configuration, and refresh traffic as anonymous so dynamic auth headers and the expired-session gate are skipped:

final config = await Nexio.get<Map<String, Object?>>(
  '/public/config',
  authMode: NexioAuthMode.anonymous,
  parser: parseMap,
);

Nexio.resetAuthSession(); // Call only after a new session is established.

Retries, Cache, Priority, and Deduplication

final offers = await Nexio.get<List<Offer>>(
  '/offers',
  retryPolicy: const RetryPolicy(
    retries: 3,
    strategy: RetryStrategy.exponential,
  ),
  cachePolicy: CachePolicy.networkFirst,
  cacheTtl: const Duration(minutes: 15),
  cacheKeyExtra: {
    'apiVersion': offersVersion,
    'country': countryCode,
  },
  priority: RequestPriority.normal,
  parser: Offer.parseList,
);

Identical in-flight requests are deduplicated by default. Disable deduplication for analytics or transaction calls that the backend must receive separately. Do not retry non-idempotent operations unless the backend contract and an idempotency key make the retry safe.

Cancellation

final token = CancelToken();

final request = Nexio.get<User>(
  '/profile',
  cancelToken: token,
  cancelTag: 'profile',
  cancelGroup: 'account',
);

token.cancel('Screen closed');
Nexio.cancelTag('profile');
Nexio.cancelGroup('account');

Uploads and Downloads

final uploaded = await Nexio.upload<Map<String, Object?>>(
  '/documents',
  files: const [
    NexioUploadFile(
      fieldName: 'document',
      path: '/path/to/document.pdf',
      contentType: 'application/pdf',
    ),
  ],
  onSendProgress: (sent, total) => print('$sent / $total'),
  logInChucker: false,
  parser: parseMap,
);
final task = Nexio.download(
  '/reports/monthly.pdf',
  destinationPath: '/path/to/monthly.pdf',
  onProgress: (received, total) => print('$received / $total'),
);

await task.pause();
await task.resume();
final savedPath = await task.completed;

Download resume requires server support for HTTP range requests.

Logging and Chucker

Enable Chucker capability globally and opt in only safe requests:

Nexio.initialize(
  environments: environments,
  initialEnvironment: 'qa',
  enableChucker: true,
  defaultLogInChucker: false,
);

await Nexio.get('/catalog', logInChucker: true);
await Nexio.post('/payments', data: payment, logInChucker: false);

Attach ChuckerFlutter.navigatorKey to MaterialApp, then call Nexio.showLogs(context). Keep payment, PIN, OTP, identity, and KYC traffic out of inspection tools.

Events, Connectivity, and Health

final eventSubscription = Nexio.events.listen((event) {
  switch (event) {
    case NexioRequestFailedEvent():
      reportFailure(event);
    case NexioUnauthorizedEvent():
      openSignInOnce();
    default:
      break;
  }
});

final onlineSubscription = Nexio.online.listen((_) => retryVisibleWork());

connectivity_plus reports network interfaces, not guaranteed internet access. Supply NexioNetworkConfig.connectivityProbe for a lightweight backend health check, call Nexio.checkConnectivity() on demand, or enable verifyBeforeRequest when every request must be actively checked.

Offline replay is intentionally opt-in twice: enable the runtime capability, then mark only replay-safe requests:

await Nexio.post<void>(
  '/analytics/events',
  data: event,
  queueWhenOffline: true,
);

Queued requests preserve environment, encryption, authentication mode, content type, and Chucker policy. Authorization headers are regenerated at replay and are not persisted by default.

final snapshot = Nexio.healthSnapshot;
print('Observed outcomes: ${snapshot.total}');
await Nexio.flushHealth();

Health aggregation stores endpoint paths and outcomes, not hosts, query strings, headers, tokens, or payloads.

Error Handling

try {
  await Nexio.get<User>('/me');
} on NexioHttpException<User> catch (error) {
  print(error.response.statusCode);
} on NexioOfflineQueuedException catch (error) {
  print('Queued as ${error.queueId}');
} on NexioException catch (error) {
  print(error.message);
}

See the error guide for the full exception model.

Production Rules

  • Use HTTPS in every non-local environment.
  • Provision encryption material outside source control and rotate it with the backend.
  • Keep defaultLogInChucker: false; opt in only non-sensitive debug traffic.
  • Use idempotency keys and deliberate retry policies for transactions.
  • Namespace cache keys by API version, tenant, locale, country, and account when those values change response meaning.
  • Enable offline replay only when needed, then opt in each replay-safe endpoint with queueWhenOffline: true.
  • Run backend-specific integration tests on Android and iOS.

Review the production checklist before shipping.

Examples

Guides

Supported Platforms

Nexio 0.2.0 supports Flutter applications on Android and iOS. Web and desktop platforms are not part of the V1 compatibility contract.

Release Status

0.2.0 is a pre-1.0 release. The API is documented and tested, but applications must still validate their backend contracts, encryption envelopes, authentication decisions, retry safety, cache policy, and offline behavior.

Contributing and Security

See CONTRIBUTING.md for development and pull-request checks. Report vulnerabilities according to SECURITY.md; never place tokens, encryption material, personal data, or exploit details in a public issue.

License

Nexio is available under the MIT License.

Libraries

nexio
Nexio is a production-grade networking runtime for Flutter.