Photo by Garett Mizunaka on Unsplash

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:

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 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 InheritedWidget that is the parent of all the widgets in our app. This way, every StatelessWidget and StatefulWidget can access it.

that is the parent of all the widgets in our app. This way, every and can access it. Or, we can create several InheritedWidget s so that we can make the information “private” and accessible only to a subset of widgets. For example, we may want to keep the ProfileBloc that is used in the ProfilePageWidget private to the FeedPageWidget , as such, we create a ProfileInjectorWidget and a FeedInjectorWidget

App flow with InheritedWidget

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

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 InjectorWidget widget 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 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 homeBloc variable. 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.

Now getHomeBloc() can be used both to create a new instance of the HomeBloc or access the current instance hold in the InjectorWidget or, 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 .

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.

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 InjectorWidget has MyApp as 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 build method. 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 getHomeBloc() several times.

, this would have to go into our method. 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 several times. 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 build since 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 build method, the same problems that occur in the StatelessWidget would 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 Sink in 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 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.

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

To

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