Skip to content

Adapters

Adapters are headless service integrations for the Air Framework. Unlike modules, they have no routes and no UI — they register infrastructure services in AirDI for modules to consume.

AspectModule (AppModule)Adapter (AirAdapter)
PurposeFeature (UI + logic)Infrastructure service
Has routes?✅ Yes❌ No
Has UI?✅ Yes❌ No
Registered viaModuleManagerAdapterManager
Boot orderAfter adaptersBefore modules

Use the CLI to scaffold an adapter:

Terminal window
air g adapter sentry

This creates:

lib/adapters/sentry/
├── contracts/
│ ├── sentry_client.dart ← abstract contract
│ └── sentry_response.dart ← response wrapper
├── sentry_adapter.dart ← AirAdapter subclass
└── sentry_impl.dart ← concrete implementation

Extend AirAdapter and override the lifecycle hooks:

class DioAdapter extends AirAdapter {
final String baseUrl;
DioAdapter({required this.baseUrl});
@override
String get id => 'dio';
@override
String get name => 'Dio HTTP Client';
@override
String get version => '1.0.0';
@override
void onBind(AirDI di) {
super.onBind(di);
final dio = Dio(BaseOptions(baseUrl: baseUrl));
// Register the abstract contract — modules use HttpClient, not Dio
di.registerLazySingleton<HttpClient>(() => DioHttpClient(dio));
}
@override
Future<void> onDispose(AirDI di) async {
di.tryGet<Dio>()?.close();
super.onDispose(di);
}
}
// ❌ BAD — modules are coupled to Dio
di.registerSingleton<Dio>(dio);
// ✅ GOOD — modules depend on HttpClient
di.registerLazySingleton<HttpClient>(() => DioHttpClient(dio));

This enables swapping the underlying library (e.g., Dio → http) without touching any module code.

Adapters must be registered before modules so that modules can safely access adapter-provided services.

void main() async {
WidgetsFlutterBinding.ensureInitialized();
configureAirState();
// 1. Adapters (infrastructure) — FIRST
final adapters = AdapterManager();
await adapters.register(DioAdapter(baseUrl: 'https://api.example.com'));
// 2. Modules (features) — SECOND
final manager = ModuleManager();
await manager.register(ProductsModule()); // can use HttpClient ✅
await manager.register(ShellModule());
runApp(const MyApp());
}

Modules depend on the contract, never the implementation:

class ProductsModule extends AppModule {
@override
void onBind(AirDI di) {
// HttpClient is provided by DioAdapter
di.registerLazySingleton<ProductService>(
() => ProductService(di.get<HttpClient>()),
);
}
}
class ProductService {
final HttpClient _http; // abstract — no Dio import
ProductService(this._http);
Future<List<Product>> getAll() async {
final response = await _http.get('/products');
return (response.data as List)
.map((j) => Product.fromJson(j))
.toList();
}
}
ElementConventionExample
Folderadapters/<name>/adapters/dio/, adapters/sentry/
Contract<Name>ClientHttpClient, ErrorReporter
Implementation<Name>ClientImpl or <Lib><Name>ClientDioHttpClient
Adapter class<Name>AdapterDioAdapter, SentryAdapter
Response<Name>ResponseHttpResponse, SentryResponse

Registered adapters appear in the ADAPTERS tab of the DevTools inspector, showing their id, name, version, and state.

AdapterContractLibrary
DioAdapterHttpClientdio
SentryAdapterErrorReportersentry_flutter
HiveAdapterStorageClienthive
FirebaseAdapterAnalyticsClientfirebase_analytics
StripeAdapterPaymentClientstripe_sdk