avatarGreg Perry

Summary

The web content discusses an alternative approach to Dependency Injection (DI) in Flutter, emphasizing a design pattern and architecture that allows for DI without relying on specialized libraries or service locators.

Abstract

The article presents a nuanced method for implementing Dependency Injection in Flutter applications, challenging the conventional use of libraries and service locators. It advocates for a design pattern that separates code into distinct categories: Model (data), View (interface), and Controller (logic and event handling). This approach leverages Dart's language features and the MVC architecture to enable DI by organizing library files and using export statements to manage dependencies. The author demonstrates this through a 'Car' example, showing how different types of 'Wheels' can be injected into the 'Car' class without changing its code. The article also touches on the use of a service locator like 'get_it' but ultimately promotes a more manual, yet modular and transparent, DI technique that aligns with the natural decomposition of a software solution into its constituent parts.

Opinions

  • The author believes that traditional DI methods, such as using libraries or service locators, introduce unnecessary boilerplate code and can be considered a "lazy" approach.
  • The author values the use of Dart's inherent capabilities for DI, favoring it over annotations or external libraries for greater transparency and simplicity.
  • There is a preference for DI techniques that encourage less coupling, more modular code, reusability, parallel development, and easier maintenance.
  • The author suggests that service locators should be used sparingly, only when there is a real-time need for dynamic dependency switching.
  • The article implies that the Flutter community's reliance on certain DI libraries and service locators may not always be the most effective or elegant solution.
  • The author expresses a desire to contribute meaningfully to the Flutter community, acknowledging the work of other contributors while offering an alternative perspective on DI.

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 a PlasticWheel it would not be possible to interchange them without changing the consumer.
class Car {  
   private Wheel wheel = Wheel()    
   
   drive() {    
      wheel.spin()  
   }
}
Dependency Injection in Flutter

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.

Flutter Simple Dependency Injection

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

car.dart
car.dart

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 a PlasticWheel 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.

Other Stories by Greg Perry

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.

car.dart

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.

home_page_view.dart

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.

car.dart

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.

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

model.dart

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.

view.dart

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

controller.dart

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.

steel_wheel.dart and plastic_wheel.dart

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.

car.dart

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.

car.dart

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.

injector.dart

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.

injector.dart

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.

injector.dart

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.

wheel.dart
steel_wheel.dart and plastic_wheel.dart

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.

controller.dart

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.

car.dart

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.

→ Other Stories by Greg Perry

DECODE Flutter on YouTube
Flutter
Programming
Android App Development
iOS App Development
Mobile App Development
Recommended from ReadMedium