davianspace_hosting_flutter

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.

Dart Flutter License: MIT pub package


Table of contents


Features

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

Installation

Add to your pubspec.yaml:

dependencies:
  davianspace_hosting_flutter: ^1.0.3

Then run:

flutter pub get

Quick start

Imperative (runFlutterApp)

The 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()

Async imperative (runFlutterAppAsync)

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)

Declarative (HostProvider)

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

Ecosystem integration

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

Resolving services

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);
}

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        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.

Advanced usage

Keyed services

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');

Multiple service implementations

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);
}

Custom loading and error UI

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(),
)

Programmatic shutdown from a widget

ElevatedButton(
  onPressed: () {
    final lifetime = context.getService<ApplicationLifetime>();
    lifetime.requestShutdown();
  },
  child: const Text('Shutdown'),
)

Widget testing with DI

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);
});

API reference

ServiceProviderScope

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.

BuildContext extensions (ServiceResolution)

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.

FlutterHostRunner

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.detached also triggers shutdown (defensive).

HostProvider

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:

  • mounted is checked before setState — no errors during async gaps.
  • Shutdown is idempotent — ApplicationLifetime guards against double-shutdown.
  • Async operations are fire-and-forget in dispose() (Flutter's State.dispose() is synchronous).

Error handling

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

Testing

Run the full test suite:

flutter test

Run with verbose output:

flutter test --reporter expanded

Run a specific test group:

flutter test --name "ServiceProviderScope"

The package includes 15 tests covering:

  • ServiceProviderScope — provider exposure, of() / maybeOf(), rebuild behaviour
  • ServiceResolutiongetService, tryGetService, serviceProvider getter
  • HostProvider — loading/error/ready states, host start, host stop on dispose
  • FlutterHostRunnerrunFlutterApp, runFlutterAppAsync (loading gate, error)

Contributing

See CONTRIBUTING.md for development setup, coding guidelines, and the pull request process.

Security

See SECURITY.md for the vulnerability reporting process and supported versions.

License

MIT — see LICENSE for details.

Libraries

davianspace_hosting_flutter
Enterprise-grade Flutter integration for the DavianSpace hosting runtime.