Skip to content

Instantly share code, notes, and snippets.

@matanlurey
Created August 23, 2022 01:36
Show Gist options
  • Save matanlurey/4651ced7feb145bd8d0e1e8f4075aed5 to your computer and use it in GitHub Desktop.
Save matanlurey/4651ced7feb145bd8d0e1e8f4075aed5 to your computer and use it in GitHub Desktop.
Some notes I have on the dependency injection frameworks for Dart/Flutter today

Survey of Dependency Injection Packages for Dart

I searched for "Dependency Injection" and looked at the top ~10 or so on pub.dev.

Configuring

class AppModule extends Module {
  @override
  List<Bind> get binds => [
    Bind.factory((i) => XPTOEmail())
    Bind.factory<EmailService>((i) => XPTOEmailService(i()))
    Bind.singleton((i) => Client(i()))
  ];
  
  // ...
}

Usage

final client = Modular.get<Client>();

// Or with a default value
final client = Modular.get<Client>(defaultValue: Client());

Notes

  • Supports other types of Flutter-specific utilities, like around build context and routing
  • Seems to support hot reloading, but not clear how unique this is.

Configuring

// Singleton.
KiwiContainer container = KiwiContainer();

// Not a singleton.
KiwiContainer container = KiwiContainer.scoped();
container.registerInstance(Sith('Anakin', 'Skywalker'));
container.registerInstance(Sith('Anakin', 'Skywalker'), name: 'DartVader');
container.registerInstance<Character>(Sith('Anakin', 'Skywalker'), name: 'DartVader');

Usage

Sith theSith = container.resolve<Sith>();
Sith theSith = container.resolve<Sith>('DartVader');
Sith theSith = container.resolveAs<Character, Sith>();

Notes

  • Includes code generation for factory generation
  • Allows hiding errors

Configuring

// Initializing a text string instance through a method toInstance()
Binding<String>().toInstance('hello world');

// Initializing a text string instance
Binding<String>().toProvide(() => 'hello world');

// Initializing an instance of a string named
Binding<String>().withName('my_string').toInstance('hello world');
class AppModule extends Module {
  @override
  void builder(Scope currentScope) {
    bind<ApiClient>().toInstance(ApiClientMock());
  }
}

Usage

final str = rootScope.resolve<String>();
final str = rootScope.tryResolve<String>();

Configuring

// Import the service provider 
import 'my_entrypoint.catalyst_builder.g.dart';

@GenerateServiceProvider()
void main() {
  // Create a new instance of the service provider
  var provider = DefaultServiceProvider();
  
  // Boot it to wire services
  provider.boot();

  // Resolve a service.
  var myService1 = provider.resolve<MyService>();

  // Inferred types are also supported
  MyService myService2 = provider.resolve();
}

Usage

@Service()
@Preload()
class MyService {
  MyService() {
    print('Service was created');
  }
}

void main() {
  ServiceProvider provider;
  provider.boot(); // prints "Service was created" 
  provider.resolve<MyService>(); // Nothing printed
}
void main() {
    /// create a Scope
    Scope()
    
    /// inject values
    ..value<int>(ageKey, 18)
    ..value<int>(heightKey, 182) // centimetres
    /// single value from factory method
    ..single<Db>(incomeKey, () => calcIncome)
    /// sequence of values from factory method.
    ..sequence<int>(countKey, () => tracker)
    
    /// run some code within the Scope
    ..run(() => a();
}

Usage

void printRealWorth() {
  print('You are the ${use(countKey)} person to ask');
  print('You are ${use(ageKey)} years old');
  print('You earn ${use(incomeKey} per hour.');
}

Configuring

final repoModule = Module([
  single<GithubRepo>(({params}) => GithubRepo(get<GithubService>())),
]);

final remoteModule = Module([
  single<GithubService>(({params}) => GithubService()),
]);

final appModule = [viewModelModule, repoModule, remoteModule];

Usage

//default
final service = inject<GithubService>();
//pass parameters
final test = inject<HomeProvide>(params: ['title']);
//different scope
final test = inject<HomeProvide>(scope:test,params: ['title']);

Configuring

final appModule = {
    single((i) => Api()), 
    single<Repository>((i) => MyRepository(i.get())),
    factory((i) => UseCase(i.get())), 
    factoryWithParams((i, p) => ViewModel(i.get(), p["dynamicParam"])),
};

Usage

final _loginViewModel = Stark.get<LoginViewModel>();

What's missing?

There are a reasonable amount of existing dependency injection and inversion of control (IoC)-style packages; some are opinionated (either Flutter only, or deep integration with Flutter), and others are generic; some are entirely runtime based, some have optional code generation, and yet others require code generation.

Typically, the documentation is lighter than you'd want for a concept such as dependency injection, but Dart as a whole has a more immature package ecosystem, and this isn't a specific issue to dependency injection.

In terms of Dart idiomatic, this is where almost every solution does something different or even surprising.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment