Gonçalo Palma
April 13, 2019

“Dependency Injection” in Flutter with InheritedWidget

Flutter and Dart gives us a lot of liberty. So much that we can write an entire app in just one file, combining UI, business logic and API calls in a tremendous dart file. However, we tend not to do this as software developers, for many reasons. For starters, it’ll be a mess to test the code, it’ll be difficult to implement new features or correct bugs while traversing through thousands of lines of code and it will also could prove troublesome for future new elements in the project, since a monolithic file of code can be daunting.

That’s why we tend to decouple the code of our app in several classes: network, models, bloc and widgets, etc… However, since these classes depend on each other, we start to notice a couple of issues:

Let’s look at the following example:

class HttpClient {
	//...
}

class NetworkEndpointsA {
	HttpClient client;

	NetworkEndpointsA(this.client);
}

class NetworkEndpointsB
	HttpClient client;

	NetworkEndpointsB(this.client);
}

class HomeBloc {
	NetworkEndpointsA endpointsA;
  NetworkEndpointsA endpointsB;

	HomeBloc(this.endpointsA, this.endpointsB);
}

If our HomePageWidget needs to access HomeBloc, it will have to know how to initialise classes HttpClient, NetworkEndpointsA and NetworkEndpointsB. But what if a DetailsPageWidget also depend on this bloc? Should we copy the code from one class to another? And how can we create a single instance of HomeBloc so that both the HomePageWidget and DetailsPageWidget can access and modify?

One solution to this problem would be to have this bloc declared globally so that every class could access it, but this tactic makes testing our code more difficult and can make the managing and disposing of the bloc objects more tricky. You can read a StackOverflow answer about why global singletons should be avoided.

Since we want to avoid using global objects, we can take advantage of the InheritedWidget. This widget can have only one child, and it’s purpose is to hold data and make it accessible to its children. To use this widget we can have two possible approaches, that can be used at the same time:

[Image exemplifying]

For the current article we will create just one instance of the InheritedWidget.

class InjectorWidget extends InheritedWidget {
 InjectorWidget({
    Key key,
    @required Widget child,
  }) : assert(child != null),
       super(key: key, child: child);

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

  @override
  bool updateShouldNotify(InjectorWidget old) => false;
}

The updateShouldNotify parameter tells the widget if it should notify his children if he is updated and force them to rebuild. Why is this set to false? Presumably, we will create the instances of our classes when the app starts. When creating a new instance, by explicit action of a child widget, the other children do not need to be rebuilt, and as such, they don’t need to be notified by the InjectorWidget. The child parameter will be the child widget to which we want to share the data/instances, usually it is our MaterialApp or CupertinoApp widget. Finally, the of method will allow any child of this widget to access it, provided they have a valid BuildContext, via InheritedWidget.of(context). We can see this pattern used in other Flutter classes such as Navigator.

Since the InjectorWidgetwidget will be responsible for the creation of the dependencies in our app, we will declare each dependency as a private variable that can only be accessed by a public getter. In our case, we just want to expose the HomeBloc, so we will only create that public getter.

class InjectorWidget extends InheritedWidget {
	//...
	
	HomeBloc _homeBloc;
	EndpointsA _endpointsA;
	EndpointsB _endpointsB;

	HomeBloc getHomeBloc() => _homeBloc;	
}

In order to initialise each depedency, we create a init() method that will handle the creation and injection of objects inside this widget.

class InjectorWidget extends InheritedWidget {
	//...

	void init() {
		httpClient = HttpClient();
		endpointA = EndpointsA(httpClient);
		endpointB = EndpointsB(httpClient);
		_homeBloc = HomeBloc(endpointA, endpointB);
	}
}

With this approach, after we call init(), the HomeBloc can be accessed by any child of the InjectorWidget. However, what happens if we need to create a new instance of the HomeBloc? Will we need to call init() and force the initialisation of every dependency in our project? What if there is another bloc, ProfileBloc that we don’t want to be initialised again?

To solve this, when we call getHomeBloc()we validate if our _homeBloc variable is null. If it is, then we should create a new HomeBloc and assign it to this variable. If not, then we should return the value that we assigned to the homeBlocvariable. Now a ProfileWidget can call getHomeBloc() and access the instance that is used by the HomeWidget. However, we can go one step ahead by adding a bool parameter that will dictate if we need a new instance of the bloc, even if the InheritedWidget already holds a reference to one. This can be used when we want to reset the bloc state.

class InjectorWidget extends InheritedWidget {
	//...

	HomeBloc getHomeBloc({bool forceCreate = false}) {
		if (_homeBloc == null || forceCreate) {
			_homeBloc = HomeBloc(endpointA, endpointB)
		}
		return _homeBloc;
	}

	//...
}

