Skip to content

Instantly share code, notes, and snippets.

@Vanethos

Vanethos/blog.md Secret

Created April 12, 2019 07:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Vanethos/ed1f834f2bffcf4e71dac0b5b606db73 to your computer and use it in GitHub Desktop.
Save Vanethos/ed1f834f2bffcf4e71dac0b5b606db73 to your computer and use it in GitHub Desktop.

Injection and Initialisations in Flutter

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:

https://gist.github.com/fa38f4ca4a210563c5ddc0de86e60178

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:

  • 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.
  • 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 ProfileBloc that is used in the ProfilePageWidget private to the FeedPageWidget, as such, we create a ProfileInjectorWidget and a FeedInjectorWidget

[Image exemplifying]

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

https://gist.github.com/fdc660ef47eb1c4e0aeda6eca2fc461d

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.

https://gist.github.com/6d2008a1b29ba9f42743f51405a1deee

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

https://gist.github.com/e9b1706bca3b0329f556f2770d63ea99

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.

https://gist.github.com/d331a7c3856bcc3f40fa0254a88dfa46

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.

https://gist.github.com/fee1ad1ce24e3304728a44244c6cc2fc

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.

https://gist.github.com/ed803a20d72acc502083ad3180931f45

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:

  • 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.
  • 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.

https://gist.github.com/17a2a0f7090569152fdcea35996f0aae

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

https://gist.github.com/8224ed9405bf1edf46ae1cdb5b6ba134

To

https://gist.github.com/fac1d9f6e3e524fcd063ab0fbff21565

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:

https://gist.github.com/7473baf61cde1dea3ebbe5e6ff55d20f

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

https://gist.github.com/ef174cdac3634b07f1a8bda33336f817

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 access the objects stored in the InheritedWidget we must always have a valid BuildContext.
  • With the current setup, if some of our dependencies needs to access the BuildContext when initialising, to get the Locale of the phone for example, we would have to restructure our init method by splitting in two: one that we could call for async operations before runApp and another that we could call inside the MyApp widget when we have access to the BuildContext.
  • 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.

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