service_waiter 1.0.0 copy "service_waiter: ^1.0.0" to clipboard
service_waiter: ^1.0.0 copied to clipboard

Simple service locator

Decoding the Code: ServiceWaiter, Emulating SwiftUI's Dependency Injection in Flutter #

In the realm of managing dependencies, simplicity and efficiency are key. This principle is reflected in ServiceWaiter, a powerful Flutter library for managing dependencies, drawing inspiration from SwiftUI's dependency injection.

The Core of ServiceWaiter #

The ServiceWaiter manages all the services in your application by providing a central location for registration and retrieval. It primarily consists of two parts:

  1. A ServiceWaiter class that manages dependencies.
  2. A set of extensions on Flutter's Widget and BuildContext classes for easy access to dependencies within widgets.

The ServiceWaiter Class #

final class ServiceWaiter {
  // Cache for storing instantiated dependencies
  final _cache = <Type, Object>{};

  // Factory for creating dependencies
  final _factory = <Type, Object Function()>{};

  ...
}

The ServiceWaiter class has a _cache to store instantiated dependencies and a _factory that holds functions responsible for creating the dependencies. Dependencies can be registered with the register method, and retrieved with the get method. This method checks the cache for an existing instance or creates a new one if not present. clearSingleton and clear methods are available to remove specific or all cached instances, respectively.

Widget and BuildContext Extensions #

extension Environment on Widget { ... }
extension ContextDependency on BuildContext { ... }

The Environment extension is defined on the Widget class and provides a method to create a ServiceWaiterProvider, passing an updater function. The ContextDependency extension is defined on the BuildContext class and offers a dependency method to retrieve dependencies from the nearest ServiceWaiterProvider in the widget tree. This retrieval is done recursively, ensuring that the method finds the closest instance of the required dependency.

A SwiftUI-Inspired Example: Routes and Authentication #

Let's explore an example showing how ServiceWaiter can elegantly handle passing dependencies to routes and manage a global authentication use-case.

class AuthUseCase {
  bool isAuthenticated() {
    // authentication logic here
  }
}

class UserInfo {
  // user information here
}

class AppSettings {
  // app settings here
}

void main() {
  runApp(
    MaterialApp(
      home: HomePage().environment(
        (serviceWaiter) {
          serviceWaiter.register((_) => AuthUseCase());
        },
      ),
      onGenerateRoute: (settings) {
        switch (settings.name) {
          case '/profile':
            return MaterialPageRoute(
              builder: (context) => ProfilePage().environment(
                (serviceWaiter) {
                  serviceWaiter.register((_) => UserInfo());
                },
              ),
            );
          case '/settings':
            return MaterialPageRoute(
              builder: (context) => SettingsPage().environment(
                (serviceWaiter) {
                  serviceWaiter.register((_) => AppSettings());
                },
              ),
            );
          default:
            return MaterialPageRoute(builder: (context) => HomePage());
        }
      },
    ),
  );
}

In this example, an AuthUseCase dependency is registered in the HomePage. Since AuthUseCase will likely be needed throughout the application, registering it at the root of the widget tree makes it accessible everywhere. Notice how we can pass different dependencies to different routes via the constructor.

class ProfilePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final authUseCase = context.dependency<AuthUseCase>();
    final userInfo = context.dependency<UserInfo>();

    if (!authUseCase.isAuthenticated()) {
      return

 Text('Please log in to view your profile.');
    }

    // build profile details using userInfo
  }
}

class SettingsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final appSettings = context.dependency<AppSettings>();

    // build settings page using appSettings
  }
}

In ProfilePage and SettingsPage, we retrieve the dependencies using context.dependency<T>(). This function will recursively attempt to find the nearest dependency in the widget tree that matches the requested type. As a result, each route can access its own dependencies, as well as those provided at the root level, like AuthUseCase.

To wrap up, ServiceWaiter presents a clean and efficient mechanism to handle dependencies in a Flutter application. This SwiftUI-inspired solution helps to keep your codebase organized and efficient, proving that the beauty of dependency injection isn't restricted to just one platform.