The #1 Required Pattern for Flutter Developers
Dependency Inversion, what it is, why we need it and how to apply it.
The refactor that makes all Flutter code testable
Often in a systemized process, there are some steps that have an outsized return compared to others. This post will cover that step.
Previously I worded it as “Separating your Application Code from its usage” but it’s much better described as “Inverting dependencies at every level”.
The previous 3 steps separate your codebase into 4 well-defined layers, UI, State, Business Logic, and Application Code, making your code base numerous times more scalable. The next step is to invert dependencies in each layer.
Dependency Inversion is a Software Design Pattern that is fundamental to building a robust architecture. Even if you’re not after a testable code base, it’s a pattern applied to large codebases at an almost universal level.
A major goal of software architecture is loose coupling. Without inverting your dependencies you cannot reach that point.
The goal of this post is not to convince you of the importance of the step. It’s to teach you how to apply it. What you’ll get from this post is:
How to identify a dependency
How to invert a dependency
How to start the processs
How to identify a dependency
A dependency, put simply, is an object that another object depends on. In code this is most clearly seen when you construct an instance of something to use in your class.
class MyClass {
final _myDependency = MyDependency();
void useDependency() {
final result = _myDependency.doStuff();
if(result) {
// Do other stuff
}
}
}
This makes it difficult to unit test. An unit test has to be deterministic. The test should be in the correct state and the result should always be the same. To achieve that you need to be able to mock your dependencies.
In the case above, if you construct MyClass
you will automatically construct MyDepdenency
which means when you call useDependency
in your test you don’t know what to expect.
How to Invert a Dependency
Inverting a dependency in simple terms means “to get the dependent object from outside of the current object”. There are 2 main ways of achieving this:
Dependency Injection
Service Location
My preferred way of doing it for Flutter, until we get Macros’s 😉, is Service Location. Both are possible within Flutter and Dart, I’ll share some nice packages to use later in this post. Let’s start with the most popular method.
Dependency Injection
There are 3 types of dependency injection:
Constructor Injection (most common)
Method injection
Property Injection
All serve the same purpose, to remove a dependency on a specific object and accept it from the outside. We’ll only cover Constructor and Method Injection.
Constructor Injection
The most common form of dependency injection, is when you supply a dependency from the outside. So instead of having
class MyClass {
final _myDependency = MyDependency();
void useDependency() {
final result = _myDependency.doStuff();
if(result) {
// Do other stuff
}
}
}
You instead supply the dependency through the constructor.
class MyClass {
final MyDependency myDependency;
MyClass({required this.myDependency});
void useDependency() {
final result = myDependency.doStuff();
if(result) {
// Do other stuff
}
}
}
What this means is, when you’re writing a unit test, you can now supply your mock implementation of that class to return deterministic results for you. Method injection is similar, but doesn’t serve the entire class.
Method injection
You can imagine that if only a single function is dependent on the dependency, then you can use this type of injection, but I want to show you a different way it’s also used. Lets say you have a function that returns the formatted date for today in a special way.
class TimeHelper {
String getFancyFormatForToday() {
final now = DateTime.now();
return now.fancyFormat;
}
}
This function has a dependency on the DateTime now, which means it would be impossible to unit test. You can’t check when calling this function what the result is, it will be different every time it’s called.
To fix this we pass this dependency from the outside.
class TimeHelper {
String getFancyFormatForToday({DateTime? now) {
final timeToUse = now ?? DateTime.now();
return now.fancyFormat;
}
}
What this allows us to do is pass in the time we want to use for the unit test into the function, removing the hard dependency on the current system time. With these simple versions you can apply the DI (Dependency Inversion) principle to any class.
Implementation
The best package that I’ve seen for this type of inversion is Injectable. It generates code for the get_it
service locator package and manages all your constructor dependency injection for you.
Service Location
My preferred implementation of DI is using service location. Instead of injecting the dependency, you ask for the dependency from an object that either knows how to construct the dependency, or has a reference to an already constructed dependency. This is how that commonly looks. We go from:
class MyClass {
final _myDependency = MyDependency();
void useDependency() {
final result = _myDependency.doStuff();
if(result) {
// Do other stuff
}
}
}
To something like this.
class MyClass {
final _myDependency = locator<MyDependency>();
void useDependency() {
final result = _myDependency.doStuff();
if(result) {
// Do other stuff
}
}
}
Instead of constructing the dependency, we ask from the locator for a dependency of type MyDependency
. It will then either construct a new one or return one if it had already been constructed.
With this you remove the requirement on the object that constructs your class to have the other objects to supply, and also makes mocking a bit easier + reduces any signature change issues that might occur.
Recommendation
I use get_it as my preferred service locator and it has worked amazingly from day 1. It has all the required functionalities like Factories, LazySingleton, Singletons, Async resolving, etc.
Starting the process
Now that you have the practical, concrete knowledge you can start applying this to your code base to make it more testable. This is my general implementation plan when it comes to my architecture.
Invert dependencies in the Business Logic that relies on Application Code objects: This allows you to gain the benefit of having code make sure that your business logic is 100% as expected, all the time. Since that’s the most important part of your app.
Invert dependencies in the State logic that relies on Business Logic objects: Another important factor in your application is how the user interacts with it. Being able to put it into any state and check that your state logic responds appropriately is quite important.
With this 2 in place you can unit test all the important code and start moving faster with an automated code base.
If you enjoyed this, subscribe to get more like this directly in your inbox every week.
Let’s work together
Book a 1-on-1 call with me to take the next steps for your Flutter project.
A placer like always !!