Dependency Injection in Dart/Flutter Apps

Dependency Injection in Dart/Flutter Apps

Dependency Injection is a way of making the dependencies of an object available via another object, and these dependencies are usually known as services.

These services can be blocks of code, containing different functionalities that can be easily reused in different parts of your project.

Dependencies are functionalities required by parts of your project to run successfully. In many cases, you'd import files, and use classes or methods inside them, that way the Classes using those imports are dependent on them.

In dart, the most basic way to handle dependency injection is by to pass services to a class through the constructor.

class MyClass {

//MyClass is dependent on OtherClass
OtherClass service;

  MyClass(this.service)
}

This method might not seem bad at first (there's nothing wrong with it), but it becomes very problematic to pass values deep down widget trees and things can get messy quickly when handling many dependencies.

Dependency Injection Options For Flutter

There are many DI methods and packages we can use in our flutter apps and we'll be taking a look at a few in this post:

  • Inherited Widget - comes with flutter out of the box.
  • IOC dart package - an easy to use package available on pub.dev.
  • get It dart package - can be found here on pub.dev.

In this article, the examples will be simple and minimal, Its goal is to serve as an intro to DI. I will make other detailed articles that show how to use these concepts in a real app.

Inherited Widgets

Inherited Widgets allows you to pass data down widget trees easily. An example of an inherited widget is shown:

import 'package:flutter/material.dart';

class InheritedHomeWidget extends InheritedWidget {
  InheritedHomeWidget({Key key, this.child}) : super(key: key, child: child);

  final Widget child;

  static InheritedHomeWidget of(BuildContext context) {
    return (context.inheritFromWidgetOfExactType(InheritedHomeWidget)
        as InheritedHomeWidget);
  }

  @override
  bool updateShouldNotify(InheritedHomeWidget oldWidget) {
    return true;
  }
}

This is an auto-generated Inherited Widget from the flutter VS code extension, let's take a look at what the code contains.

  • A widget called child (can be named anything) is passed in, this is where you put your sub widget tree.
  • The next block of code looks up the widget tree, finds the closest InheritedWidget and registers the BuildContext with that widget so that it can rebuild the tree when that widget is changed in any way.
  • updateShouldNotify() returns a boolean that states whether the subtree widgets should be rebuilt when a change occurs to the InheritedWidget.
Use In The Widgets

To make your dependencies available in this widget tree, you can maybe send the dependencies via the Inherited Widget's constructor, or use a getter, or any way that works for your use case, you will still be able to use it as deep in the tree as you want.

InheritedHomeWidget({Key key, this.child, this.homeText})
      : super(key: key, child: child);

final Widget child;
//add the new dependency
final String homeText;

Wrap your entire widget tree with the Inherited Widget you created, if you want your dependencies to be propagated down the whole tree. In this case, I'll simply pass a text into the constructor, this would probably be an instance of a service.

class FunApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return InheritedHomeWidget(
      homeText: 'This is printed on the screen',
      child: MaterialApp(
        title: 'Inherited Widget',
        home: FunHomePage(),
      ),
    );
  }
}

You can then Use that text down the tree by calling .of(context) on the inherited widget, like so:

class FunHomePage extends StatelessWidget {
  const FunHomePage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    var homeText = InheritedHomeWidget.of(context).homeText;
    return Container(
      color: Colors.white,
      child: Center(
        child: Text(homeText),
      ),
    );
  }
}

IOC package

IOC stands for Inversion Of Control, and it's a pattern that states that services/dependencies should be created in a separate class, and is the underlying principle behind Dependency Injection.

This package is very easy to use, It allows you to initialize your service as a singleton and you can bind your dependency to any data type of choice.

To use this package, you first need to add it to your pubspec.yaml file like this:

dependencies:
  ioc: ^0.3.0

Next, create a dart file to bind all your services. I call mine ioc_locator.dart, you can call it whatever you want. The content of this file is:

import 'package:ioc/ioc.dart';

void iocLocator() {
  Ioc().bind('service1', (ioc) => InfoService());
}
  • Here, I'm binding the class(service) InfoService to a string "service1" which acts sort of like a key, and it is dynamic.
    Ioc().bind(key, (ioc) => Service());
    
  • The InfoService is another very simple class that just has a string in it called infoText.

Now, you want to run the iocLocator() function in the main file before your app runs, so import it and add it above your runApp() function like so:

void main() {
  iocLocator();
  runApp(MyApp());
}

Now you can use the data from that service in your widgets and files, I'll attempt to print the infoText out.

import 'package:flutter/material.dart';
import 'package:ioc/ioc.dart';

class IocView extends StatelessWidget {
  final infoService = Ioc().use('service1');

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(infoService.infoText),
      ),
    );
  }
}

To see other cool features with the IoC package, check out their pub.dev page.

Get It

Get it is one of the more popular ways to handle DI in flutter apps. You can also register your dependencies as singletons, lazy singletons or factory.

  • A singleton will always return the same instance of that service.
  • A lazy Singleton will create the object on the first instance when it is called. This is useful when you have a service that takes time to start and should only start when it is needed.
  • A Factory will return a new instance of the service anytime it is called.
Usage

First, add get_it to your pubspec.yaml file:

get_it: ^3.1.0

Now, you can create a file to register all your objects, I'll call mine service_locator.dart, and place a single function in it called getServices().

import 'package:get_it/get_it.dart';

GetIt getIt = GetIt.instance;

void getServices() {
  getIt.registerFactory(() => InfoService());
  getIt.registerSingleton(() => MyService());
  getIt.registerLazySingleton(() => OtherService());
}

As of version 2.0.0, get_it was remade into a singleton, you now get the same instance of get_it with GetIt.instance or GetIt.I.

Now place the getServices() function before running the app in your main.dart file:

void main() {
  setupLocator();
  runApp(MyApp());
}

To use the registered objects in your widgets, simply call locator.get<Type>() or locator<Type>(), where type, in our case, is InfoService.

class InfoView extends StatelessWidget {
  final infoService = getIt.get<InfoService>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(infoService.infoText),
      ),
    );
  }
}

Why Should I Use Dependency Injection?

  • DI is helpful for better development experience.
  • Adding a bit of structure to your projects.
  • Great for mocking objects when writing tests.

That's all for this article! you can reach me on Twitter, and check out my other articles.