Что знаешь про Dependency Injection (DI) в Dart/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), которые по сути являются контейнерами зависимостей для поддерева виджетов.

Ответ 18+ 🔞

Давай разберём эту тему про внедрение зависимостей, а то некоторые её так преподносят, будто это какая-то магия космического уровня, а на деле всё просто, как три копейки.

Смотри, Dependency Injection — это, по сути, просто способ не быть распиздяем. Вместо того чтобы твой класс сам внутри себя как угорелый создавал всё, что ему нужно (типа final client = ApiClient()), он говорит: «Эй, дайте мне это сюда, я работать буду». И ему это дают извне. Всё. Никакой ебушки-воробушки.

Как это обычно выглядит в Dart/Flutter:

  1. Внедрение через конструктор (Constructor Injection). Самый частый и нормальный способ. Выглядит честно и прямолинейно.

    class AuthService {
      final ApiClient _client; // Зависимость №1
      final StorageRepository _storage; // Зависимость №2
    
      // Сюда, сука, принесите мне то, что я прошу. Сам я ничего искать не буду.
      AuthService(this._client, this._storage);
    
      Future<void> login() async {
        final token = await _client.post('/login');
        await _storage.saveToken(token);
      }
    }

    Прелесть в чём? А в том, что теперь AuthService — хитрая жопа. Он не парится, откуда взялись ApiClient и StorageRepository. Ему дали — он и работает. А в тестах ты ему вместо настоящего клиента суёшь заглушку, и он даже не заметит подмены. Доверия к нему, блядь, ноль, но это и хорошо.

  2. Использование пакетов. Когда проект растёт, вручную всё это собирать становится заебище. Тут на помощь приходят get_it и injectable. Это как большой шкаф, где всё разложено по полочкам.

    // Настраиваем эту самую полку (locator)
    final getIt = GetIt.instance;
    
    @injectable // Говорим системе: "Эй, этот класс можешь создавать сам"
    class ApiClient { ... }
    
    @injectable
    class AuthService {
      AuthService(this.apiClient); // Система видит, что нужен ApiClient, идёт на полку, берёт его и суёт сюда
      final ApiClient apiClient;
    }
    
    // А где-то в коде ты просто берёшь готовый сервис:
    final authService = getIt<AuthService>(); // Все зависимости уже внутри, как матрёшка!

    Сначала кажется, что это овердохуища магии, но потом привыкаешь. Главное — правильно полки настроить.

А нахуя это всё, спросишь?

  • Тестируемость — пизда рулю. Это главный козырь. Хочешь потестить AuthService? Подсовываешь ему мок ApiClient, который не лезет в сеть, а тупо возвращает фейковый токен. И всё. Никаких танцев с бубном.
  • Гибкость. Решил, что HiveStorageRepository — говно, и хочешь перейти на Isar? Идешь в одно-единственное место (в тот самый «шкаф» или провайдер) и меняешь там настройку. А не бегаешь по всему коду, выискивая, где ты этот репозиторий создавал. Экономия нервов — просто волнение, ебать.
  • Чистота. Классы перестают быть всезнайками. Они знают только свою работу, а не то, как рождаются их помощники. Это и есть слабая связность. Красота.

Во Flutter, кстати, виджеты-провайдеры (Provider, Riverpod) — это по своей сути те же контейнеры для зависимостей, только заточенные под виджетное дерево. Кинул провайдер сверху — и все виджеты ниже могут достучаться до нужного объекта. Удобно, ёпта.

В общем, суть не в том, чтобы выучить кучу терминов, а в том, чтобы перестать создавать зависимости внутри класса, как последний маньяк. Отдай эту работу наружу — и жить станет проще. Сам от этого подхода охуел, когда осознал, насколько всё логичнее становится.