DEV Community

loading...
Cover image for Dependency Injection with Flutter

Dependency Injection with Flutter

Julian Finkler
Programmierer aus Leidenschaft. Ich lege besonders Wert auf Clean Code und liebe es mich durch legacy Code zu wühlen... und den dann zu refaktorieren 😉.
Updated on ・3 min read

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
Enter fullscreen mode Exit fullscreen mode

Since we use the build_runner you need to add this to your dev_dependencies:

# pubspec.yaml

dev_dependencies:
  build_runner: ^2.0.4
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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';
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>();
}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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>(),
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Hope you like and use the package ;-)

Discussion (4)

Collapse
carlmobiledev profile image
Carl Wills

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.

Collapse
devtronic profile image
Julian Finkler Author • Edited

Thanks for your comment. Both flutter pub pub ... and flutter pub ... should work.

flutter pub pub was necessary in a older version of Flutter.

I updated the post and removed the redundant pub.

Collapse
pablonax profile image
Pablo Discobar

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...

Collapse
devtronic profile image
Julian Finkler Author

Thanks 🙂