context_di 0.2.4
context_di: ^0.2.4 copied to clipboard
Flutter Provider based Dependency Injection Tool supports two main scopes factory and singleton with code generation
Project is based on Provider package. Main goal is to create a tool that will help reduce boilerplate code and make it easier to work with dependency injection via Provider.
Features #
2 scopes are supported:
- factory scope (creates instance every time)
- singleton scope (one instance per feature)
Parameterized factories
final bloc = context.read<CreateEntityBloc>()(context, (id: _id));
//or
final bloc = createEntityBloc(context, (id: _id));
Scopes based on widget tree lifecycle:
Added Code Generation #
add build_runner and context_di_generator to your project
dev_dependencies:
build_runner:
context_di_generator:
create empty feature class that extends FeatureDependencies with _$(ClassName)Mixin
part 'basic_feature.g.dart';
typedef EntityBlocParams = ({int id});
void _dispose(BuildContext context, Repository instance) => instance.dispose();
@Feature()
@Singleton(Repository, as: RepositoryInterface, dispose: _dispose)
@Factory(ListBloc)
@Factory(EntityBloc, params: EntityBlocParams)
class BasicFeature extends FeatureDependencies with _$BasicFeatureMixin {
const Basic({super.key, super.builder});
}
then run dart run build_runner build -d
to generate feature file
generation could be combined with manual registration approach
@Feature()
@Singleton(SupabaseAuthUtils)
@Singleton(GetUserHandler)
@Singleton(SupabaseAuthApi)
@Singleton(AuthRepository, as: IAuthRepository)
@Singleton(ExternalOpenUrl)
class AppFeature extends FeatureDependencies with _$AppFeatureMixin {
const AppFeature(this.initialDependencies, {super.key, super.builder});
final List<Registration> initialDependencies;
@override
List<Registration> register() => [
...initialDependencies, //some initial dependencies
...super.register(), //generated dependencies
registerSingleton<AppBloc>(
(c) => AppBloc(c.resolve(), c), dispose: (c, bloc) => bloc.close()
), //manual registration
];
}
if clas has constructor with BuildContext parameter, context will be passed to constructor automatically
Resolve approach #
Now better approach is use code generation and resolve like this:
factories:
final bloc = context.read<CreateListBloc>()(context);
final bloc = context.read<CreateEntityBloc>()(context, (id: _id));
//new preferred way
final bloc = createEntityBloc(context, (id: _id));
createEntityBloc
generated top level function
CreateListBloc
and CreateEntityBloc
will be generated
singletons:
final repo = contex.read<RepositoryInterface>();
old context.resolve<T>()
approach still works
Getting started #
Create a feature class that extends FeatureDependencies
and override register
method.
When you need to register a dependency, use registerSingleton
, registerSingletonAs
,
registerFactory
or registerParamsFactory
methods.
typedef EntityBlocParams = ({int id});
class BasicFeature extends FeatureDependencies {
const BasicFeature({super.key, super.builder});
@override
List<Registration> register(BuildContext context) {
return [
registerSingletonAs<Repository, RepositoryInterface>(
(context) => Repository(context.resolve()),
dispose: (context, instance) => instance.dispose(),
),
registerFactory(
(context) => ListBloc(context.resolve<RepositoryInterface>()),
),
registerParamsFactory(
(context, EntityBlocParams params) =>
EntityBloc(
params.id,
context.resolve(),
),
),
];
}
}
Add to your widget tree
@RoutePage()
class BasicFeaturePage extends StatelessWidget {
const BasicFeaturePage({super.key});
@override
Widget build(BuildContext context) {
return BasicFeature(builder: (context) {
return _Content();
});
}
}
And resolve dependencies in feature context
class _Content extends StatefulWidget {
const _Content();
@override
State<_Content> createState() => _ContentState();
}
class _ContentState extends State<_Content> {
late final ListBloc _listBloc;
int? _selectedId;
@override
void initState() {
_listBloc = context.resolve<ListBloc>(); //old approach
_listBloc = context.read<CreateListBloc>()(context); //old approach
_listBloc = createListBloc(context); //new approach
super.initState();
}
@override
void dispose() {
_listBloc.close();
super.dispose();
}
//...
}
@RoutePage()
class EntityPage extends StatelessWidget {
final int _id;
const EntityPage({super.key, required int id}) : _id = id;
@override
Widget build(BuildContext context) {
return BlocProvider<EntityBloc>(
create: (_) => context.resolveWithParams((id: _id)),
child: BlocBuilder<EntityBloc, EntityState>(builder: (context, state) {
return Scaffold(
body: switch (state) {
Initial() => Center(child: CircularProgressIndicator.adaptive()),
Loaded() => _Content(state),
},
);
}),
);
}
}
basic resolve
final listBloc = context.resolve<ListBloc>(); //old approach
final listBloc = context.read<CreateListBloc>()(context); //old approach
final listBloc = createListBloc(context); //new approach
parametrized resolve
var create = (_) => context.resolveWithParams((id: _id)); //old approach
var create = (_) => context.read<CreateEntityBloc>()(context, (id: _id)); //old approach
var create = (_) => createEntityBloc(context, (id: _id)); //new approach
Additional information #
IMPORTANT NOTICE! You can use resolve
or resolveWithParams
only in feature context, in lover
levels of widget tree.
If you have some core dependencies register them on app level:
class MyApp extends StatelessWidget {
MyApp({super.key});
final _router = AppRouter();
@override
Widget build(BuildContext context) {
return _RootDependencies(
builder: (context) => MaterialApp.router(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
routerConfig: _router.config(),
),
);
}
}
class _RootDependencies extends FeatureDependencies {
const _RootDependencies({super.builder});
@override
List<Registration> register(BuildContext context) {
return [
registerSingleton<Logger>((_) => Logger()),
];
}
}