Что знаешь про Dependency Injection (DI) в Dart/Flutter?

«Что знаешь про Dependency Injection (DI) в Dart/Flutter?» — вопрос из категории DI, который задают на 29% собеседований Flutter Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Dependency Injection (DI) — это паттерн, который я активно использую для создания тестируемого и слабосвязанного кода. Вместо того чтобы класс сам создавал свои зависимости, он получает их извне.

Основные подходы в Dart/Flutter:

  1. Constructor Injection (наиболее частый в моём коде): Зависимости передаются через конструктор.

    class AuthService {
      final ApiClient _client;
      final StorageRepository _storage;
    
      AuthService(this._client, this._storage); // Зависимости инжектятся
    
      Future<void> login() async {
        final token = await _client.post('/login');
        await _storage.saveToken(token);
      }
    }
  2. Использование пакетов для DI: Для больших проектов я использую get_it в связке с injectable для автоматической генерации регистрации.

    // Настройка locator (get_it)
    final getIt = GetIt.instance;
    
    @injectable
    class ApiClient { ... }
    
    @injectable
    class AuthService {
      AuthService(this.apiClient);
      final ApiClient apiClient;
    }
    
    // Где-то в коде:
    final authService = getIt<AuthService>(); // Все зависимости разрешены автоматически

Преимущества, которые я получил на практике:

  • Тестируемость: Легко подменить ApiClient моком в unit-тестах.
  • Гибкость: Замена реализации (например, FirebaseStorageRepository на HiveStorageRepository) происходит в одном месте — контейнере DI.
  • Чистая архитектура: Классы перестают знать о способе создания своих зависимостей.

Во Flutter DI часто реализуется через виджеты провайдеры (Provider, Riverpod), которые по сути являются контейнерами зависимостей для поддерева виджетов.