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.
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.
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:
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.
The service:
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:
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>;
}
}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.