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-outputsreally 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 pubwas 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 🙂