Now getHomeBloc() can be used both to create a new instance of the HomeBloc or access the current instance hold in the InjectorWidgetor, it can force the creation of a new HomeBloc with getHomeBloc(forceCreate = true).

InjectorWidget Initialization

We currently have a InjectorWidget that can provide us the HomeBloc, given a valid BuildContext. To be able to access this bloc, we will first need to initialise it via the init() function, that we will assume as async.

class InjectorWidget extends InheritedWidget {
  //...

	void init() async {
		// this can be an access to the SharedPreferences, for example
   	var token = await getToken();
		var httpClient = HttpClient(token);
		_endpointA = EndpointsA(httpClient);
		_endpointB = EndpointsB(httpClient);
		_homeBloc = HomeBloc(endpointA, endpointB);
	}	
}

The purpose of this widget is to be accessed by all of the remaining widgets in our app. In fact, we may even want for our MaterialApp or CupertinoApp to access it if the initialisation process gives us some critical information that is needed to decide the initial route, or widget, or our app. For this reason, we will create the widget and initialise it in our main() method.

void main() async {
	var injector = InjectorWidget(child : MyApp());
	// assume that the `init` method is an async operation
	await injector.init();
	runApp(injector);
}

When running the above code we notice the following: we get a black screen before our app launches. While this screen is shown, Flutter is initialising the engine and also running our injector.init() before creating the first widget, and since Flutter doesn’t have anything configured to display, it shows the black screen. In order to avoid this, we can take advantage of the Native Splash Screen. When using this solution, Flutter will show the native Android and iOS splash screen when initialising the code, giving us the opportunity to show our app logo. To know more about how to use a native splash screen in a Flutter app, please refer to this article.

As we can se above, our InjectorWidgethas MyAppas a child. This means that both MyApp and its children will be able to access our injector via InjectorWidget.of(context), including the HomePageWidget that can access the HomeBloc. However, some issues might arise:

From the issues raised above, we reach the following conclusion: it would be best to initialise the block outside the State of our widgets, or outside our StatelessWidgets. We can do this by using Named Routes. Using named routes, every time we use Navigator.of(context).pushNamed(routeName), we are either calling the routes or onGenerateRoute of our MyApp widget. Since here we have access to the BuildContext, this can be a good place to initialise our widgets.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Injection Example",
      theme: ThemeData(
        primaryColor: Colors.blue,
        accentColor: Colors.pink,
      ),
      initialRoute: "/",
      routes: {
			// other routes...
        "/home": (context) { 
			var bloc = InjectorWidget.of(context).getHomeBloc(forceCreate = true));
			return HomePageWidget(
            bloc: bloc)},
      }
  );
}

An important note. We might be tempted to refactor the following lines:

routes: {
	// other routes...
  "/home": (context) { 
		var bloc = InjectorWidget.of(context).getHomeBloc(forceCreate = true));
		return HomePageWidget(bloc: bloc);
	},
}

To

routes: {
	// other routes...
  "/home": (context) =>  
		HomePageWidget(
            bloc: InjectorWidget.of(context).getHomeBloc(forceCreate = true))},
}

This approach has an issue: Since in our declaration we are getting directly the bloc instance, this means that every time the whole widget has to be recreated, we are going to be calling again the InjectorWidget to provide us with a new HomeBloc. This can happen if we are using Navigator.of(context).pushNamed(otherRoute) to push, but not replace, a new route over our HomeWidget. When this new route pops, the code:

HomePageWidget(
            bloc: InjectorWidget.of(context).getHomeBloc(forceCreate = true))}

Is going to be called again and our HomeBloc is going to be recreated. If, on the other hand, we declare the bloc as a local variable, every time the widget has to be recreated, we are going to call

HomePageWidget(bloc: bloc)

Referencing the bloc that we created when we first navigated to this page.


And that’s it! 🙌 We have created an InheritedWidget that allows us to store and access objects in it without using any external packages. Though this approach can be used in many Flutter projects, it has some drawbacks:

To conclude, this approach does not invalidate the need for dependency injection or service locator packages, we should search them, learn how they are used and eventually use them in our projects! Nonetheless, if we see that the drawbacks that this approach has don’t impact our project, we can consider a “pure Flutter” solution.

Want to get the latest articles and news? Subscribe to the newsletter here 👇

Diving Into Flutter

In Diving into Flutter we will discover the Flutter Framework, bit by bit.

From the BuildContext, to pubspec files, going through exotic backend solutions, there won't be a stone left unturned.

Each week I'll also share one or two links for interesting resources I found around the Web

And for other articles, check the rest of the blog! Blog - Gonçalo Palma

Want to get the latest articles and news? Subscribe to Diving Into Flutter

Hey! I'm Gonçalo Palma! 👋

I often share Deep Diving articles on Flutter 💙