Flutter’s Dependency Injection?
Another more intuitive yet subtle approach to DI in Flutter
A quick search of the phrase, “Dependency Injection” in Flutter Community will give you a list of articles. All are very informative and offer libraries and ‘service locators’ to implement Dependency Injection. This article will offer an alternative to implementing code and or annotations to supply DI.
Now, I’m not picking on Marc Guilera in particular and his article, Dependency Injection In Flutter. It’s a good read. I only took issue with the two bullet points in his article now displayed below as they pertained to the example code displayed further below.
- It’s impossible to mock the wheel to test the
Car
class in isolation. - If you had a
SteelWheel
and aPlasticWheel
it would not be possible to interchange them without changing the consumer.
class Car {
private Wheel wheel = Wheel()
drive() {
wheel.spin()
}
}
It involved dependency injecting of different types of ‘Wheels’ into the consumer class, Car. He noted the one solution is a parameter of type, Wheel, in the constructor of the consumer class allowing you to create a particular type of Wheel ‘outside’ the class, Car. In the end, he offers his own library, dependencis_flutter, as another solution to overcome these issues. Great work. It is a good read. My article, however, will supply one other approach.
I felt Arathi Shankri, in his article, Flutter Simple Dependency Injection, gave a rather fulfilling definition of Dependency Injection. It’s presented below.
“Dependency Injection (DI) is a technique used to reduce tight coupling between classes thus achieving greater reusability of your code. Instead of your class instantiating/creating the dependent classes/objects, the dependent objects are injected or supplied to your class; thus maintaining an external library of sorts to create and manage object creation in your project. Since you have externalized object creation from your actual view/model; those classes can in turn plainly focus on their actual application logic. And since you some of such classes that we instantiate are singletons, it is even more convenient to hold a basket of objects for future reference.” — Arathi Shankri in Flutter Simple Dependency Injection
He too follows up in his article with a library package that implements DI, called, “flutter_simple_dependency_injection.” Further articles in the Flutter Community publication recommend still other libraries and what are called ‘service locators’ like get and get_it. All recognizing how DI encourages less coupling, more modular code, reusable code, parallel development, easier maintenance, and so on. Allow me to now supply one more approach.
Explain By Example
We’re going to combine that ‘Car’ example with the ol’ Counter app created for you every time you create a new Flutter project. There are three screenshots below of three separate classes instantiating into an instance variable of the class type, Wheel. The variable itself is called ‘wheel’ and is assigned inside the consumer class, Car. Alongside is a screenshot of the sample app announcing the ‘type of wheel’ currently created. Keeping faith with the original example, there will be three types of wheels: a plain old, Wheel, a Steel wheel, and a Plastic Wheel.
Accompanying these three screenshots, are the following two bullet points:
- It’s possible to mock the wheel to test the
Car
class in isolation. - If you had a
SteelWheel
and aPlasticWheel
it’s possible to interchange them without changing the consumer.
Indeed, in all three screenshots, there’s nothing changed in the consumer class, Car. Of course, this blatantly goes against the two bullet points listed at the start of this article. This is done without the use of specialized libraries or Service Locators — or annotations for that matter.
This other approach allows for unit testing. For example, let’s say one of those ‘wheel types’ (let’s pick the ‘plastic wheel’) is a test object easily created with empty methods or no substantial code which mocks the intended role of such a dependency so the inner workings of the consumer class, Car, can instead be tested. Further, I’m able to switch out the ‘type of wheel’ without changing one bit of code in the consumer class, Car. Now, how is this done without the ‘DI boilerplate code’ traditionally prescribed?
I Like Screenshots. Tap Caption For Gists.
As always, I prefer using screenshots in my articles over gists to show concepts rather than just show code. I find them easier to work with frankly. However, you can click or tap on their captions to see the code in a gist or in Github. Tap or click on the screenshots themselves to zoom in on them.
Let’s begin.
DI By Design
Like many solutions in Life, it starts with a sound foundation. As it pertains to this case, I’ve taken the time and made the effort to ‘physically separate’ the code even in this simple app into three recognized categories. Literally, in one directory is the code responsible for the app’s data, another directory contains the code responsible for the app’s interface, and still another location contains the code for the app’s logic and event handling.
With every project, I apply a design pattern and impose a specific architecture dictating the way the code is organized and the way pieces of code talks to each other. In this example, it’s the MVC design pattern. In short: M is for Model (data), V is for View (Interface), and C is for Controller (Logic & Event Handling). Right from the get-go, you have the decoupling of code readily available to you when using such a design pattern.
Below is the ‘View’ for the ‘Counter’ sample app. More specifically the Widget that’s returned from the State object’s build() function is considered the View component in the MVC architecture. Traditionally, with MVC, one or more Controller is provided to the View to respond to any and all events that may occur. In many cases, the Controller would contain the overall logic for the app. The red arrows below highlight points of interest.
Note, as simple as this app is, it still delegates the Controller unimaginatively named, Controller, to take care of things when the floating button is pressed. The controller has its own method again unimaginatively named, incrementCounter, to perform the necessary operations. Finally, it’s to the Controller to display the current count with its public property not surprisingly called, counter. It works like a charm just like the original example app, but now the code is separated into distinct areas of responsibility.
No doubt, you also noticed the conspicuous insertion of the class, Car, in the sample code with its one-line defining an instance,car = Car()
, and another bit of code, car.wheel.type
, displaying the ‘type of wheel’ at the time. I know, I know — not the most sophisticated demonstration. Go away. Leave me alone. It’ll have to do with such a simple sample app. Let’s continue.
Below is a screenshot of the class, Car. It’s the very same code displayed in other articles except with an import statement displayed — the other articles didn’t bother to display the import statements. I suspect that’s because the assumption was if you wanted to switch out dependencies, for example, you would have to switch out import statements thus changing the consumer class.
Highlighted in the screenshot below is the class, Wheel. It is being instantiated and assigned to the instance variable, wheel, of class type, Wheel. In this article, I’ll present an approach used and promoted by Google itself when organizing a library package to allow you to introduce Dependency Injection. There’s no ‘Re-inventing the Wheel’ in this approach.
Export Dependency
In this approach, the Dependency Injection will come from the practice of producing library files (.dart files) that contain nothing but ‘export statements’ pointing to other library files not publicly accessible under the directory, lib/src. I’ll call these specific files, export files.
Following this MVC architecture, three such files are found in a designated location in the project — in the directory, src. With this design pattern, each of these three files contains export statements only accessing source code concerned with specific areas of interest. The names of these files are, model.dart, view.dart, and controller.dart.
Let’s first look at the Model export file. It exports and makes accessible the one lone library file involving the data source for the app.
The next export file is called, view.dart, and contains all the code that pertains to the app’s interface. I’ve decided to place both Flutter’s Material and Cupertino libraries in this file since much of the code they contain is also concerned with the ‘look and feel’ of your app. Of course, this export file also contains the file, home_page_view.dart — the very ‘View’ for this simple app.
Finally, there is the Controller export file that exports all the Controllers for this particular app. Note the path of each file listed below. These files are all living in a directory called, controller.
It is in this file where all the magic happens. Notice the last three export statements are commented out. Let’s walk through this now, and reveal the mechanism involved in ‘switching out’ the different types of ‘Wheel.’
Below are three screenshots of the running sample app. Each screenshot has a different type of ‘Wheel’ injected and accessed by the consumer class, Car. It’s the export file, controller.dart, that is changed to provide a specific type of ‘Wheel’ to the consumer class. In the first screenshot, we see the original class, Wheel, is being exported (third line from the top) and, of course, the sample app displays it as such on the screen.
The next screenshot below displays the word, ‘Steel’, in the sample app’s screen. Looking at its corresponding export file, controller.dart, we see the original export statement for the class, Wheel, is now commented out, and it is now exporting the file, steel_wheel.dart. Finally, with the last screenshot displaying the word, Plastic, the export file is now only exporting the file, plastic_wheel.dart. In all this, those two bullets points hold true.
Inject The Wheel
Of course, to inject these other two classes (Steel and Plastic), they also have to be a subtype of, Wheel. However, here’s the trick: When the other two are displayed, the instance variable, wheel, is not exactly the same ‘Wheel’ type. So not to change the consumer class, all you do is ‘switch out’ the class, Wheel, with a class that has the same name.
For example, when we changed the export file, controller.dart, and uncomment the line to insert the SteelWheel class, we had to comment out the original line exporting the class, Wheel. The Dart compiler would have complained. In Dart, you can’t have more the one class named, Wheel, defined in the same scope.
'package:dependencyinjection/src/controller/steel_wheel.dart';
What’s In A Name
Look below on the left-hand side at the library file, steel_wheel.dart. The little red arrow is pointing at a class also named, Wheel. It extends the class, SteelWheel which, in turn, extends the original class, Wheel. It is this class hierarchy that allows for the Dependency Injection in an unchanged consumer class. The class type, Wheel, in the consumer class, Car, is suddenly not the very same class type, Wheel, moments before. It’s now the one defined in the file, steel_wheel.dart, and not the one defined in the file, wheel.dart.
It’s the use of the ‘c’ prefix in the screenshots below that makes all this possible. It’s the prefix that allows the original class, Wheel, to be accessed and extended even in a file containing a class with the same name. The approach is a little sneaky, granted, but a somewhat elegant approach, no?
In the next screenshot, is the second type of wheel, Plastic. Now I suggested the class, PlasticWheel, be a test class. And so, it will ‘mock’ the usual behavior of the class, Wheel. It even extends the class, Mock, commonly used when a class takes on such a role. Therefore, the class, PlasticWheel, ‘implements’ instead of ‘extends’ the original type, Wheel.
And so again, looking below at the screenshot of the class, Car, if the word ‘Steel’ is displayed on your phone’s screen, you then know the instance variable, wheel, highlighted below is defined by the class, Wheel, in the file, steel_wheel.dart.
This approach is modular — the rest of the app doesn’t know of the changes being made in that one export file, controller.dart. The consumer class, Car, may know of the class type called, Wheel, but doesn’t know of the class types, SteelWheel, and PlasticWheel. It doesn’t need to. Simply put, there’s nothing like an import statement to inject a dependency into source code.
I know some may think the other approaches using libraries and such are better, but it’s my opinion they’re not so much better as they are a lazy approach. It’s ‘lazy code.’ It’s throwing in the dependency, granted, but with the cost of more boilerplate.
Let’s Inject A Service
To be a little more well-rounded in my presentation, let’s now apply one of the DI implementations mentioned in the many articles. Let’s see how would one, for example, implement the popular simple service locator, get_it, into this very same simple example app.
Now below, the class, Car, has been changed slightly. A service has been introduced. It literally ‘gets’ the wheel for the class, Car. Like the original Car class, nothing will be changed any further in this class — the service alone will provide the three types of wheels (Wheel, Steel, and Plastic) depending on particular circumstances.
Inspect The Injector
The file, injector.dart, is self-contained. It’s displayed in full in the screenshot below for your review. It too is modular — the rest of the app doesn’t know of the library package, get_it.dart. It’s doesn’t need to. The consumer class, Car, knows of the class type called, Wheel, but doesn’t know of the class types, SteelWheel, and PlasticWheel. It doesn’t need to. The three get() functions highlighted by the last three red arrows, serve as the API for the consumer class, Car.
Let’s continue now and present how such an implementation could be applied in this example. Let me suggest in most instances, the various ‘class types’ are not used all at once or in quick succession. They’re instead instantiated in separate instances to, for example, make unit testing possible, to allow for the switching out of business logic, or to allow for a different data source.
Note, I am unable to uncomment all three of the ‘get’ methods without again causing the Dart compiler to complain. You can’t have more than one method with the same name within the same class. I could rename the three methods making each unique, but that would then require the consumer class, Car, to be changed to use each one. That would compromise one of the bullet points.
Like the three screenshots of the simple app above, the ‘wheel type’ is again displayed in the screenshots below. However, this time, each comes about with the use of a different get() function having been uncommented and called. With this approach, you have those two bullet points still satisfied. And so, it is this lone file, injector.dart, that is changed to supply a ‘different class’ of a ‘related type’ in the form of Dependency Injection.
Note, the original class, Wheel, continues to be left unchanged — its screenshot is below. Further, the two subtypes continue to extend the original class, Wheel, leaving their own ‘Wheel class’ left untouched — they’re simply ignored by the Service Locator.
In the export file, controller.dart, the ‘Service Locator’ is introduced with an export statement to the library file, injector.dart. The export statement for the original class, Wheel, is returned while its three subtypes are commented out.
This may all be semantics, but I wanted to provide an alternate implementation of DI. Other implementations of DI described so far included ‘Constructor Dependency Injection’ and ‘Setter Dependency Injection.’ While I chose to ‘keep it simple’ and turn to the abilities of a programming language itself and to the very nature of decomposing what makes up a software solution into its manageable parts. Such an approach also encourages less coupling, more modular code, reusable code, parallel development, easier maintenance, and as it happens — Dependency Injection.
Note The Annotations And Services
As a side note, Annotations were originally used to ‘provide commentary on the code’ — making code more readable, but it would evolve ‘to instruct and direct code at runtime’ as well. With the popular release of Java Platform, Enterprise Edition (Java EE), annotations were used to inject a particular dependency class into another class at runtime. I never liked the use of annotations for DI. As clean as it is, it’s also not as transparent as other means, and I prefer to avoid their use for Dependency Injection.
Of course, with the use of Service Locators, you could switch out ‘related classes’ in real-time. Personally, that’s the only reason I would use a service locator myself. For example, situations where dependencies are optional to a consumer class in real-time, or when dependencies vary to a consumer class in real-time. A screenshot of the class, Car, quickly modified demonstrates how one might inject the three types of ‘Wheel.’ It works as you’d expect — the instance variable, wheel, would be assigned a different class in quick succession. If such a requirement arises, this would be a feasible approach.
Define Your Dependency
In conclusion, allow me now to modify Arathi Shankri’s explanation of Dependency Injection just a little bit to demonstrate where I’m coming from with this alternate approach:
“Dependency Injection (DI) is a technique used to reduce coupling between classes thus achieving greater reusability of code. Instead of your class defined & instantiated in the same library/file as its consumer classes/objects, these dependency classes are instead imported/injected to your class. Such an arrangement thus allows for an external library of sorts and better manages object creation in your project. In MVC for example, because you’ve externalized an depending controller’s definition from its actual view/model; each of those three components will now naturally focus on their particular logic — reducing coupling. As an aside, collecting such classes into service locators when such a need arises, allows you to hold a basket of such objects for future reference, but only when the need for such a basket of ‘common type’ objects is required.”
How’s that?
Those other articles do offer solutions backed with sound circumstances and feasible benefits. I also recognize some of the authors of those articles and acknowledge them as irreplaceable contributors to our fledging flutter community. I can only hope to be just as helpful with my own contributions. I’m definitely a fan/follower. Now, go away. Leave me alone.
Cheers.