“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:
- Where should we initialise each class?
- Where and how should we access them?
Let’s look at the following example:
HomePageWidget needs to access
HomeBloc, it will have to know how to initialise classes
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
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 this 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:
- We can use one
InheritedWidgetthat is the parent of all the widgets in our app. This way, every
StatefulWidgetcan access it.
- Or, we can create several
InheritedWidgets so that we can make the information “private” and accessible only to a subset of widgets. For example, we may want to keep the
ProfileBlocthat is used in the
ProfilePageWidgetprivate to the
FeedPageWidget, as such, we create a
For the current article we will create just one instance of 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
child parameter will be the child widget to which we want to share the data/instances, usually it is our
CupertinoApp widget. Finally, the
of method will allow any child of this widget to access it, provided they have a valid
InheritedWidget.of(context). We can see this pattern used in other Flutter classes such as
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.
In order to initialise each depedency, we create a
init() method that will handle the creation and injection of objects inside this widget.
With this approach, after we call
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.
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
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
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
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
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
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:
- If we are in a
StatelessWidget, this would have to go into our
buildmethod. This method can be called again in some special situations, so we would have to add additional checks in the state so that we are not calling
- If, by some reason, we have static information in our bloc, such as a String, the first time we could access it is after the first
buildsince we need to access
BuildContext, which means that to be able to show that String, we would need to rebuild the widget tree. Additionally, since we are accessing our bloc in the
buildmethod, the same problems that occur in the
StatelessWidgetwould happen here. The same problem would occur if we need to initialise our bloc with some data provided by the widget’s constructor. In this case, where we ideally would put this logic in
initState(), we would have to put it inside our
build()method with additional flags to not be adding data to our
Sinkin every rebuild.
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
onGenerateRoute of our
MyApp widget. Since here we have access to the
BuildContext, this can be a good place to initialise our widgets.
An important note. We might be tempted to refactor the following lines:
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:
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
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 access the objects stored in the
InheritedWidgetwe must always have a valid
- With the current setup, if some of our dependencies needs to access the
BuildContextwhen initialising, to get the
Localeof the phone for example, we would have to restructure our
initmethod by splitting in two: one that we could call for async operations before
runAppand another that we could call inside the
MyAppwidget when we have access to the
- This solution generates a lot of boilerplate code. In our app we currently have close to 500 lines of code in this class.
- By using
getHomeBloc(forceCreate : true)we are not disposing the previous bloc. This can be solved if we add further logic to our getter method.
- Though we must call the
init()method before accessing any data stored in this widget, there isn’t any indication that either the
init()method has been called or that it has completed the initialisation.
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.