DEV Community

Cover image for Dependency Injection in Flutter: implementing the Dependency Inversion principle
Bernardo Iribarne
Bernardo Iribarne

Posted on

Dependency Injection in Flutter: implementing the Dependency Inversion principle

Photo by Vardan Papikyan on Unsplash

If you are looking to get your code to the next level, you have to know about dependency injection.

Introduction

When we write code we follow the SOLID guidelines. One of those principles (“D”) take care of Dependency Inversion. Let's see what it means.

Suppose you have a service class called “UserService” that uses a datasource called UserSqlite to get users from a SQLite database:

Package 1 depends on Package 2

So Package 1 depends on Package 2. If you have this relation on your code, Mr. SOLID will go to your desktop and say: “Ey, do NOT couple your code with other libraries, what are you thinking about?”.

What about this:

Package 2 depends on Package 1

What we have already done was change the dependency direction. Now, Package 2 depends on Package 1. The right direction of dependency makes our code reusable, and eliminates cyclic dependencies.

Dependency Injection

The dependency injection concept will help us to drive the right implementation of the interface IUserDataSource to our UserService.

UserService wouldn’t be concerned about the IUserDataSource implementation, it just uses it.

How can you implement Dependency Injection in Flutter?

I use a package called get_it, you can find it here:

GetIt

I like to wrap the third party packages, because maybe tomorrow I want to change it to another that I like more than this. It is a premise that I have, you can skip that step, but it is one more thing that helps me to have my code the most descouple than I can.

So, I created a class called DependencyManager which wraps the get_it library:

Dependency Manager

Inject dependencies

In your app config class or your app initializer you have to inject the dependencies you want to have. In the example of the second picture, we have to inject the implementation of IUserDataSource:

 void injectDependencies() {
    try {

      /// Get dependency manager
      DependencyManager manager = DependencyManager();

      /// Register UserSQLite as IUserDataSource
      manager.registerFactory<IUserDataSource>(() => UserSQLite());


    } catch (e2) {
      debugPrint("Manage depency injections error");
    }
  }
Enter fullscreen mode Exit fullscreen mode

And then we must use this manager everywhere we need to use the user data source implementation. Let’s see:

class UserService {

  /// User datasource.
  final IUserDataSource datasource= DependencyManager().get<IUserDataSource>();

  ....
}
Enter fullscreen mode Exit fullscreen mode

DependencyManager manages the implementations of our interface. It registers and returns concrete classes.

DependencyManager

To have the example simple I just showed you how to register a factory for our user datasource, but the dependency manager also could call constructors that requires some parameters, could be a little more complex.

My DependencyManager looks like this:

import 'package:get_it/get_it.dart';

class DependencyManager {

  /// GetIt instance.  
  final getIt = GetIt.instance;

  /// I use singleton to create this manager.
  DependencyManager._internal();

  static final DependencyManager _singleton = DependencyManager._internal();

  factory DependencyManager() {
    return _singleton;
  }

  T get<T extends Object>(){
     return getIt.get<T>();
  }

  T getWithParam<T extends Object, P1>(dynamic p){
    return getIt.get<T>(param1: p);
  }

  T getWith2Param<T extends Object, P1, P2>(dynamic p1, dynamic p2){
    return getIt.get<T>(param1: p1, param2: p2);
  }

  bool isRegistered<T extends Object>(){
    return getIt.isRegistered<T>();
  }

  void registerLazySingleton<T extends Object>(FactoryFunc<T> factoryFunc){
    getIt.registerLazySingleton<T>(factoryFunc );
  }

  void registerFactory<T extends Object>(FactoryFunc<T> factoryFunc){
    getIt.registerFactory<T>(factoryFunc );

  }

  void registerFactoryParam<T extends Object, P1, P2>(
      FactoryFuncParam<T, P1, P2> factoryFunc, {
        String? instanceName,
      }){
    getIt.registerFactoryParam<T,P1,P2>(factoryFunc);
  }

}

Enter fullscreen mode Exit fullscreen mode

Conclusion

We talked about a SOLID principle, the dependency inversion, we introduced to dependency injection and saw how to get implemented it in Flutter.

Now you have the concept, and you know how to apply it. You will drive your code to the next level:

  1. Descoupled

  2. Testeable

  3. Easy to mantain

Thanks for reading, Clap if you like it!

Let me know your comments below.

Top comments (0)