FilledStacks

Share this post

The #1 Required Pattern for Flutter Developers

filledstacks.substack.com

The #1 Required Pattern for Flutter Developers

Dependency Inversion, what it is, why we need it and how to apply it.

Dane Mackier
May 12, 2023
7
1
Share
Share this post

The #1 Required Pattern for Flutter Developers

filledstacks.substack.com

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.

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

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

Thank you for reading, I’ll see you in the next one.

Thanks for reading. Don’t miss the next one. Subscribe below.

7
1
Share
Share this post

The #1 Required Pattern for Flutter Developers

filledstacks.substack.com
1 Comment
Fernando
May 15Liked by Dane Mackier

A placer like always !!

Expand full comment
Reply
Top
New
Community

No posts

Ready for more?

© 2023 FilledStacks (Pty) Ltd
Privacy ∙ Terms ∙ Collection notice
Start WritingGet the app
Substack is the home for great writing