You know dependency injection? You love dependency injection!
Unfortunately, Flutter don't provide any built-in DI feature.
For this, I created last year the flutter_catalyst
package with is a port of the catalyst
package which is only supported for Dart native.
flutter_catalyst
was a good starting point for me to implement DI in my Flutter apps but in large projects it's a mess to configure.
In the last two months I created a new package catalyst_builder
which supports all platforms and is easy to configure.
This package uses the build_runner which performs tasks when you run it.
catalyst_builder
has a build_runner task that reads annotations from your dart files and generate a service provider for DI.
Setup
Run flutter pub add catalyst_builder
or add the package to your pubspec.yaml
# pubspec.yaml
dependencies:
catalyst_builder: ^1.0.1
Since we use the build_runner you need to add this to your dev_dependencies:
# pubspec.yaml
dev_dependencies:
build_runner: ^2.0.4
Create a build.yaml
beside your pubspec.yaml
. This file contains the configuration for the service provider (output file name and provider class name)
targets:
$default:
auto_apply_builders: true
builders:
catalyst_builder|buildServiceProvider:
options:
providerClassName: 'AppServiceProvider'
outputName: 'app_service_provider.dart'
Run flutter pub get
to install the packages
Now run flutter pub run build_runner watch --delete-conflicting-outputs
which watches for changes and create the service provider dart file
Usage
You can declare every class as a service with the @Service
annotation from the catalyst_builder
package:
@Service()
class MyService {
final String username = 'TestUser';
}
Ensure that flutter pub run build_runner watch --delete-conflicting-outputs
is running. You should see now a app_service_provider.dart
file that you can include in your project.
Create the service provider and retrieve the service from it:
var myProvider = AppServiceProvider();
myProvider.boot(); // This is important
var myService = myProvider.resolve<MyService>();
// also works: MyService myService = myProvider.resolve();
print(myService.username); // prints TestUser
Thats all for a simple service.
Nested services a.k.a. Dependency Injection
In the real world you've services that depend on other services that depend on configuration parameters etc.
catalyst_builder
also supports this scenario:
@Service()
class ServiceA {}
@Service()
class ServiceB {
final ServiceA serviceA;
ServiceB(this.ServiceA);
}
class ServiceC {}
@Service()
class ServiceD {
final ServiceC serviceC;
ServiceD(@Parameter('otherService') this.ServiceC);
}
void main() {
var myProvider = AppServiceProvider();
myProvider.boot();
// This works:
var serviceB = myProvider.resolve<ServiceB>();
// This not because ServiceC is not known as a service:
var serviceD = myProvider.resolve<ServiceD>();
// But this works, because the provider contains a
// parameter with the same name as the required argument:
myProvider.parameters['serviceC'] = ServiceC();
var serviceD = myProvider.resolve<ServiceD>();
// This also works, because the provider contains a
// parameter with the name which is given in the
// Parameter annotation.
myProvider.parameters['otherService'] = ServiceC();
var serviceD = myProvider.resolve<ServiceD>();
}
Service lifetime
By default, all services are singeltons. You will get the same instance everytime you call resolve<T>
.
You can specify the lifetime with the lifetime argument in the @Service
annotation:
/// Transient services are always recreated
@Service(lifetime: ServiceLifetime.transient)
class TransientService {}
/// Default is singleton
@Service(lifetime: ServiceLifetime.singleton)
class SingletonService {}
Code Against Interfaces, Not Implementations.
Every programmer would tell you that you shouldn't depend on implementations but interfaces.
Also this is possible with the exposeAs
Property in the @Service
annotation. Expose as will return the implementation if you request the type that you provide as exposeAs
. This also works for nested services.
// interface
abstract class BaseService {}
// implementation
@Service(exposeAs: BaseService)
class MyService implements BaseService {}
Preloading services
Some services are background services (connectivity checks for example).
Decorate this services with @Preload()
to create a instance of the service while boot()
-ing the provider.
@Service()
@Preload()
class MyService {
MyService(){
print('Service was created');
}
}
void main() {
ServiceProvider provider;
provider.boot(); // prints "Service was created"
provider.resolve<MyService>(); // Nothing printed
}
Flutter specific tips:
- Screens (widgets) should be always transient services.
- You can use
resolve<T>
in the router:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: '/',
routes: {
'/': (_) => container.resolve<HomeScreen>(),
},
);
}
}
Hope you like and use the package ;-)
Top comments (4)
Awesome! This looks really cool and I'm excited to try it! One question, is
flutter pub pub run build_runner watch --delete-conflicting-outputs
really the right command to generate the build_runner stuff? Looks like there might be an extra 'pub' in there.Thanks for your comment. Both
flutter pub pub ...
andflutter pub ...
should work.flutter pub pub
was necessary in a older version of Flutter.I updated the post and removed the redundant
pub
.Helpful article! Thanks! If you are interested in this, you can also look at my article about Flutter templates. I made it easier for you and compared the free and paid Flutter templates. I'm sure you'll find something useful there, too. - dev.to/pablonax/free-vs-paid-flutt...
Thanks 🙂