Dependency Injection with Flutter
Tags: flutter, android, ios, tutorial
Published: 2021-06-18 05:57:01

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`](https://pub.dev/packages/flutter_catalyst) package with is a port of the [`catalyst`](https://pub.dev/packages/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`](https://pub.dev/packages/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`
```yaml
# pubspec.yaml
dependencies:
  catalyst_builder: ^1.0.1
```

Since we use the build_runner you need to add this to your dev_dependencies:
```yaml
# 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)
```yaml
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:

```dart
@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:
```dart
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:
```dart
@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:
```dart
/// 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 **pro**grammer 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.

```dart
// 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.
```dart
@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: 
   ```dart
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 ;-)