Skip to content

Async services ​

Some services can't be used the moment they're constructed. A database has to open, a config file has to load, a socket has to connect. With Complete Dependency Injector, the service announces when it's ready, and the injector won't hand it out or build anything that depends on it until then.

Mark the service as ready-aware ​

Add InjectableWithReadyMixin, start the async work in the constructor, and call setInjectableReadyResult(true) when it's done.

dart
class Database with InjectableWithReadyMixin {
  Database() {
    _open();
  }

  static final Factory = InjectableFactory.withoutDependencies<Database>(
    () => Database(),
  );
  static final DefaultProvider = Provider(Database);

  bool isOpen = false;

  Future<void> _open() async {
    // Stand-in for real async work: opening a file, a socket, a connection pool.
    await Future<void>.delayed(const Duration(milliseconds: 50));
    isOpen = true;
    // Tell the injector this service is ready to hand out.
    setInjectableReadyResult(true);
  }

  Future<List<String>> query(String sql) async => ['Ada', 'Grace'];
}

Depend on it like any other service ​

UserRepository doesn't know or care that Database is asynchronous. It just takes one in its constructor.

dart
class UserRepository {
  UserRepository(this._db) : dbWasOpenWhenCreated = _db.isOpen;

  static final Factory = InjectableFactory.withDependencies<UserRepository>(
    DependencyDataBuilder.list([Database]),
    (deps) => UserRepository(deps.get<Database>()),
  );
  static final DefaultProvider = Provider(UserRepository);

  final Database _db;
  final bool dbWasOpenWhenCreated;

  Future<List<String>> allUsers() => _db.query('SELECT name FROM users');
}

Ask for it ​

getService completes only once the whole chain is ready:

dart
final injector = Injector([
  Database.DefaultProvider,
  UserRepository.DefaultProvider,
]);
injector.registerFactories([Database.Factory, UserRepository.Factory]);

// Completes only after Database has called setInjectableReadyResult(true),
// so UserRepository never sees a closed database.
final users = await injector.getService<UserRepository>();
print(users.dbWasOpenWhenCreated); // true
print(await users.allUsers()); // [Ada, Grace]

The repository is created after the database has opened, so it can use it straight away.

In Flutter: loading states for free ​

DependenciesBuilder reports loading until every dependency of the ViewModel is ready, so a slow service turns into a spinner with no extra code. Press Reload to open a fresh database.

Async services demoOpen full screen ↗

The service:

dart
class SlowDatabase with InjectableWithReadyMixin {
  SlowDatabase() {
    _open();
  }

  // Instance scope: each screen that provides it gets a fresh connection,
  // so reloading the screen opens the database again.
  static final Factory = InjectableFactory.withoutDependencies<SlowDatabase>(
    () => SlowDatabase(),
    scope: InjectableScope.instance,
  );
  static final DefaultProvider = Provider(SlowDatabase);

  static Duration openDelay = const Duration(seconds: 2);

  final _users = BehaviorSubject<List<String>>();
  Stream<List<String>> get users => _users.stream;

  Future<void> _open() async {
    await Future<void>.delayed(openDelay); // pretend to connect
    _users.add(['Ada Lovelace', 'Grace Hopper', 'Margaret Hamilton']);
    setInjectableReadyResult(true); // now the injector hands it out
  }

  @override
  Future<void> dispose() => _users.close();
}

The ViewModel and screen:

dart
class UsersViewModel extends DependencyBuilderViewModelBase {
  UsersViewModel()
      : super(dependencyDataList: DependencyDataBuilder.list([SlowDatabase]));

  late SlowDatabase _db;
  List<String> users = [];

  @override
  void setPropertiesOnDependencyResolution(Dependencies deps) {
    _db = deps<SlowDatabase>();
  }

  @override
  Iterable<Stream> getListOfStreamsToObserve() => [_db.users];

  @override
  void updateViewModelPropertiesOnStreamEmission(List snapshots) {
    users = snapshots[0] as List<String>;
  }
}
dart
class AsyncLoadingDemo extends StatefulWidget {
  const AsyncLoadingDemo({super.key});

  @override
  State<AsyncLoadingDemo> createState() => _AsyncLoadingDemoState();
}

class _AsyncLoadingDemoState extends State<AsyncLoadingDemo> {
  int _generation = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Padding(
          padding: const EdgeInsets.all(16),
          child: OutlinedButton.icon(
            // A new key builds a new scope, so a new SlowDatabase opens.
            onPressed: () => setState(() => _generation++),
            icon: const Icon(Icons.refresh),
            label: const Text('Reload'),
          ),
        ),
        Expanded(
          child: InjectorScopeWidget(
            key: ValueKey(_generation),
            providers: [SlowDatabase.DefaultProvider],
            child: DependenciesBuilder<UsersViewModel>(
              cleanViewModelInstance: UsersViewModel(),
              builder: (context, vm) => dependencyBuilderViewModelSimpleStateHandlingBuildUtilityFn(
                context: context,
                viewModel: vm,
                loadingWidget: const Center(
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      CircularProgressIndicator(),
                      SizedBox(height: 12),
                      Text('Opening database...'),
                    ],
                  ),
                ),
                build: (context, vm) => ListView(
                  children: [for (final name in vm.users) ListTile(leading: const Icon(Icons.person), title: Text(name))],
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

SlowDatabase uses InjectableScope.instance, so each InjectorScopeWidget that provides it gets its own connection. Changing the scope's key builds a new scope, and so a new database.

Why not return a Future from the factory?

A factory's creator function must return the service type itself, so () => Database.open() (a Future<Database>) doesn't type-check. Use InjectableWithReadyMixin for async setup.

Released under the MIT License.