Enterprise-grade Flutter integration for the DavianSpace hosting runtime. Bridges dependency injection, hosted-service lifecycle management, configuration, and structured logging into Flutter's widget tree β conceptually equivalent to adding Microsoft.Extensions.Hosting support to a Flutter app.
- Features
- Installation
- Quick start
- Ecosystem integration
- Resolving services
- Architecture
- Advanced usage
- API reference
- Error handling
- Testing
- Contributing
- Security
- License
| Feature | Description |
|---|---|
| ServiceProviderScope | InheritedWidget that exposes the DI container to the entire widget tree |
| BuildContext extensions | getService<T>(), tryGetService<T>(), getAllServices<T>(), keyed variants, and serviceProvider |
| FlutterHostRunner | Host.runFlutterApp() / Host.runFlutterAppAsync() β imperative one-liner startup |
| HostProvider | Declarative StatefulWidget β loading/error/ready states, lifecycle observer |
| Lifecycle integration | Automatic Host.stop() and Host.dispose() on widget disposal and AppLifecycleState.detached |
| Zero reflection | No dart:mirrors, no code generation β full AOT and tree-shaking support |
| State-management agnostic | Works with Provider, Bloc, Riverpod, or any other approach |
Add to your pubspec.yaml:
dependencies:
davianspace_hosting_flutter: ^1.0.3Then run:
flutter pub getThe simplest approach β start the host and mount the widget tree in one call:
import 'package:davianspace_hosting/davianspace_hosting.dart';
import 'package:davianspace_hosting_flutter/davianspace_hosting_flutter.dart';
import 'package:flutter/material.dart';
void main() async {
final host = await createDefaultBuilder()
.configureServices((ctx, services) {
services.addInstance<GreetingService>(
const GreetingService('Hello!'),
);
})
.build();
await host.runFlutterApp(() => const MyApp());
}Lifecycle flow:
main() β host.build() β host.start() β runApp(ServiceProviderScope β MyApp)
β (on dispose / detach)
host.stop() β host.dispose()
Show a loading indicator while the host starts:
void main() async {
final host = await createDefaultBuilder().build();
await host.runFlutterAppAsync(
() => const MyApp(),
loadingWidget: const MaterialApp(
home: Scaffold(body: Center(child: CircularProgressIndicator())),
),
errorBuilder: (error) => MaterialApp(
home: Scaffold(body: Center(child: Text('Error: $error'))),
),
);
}State transitions:
runApp() β loadingWidget β [Host.start()] β builder() + ServiceProviderScope
β (on error)
errorBuilder(error)
Fully declarative lifecycle management inside the widget tree:
void main() {
runApp(
HostProvider(
hostFactory: () async => await createDefaultBuilder().build(),
loadingBuilder: (_) => const MaterialApp(
home: Scaffold(body: Center(child: CircularProgressIndicator())),
),
errorBuilder: (_, error) => MaterialApp(
home: Scaffold(body: Center(child: Text('Error: $error'))),
),
child: const MyApp(),
),
);
}HostProvider phases:
| Phase | Action |
|---|---|
initState |
Calls hostFactory(), then Host.start() |
| Build (loading) | Renders loadingBuilder (defaults to SizedBox.shrink) |
| Build (error) | Renders errorBuilder (defaults to red error text) |
| Build (ready) | Wraps child in ServiceProviderScope |
dispose |
Stops and disposes the host |
AppLifecycleState.detached |
Same shutdown as dispose |
davianspace_hosting_flutter is the Flutter bridge for the DavianSpace ecosystem:
| Package | Role |
|---|---|
davianspace_configuration |
Hierarchical configuration (JSON, env vars, in-memory) |
davianspace_dependencyinjection |
Service collection & provider (singleton, scoped, transient) |
davianspace_logging |
Structured logging with providers and filtering |
davianspace_options |
Options pattern for strongly-typed settings |
davianspace_hosting |
Orchestrates all of the above |
davianspace_hosting_flutter |
Bridges the hosting runtime into Flutter's widget tree |
davianspace_http_resilience |
(optional) Retry, circuit breaker, timeout policies |
davianspace_http_ratelimit |
(optional) HTTP rate limiting |
Once a ServiceProviderScope is in the tree (provided automatically by
runFlutterApp, runFlutterAppAsync, or HostProvider), resolve services
from any descendant widget:
@override
Widget build(BuildContext context) {
// Required β throws if not registered
final auth = context.getService<AuthService>();
// Optional β returns null if not registered
final analytics = context.tryGetService<AnalyticsService>();
// All implementations of an interface
final handlers = context.getAllServices<EventHandler>();
// Keyed services
final primary = context.getKeyedService<Database>('primary');
final backup = context.tryGetKeyedService<Database>('backup');
// Raw provider access (advanced)
final sp = context.serviceProvider;
return Text(auth.currentUser);
}βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Flutter App β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β ServiceProviderScope (InheritedWidget) β β
β β provider: host.services β β
β β β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β HostLifecycleObserver (WidgetsBindingObserver) β β β
β β β β’ stops host on dispose β β β
β β β β’ stops host on AppLifecycleState.detached β β β
β β β β β β
β β β ββββββββββββββββββββββββββββββββββββββββββββββββββ β β β
β β β β Your Widget Tree β β β β
β β β β context.getService<AuthService>() β β β β
β β β β context.tryGetService<Analytics>() β β β β
β β β β context.getKeyedService<Db>('primary') β β β β
β β β ββββββββββββββββββββββββββββββββββββββββββββββββββ β β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β davianspace_hosting (Host, HostedService, Lifetime) β β
β β davianspace_dependencyinjection (ServiceProvider) β β
β β davianspace_logging (LoggerFactory, Logger) β β
β β davianspace_configuration (Configuration) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
For a deep dive into internal design decisions, see doc/architecture.md.
Disambiguate multiple implementations of the same type by key:
builder.configureServices((ctx, services) {
services
..addKeyedSingleton<Database>('primary', PrimaryDatabase())
..addKeyedSingleton<Database>('analytics', AnalyticsDatabase());
});
// In a widget:
final primary = context.getKeyedService<Database>('primary');
final analytics = context.tryGetKeyedService<Database>('analytics');Collect all implementations of an interface:
builder.configureServices((ctx, services) {
services
..addSingleton<EventHandler>(AuditHandler())
..addSingleton<EventHandler>(MetricsHandler());
});
// In a widget:
final handlers = context.getAllServices<EventHandler>();
for (final handler in handlers) {
handler.handle(event);
}HostProvider(
hostFactory: buildHost,
loadingBuilder: (context) => const MaterialApp(
home: Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Initialising servicesβ¦'),
],
),
),
),
),
errorBuilder: (context, error) => MaterialApp(
home: Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline, color: Colors.red, size: 48),
SizedBox(height: 16),
Text('Startup failed: $error'),
],
),
),
),
),
child: const MyApp(),
)ElevatedButton(
onPressed: () {
final lifetime = context.getService<ApplicationLifetime>();
lifetime.requestShutdown();
},
child: const Text('Shutdown'),
)No mocking framework needed β the DI container itself serves as the test fixture:
testWidgets('displays greeting', (tester) async {
final services = ServiceCollection()
..addInstance<GreetingService>(const GreetingService('Hello'));
final provider = services.buildServiceProvider();
await tester.pumpWidget(
ServiceProviderScope(
provider: provider,
child: const MyGreetingWidget(),
),
);
expect(find.text('Hello'), findsOneWidget);
});An InheritedWidget that exposes a ServiceProvider to the widget tree.
| Member | Description |
|---|---|
ServiceProviderScope.of(context) |
Returns the nearest scope; throws FlutterError with actionable guidance if none found |
ServiceProviderScope.maybeOf(context) |
Returns the nearest scope or null |
provider |
The ServiceProvider instance |
Rebuild behaviour: updateShouldNotify compares provider identity β since the provider is created once at host startup, this never triggers spurious descendant rebuilds.
Extension ServiceResolution on BuildContext:
| Method | Delegates to | Behaviour on missing registration |
|---|---|---|
getService<T>() |
ServiceProvider.getRequired<T>() |
Throws DI exception |
tryGetService<T>() |
ServiceProvider.tryGet<T>() |
Returns null |
getAllServices<T>() |
ServiceProvider.getAll<T>() |
Returns empty list |
getKeyedService<T>(key) |
ServiceProvider.getRequiredKeyed<T>(key) |
Throws DI exception |
tryGetKeyedService<T>(key) |
ServiceProvider.tryGetKeyed<T>(key) |
Returns null |
serviceProvider |
Direct getter | N/A |
All methods throw a FlutterError if no ServiceProviderScope is in the ancestor tree.
Extension FlutterHostRunner on Host:
| Method | Description |
|---|---|
runFlutterApp(builder) |
Starts host synchronously, wraps widget in scope, calls runApp |
runFlutterAppAsync(builder, {loadingWidget, errorBuilder}) |
Mounts immediately with loading UI, starts host in background |
Lifecycle guarantees:
- The host is started exactly once.
- Shutdown is triggered by either widget disposal or
ApplicationLifetime.requestShutdown. Host.stop()βHost.dispose()are called in sequence during teardown.AppLifecycleState.detachedalso triggers shutdown (defensive).
A StatefulWidget that manages the full Host lifecycle declaratively.
| Parameter | Type | Description |
|---|---|---|
hostFactory |
Future<Host> Function() |
Creates and returns the host (called once in initState) |
child |
Widget |
The application widget shown when the host is ready |
loadingBuilder |
WidgetBuilder? |
Optional builder shown during startup |
errorBuilder |
Widget Function(BuildContext, Object)? |
Optional builder shown on startup failure |
Lifecycle safety:
mountedis checked beforesetStateβ no errors during async gaps.- Shutdown is idempotent β
ApplicationLifetimeguards against double-shutdown. - Async operations are fire-and-forget in
dispose()(Flutter'sState.dispose()is synchronous).
| Scenario | Behaviour |
|---|---|
Missing ServiceProviderScope |
FlutterError with actionable guidance (which widget to add and where) |
| Service not registered (required) | DI exception from getRequired<T>() |
| Service not registered (optional) | null from tryGet<T>() |
Host startup failure (runFlutterApp) |
Exception propagates to the caller; no widget tree mounted |
Host startup failure (runFlutterAppAsync) |
errorBuilder is called with the error |
Host startup failure (HostProvider) |
errorBuilder is called; defaults to red error text |
| Widget disposed during async startup | mounted check prevents setState on unmounted state |
| Double shutdown (dispose + detach) | Idempotent β ApplicationLifetime prevents double invocation |
Run the full test suite:
flutter testRun with verbose output:
flutter test --reporter expandedRun a specific test group:
flutter test --name "ServiceProviderScope"The package includes 15 tests covering:
ServiceProviderScopeβ provider exposure,of()/maybeOf(), rebuild behaviourServiceResolutionβgetService,tryGetService,serviceProvidergetterHostProviderβ loading/error/ready states, host start, host stop on disposeFlutterHostRunnerβrunFlutterApp,runFlutterAppAsync(loading gate, error)
See CONTRIBUTING.md for development setup, coding guidelines, and the pull request process.
See SECURITY.md for the vulnerability reporting process and supported versions.
MIT β see LICENSE for details.