Mastering dependency injection in Flutter
Tags: flutter, dependencyinjection, mobile, dart
Published: 2023-08-10 09:58:47

## Depend on the abstract class and not the concrete class
One of the 5 SOLI **D** principles capitulates that a high-level module should depend only on abstract class not on the implementation. This leads to less coupling between modules (more [here](https://en.wikipedia.org/wiki/Dependency_inversion_principle)).

In this article we will focus on how to apply this pattern in flutter. 

### What are we going to use
[get_it](https://pub.dev/packages/get_it) is a [service locator](https://en.wikipedia.org/wiki/Service_locator_pattern) used to get at run-time the concrete class of the abstract class we are requesting. 
[injectable](https://pub.dev/packages/injectable) and [injectable_generator](https://pub.dev/packages/injectable_generator) will be used to annotate and generate the get_it's code ([an official example of code generation](https://docs.flutter.dev/data-and-backend/serialization/json#serializing-json-using-code-generation-libraries)).

### Why injectable?
When the service locator pattern is used throughout the whole application and each abstract class can have several concrete classes it is very useful not to have to think about the implementation of get_it but to **let the code generation take care of it**. 

By importing the libraries the `pubspec.yaml` will look like the following:

```yaml
dependencies:
  injectable:
  get_it:
  ...

dev_dependencies:
  injectable_generator:
  ...
```

## A concrete use case 

### Naive
An application needs to display a list of users and the user 's detail. 
An idea might be to create a `UserRepository`.

```dart
// foo.dart

/// business logic module
class HighLevelModule {
  void foo() {
    final users = UserRepository().getUsers();
  }
}

// user.dart

/// user model
class User {
  const User({required this.id, required this.displayName});
      
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'],
      displayName: json['displayName'],
    );
  }
  
  final String id;
  final String displayName;
}

// user_repository.dart
class UserRepository {
  
  Future<List<User>> getUsers() async {
    // Don't take this as an example of how to make an http call
    final response = await http.get(Uri.parse('https://example.com/api/users'));
    final List<dynamic> usersData = json.decode(response.body);
      
    return usersData.map((userData) => User.fromJson(userData)).toList();
  }
  
  Future<User> getUser({required String id}) async {
    // Don't take this as an example of how to make an http call
    final response = await http.get(Uri.parse('https://example.com/api/users/$id'));
    return User.fromJson(json.decode(response.body));  
  }
}
```

**In this way the caller (high-level module) will depend on the low-level `UserRepository` module**. 

### A better approach
Applying dependency inversion to decouple the two modules we can create:

```dart
class HighLevelModule {
  void foo(UserRepository repository) {
    // doesn't know the repository implementation
    final users = repository.getUsers();
  }
}

abstract class UserRepository {
    Future<List<User>> getUsers(); 
    Future<User> getUser({required String id});
}

class ConcreteUserRepository implements UserRepository {
  // previous UserRepository code
}
```

## Now magic comes into play
Create a `injection.dart` file like:
```dart
import 'injection.config.dart';

@InjectableInit(
    initializerName: 'init',
    preferRelativeImports: true,
    asExtension: false, 
)
FutureOr<void> configureInjection() => init(getIt);
``` 

Annotate `ConcreteUserRepository`

```dart
@LazySingleton(
  as: UserRepository,
)
class ConcreteUserRepository implements UserRepository {
  ...
}
```

([singleton](https://refactoring.guru/design-patterns/singleton), [lazy](https://www.linkedin.com/advice/1/how-do-you-compare-contrast-lazy-eager-evaluation))

The code generation command (`flutter pub run build_runner watch --delete-conflicting-outputs`) will create an `injection.config.dart` file with the `get_it` code needed to get the concrete class from the abstract one. 

`getIt<UserRepository>()` will return the most suitable concrete class among those registered.

## Multiple implementation
Now that the high-level module no longer depend on   implementation, we can use different implementations of `UserRepository` based on environment variables ([Configuring apps with environment](https://dart.dev/guides/environment-declarations)).

```dart
@LazySingleton(
  as: UserRepository,
  env: ["prod"]
)
class ConcreteUserRepository implements UserRepository {
  ...
}

@LazySingleton(
  as: UserRepository,
  env: ["dev"]
)
class DemoUserRepository implements UserRepository {
   @override
  Future<List<User>> getUsers() async {
    return [User(id: "1",displayName: "A"),User(id: "2",displayName: "B")];
   }
  
    @override
  Future<User> getUser({required String id}) async {
    return User(id: "1",displayName: "A");
  }
}

// file injection.dart
@InjectableInit(...)
FutureOr<void> configureInjection(String env) => init(
      getIt,
      //will be taken the concrete class that contains the env `env`. 
      environmentFilter: NoEnvOrContains(env),
    );
```

Now we can test our high-level module by ignoring the user repository implementation.

## Conclusion
This concludes the article on dependency inversion.

For more information on dependency inversion in flutter I recommend reading [get_it](https://pub.dev/packages/get_it) and [injectable](https://pub.dev/packages/injectable) documentation.