Handling Asynchronous Dependencies in Flutter & Dart

In Flutter and Dart applications, it is common to encounter scenarios where a class depends on an asynchronous operation. For instance, a client or service may need to fetch data from a network, or a database may need to establish a connection before being utilized. There are various ways to handle these dependencies efficiently. This article will explore five different approaches to managing asynchronous dependencies in your Dart code.

For example, we have a base class for all examples like a:

abstract class ClientBase {
  abstract final FutureOr<Dependence> _dependence;

  FutureOr<Result> fetch() async {
    final dependence = await _dependence;
    /* Do some computation with dependence */
  }
}

Passing the Dependency as a Parameter:

The first approach is to pass the dependency as a parameter to the method or constructor of your class. This lets you provide the dependency directly, ensuring it is available when needed. In the example code, the ClientPassed class receives the _dependence as a constructor parameter:

class ClientPassed extends ClientBase {
  ClientPassed(this._dependence);

  @override
  final Dependence _dependence;
}

Preparing the Dependency in the Constructor:

In this approach, you initialize the dependency directly within the class's constructor. This ensures the dependency is ready for use after the class is instantiated. The ClientPrepared class demonstrates this method by initializing _dependence in the constructor:

class ClientPrepared extends ClientBase {
  ClientPrepared() : _dependence = $dependenceInitializer();

  @override
  final Future<Dependence> _dependence;
}


Using a Factory Method to Initialize the Dependency:

Another approach is to utilize a factory method to create an instance of the class with the dependency already initialized. This method involves awaiting the dependency initialization before returning the class instance. The ClientFactory class shows this technique with its initialize() method:

class ClientFactory extends ClientBase {
  static Future<ClientBase> initialize() async {
    final dependence = await $dependenceInitializer();
    return ClientFactory._(dependence);
  }

  ClientFactory._(this._dependence);

  @override
  final Dependence _dependence;
}

Lazy Initialization of the Dependency:

In some cases, it is more efficient to initialize the dependency only when it is needed for the first time. This can be achieved through lazy initialization. The ClientLazy class in the example code achieves this by using the late keyword:

class ClientLazy extends ClientBase {
  ClientLazy();

  @override
  late final Future<Dependence> _dependence = $dependenceInitializer();
}

Lazy Initialization for Pre-Null Safety:

You can implement a similar approach using nullable types for projects that have not yet migrated to null safety. The ClientLazyNullable class demonstrates this method by checking if the _dependence is null before initializing it:

class ClientLazyNullable extends ClientBase {
  ClientLazyNullable();

  @override
  Future<Dependence> get _dependence => _$dependence ??= $dependenceInitializer();
  Future<Dependence>? _$dependence;
}

Handling async dependencies in Flutter and Dart can be achieved in various ways. The five tips provided in this article – passing the dependency as a parameter, preparing the dependency in the constructor, using a factory static method, and using lazy initialization with or without null-safety – can help developers choose the most suitable approach for their specific use case.