This content originally appeared on DEV Community and was authored by Julian Finkler
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 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 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 ;-)
This content originally appeared on DEV Community and was authored by Julian Finkler
Julian Finkler | Sciencx (2021-06-18T05:57:01+00:00) Dependency Injection with Flutter. Retrieved from https://www.scien.cx/2021/06/18/dependency-injection-with-flutter/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.