Внедрение зависимостей (Dependency Injection) с GetIt во Flutter

от автора

Внедрение зависимостей — DI — Dependency injection — термин часто встречающийся на собеседованиях. Сам по себе концепт опирается на более объемный принцип инверсии зависимостей (буква D в SOLID), но намного проще и ближе к практике. Кратко можно сказать, что при внедрении зависимостей, мы задаем значения переменных объекта в момент выполнения программы, а не в момент компиляции.

В этой статье я постараюсь показать, что использование специальных библиотек для DI  — это легко и удобно, даже для небольших проектов и опишу три случая с кодом ДО и ПОСЛЕ. Надеюсь, даже в небольшом проекте сразу станет понятно, что код после применения внедрения зависимостей стал чуть-чуть лучше. 

Часто программисты не понимают, для чего им в их небольших проектах, которые далеки от тысяч файлов корпоративных громад, нужно внедрение зависимостей. В таких проектах не описываются интерфейсы, используются одни и те же классы, экземпляры которых можно передать всем, кому это необходимо. На самый крайний случай, используются синглтоны для получения единственного экземпляра класса во всем приложении.

Работать мы будем с достаточно популярной библиотекой GetIt. Проект минималистичен: приложение показывает погоду в настоящий момент  с использованием одного из двух сервисов: Yandex.Weather или VisualCrossing. Если пользователь разрешит, то учитывается его местоположение и погода будет актуальна для его города.

Пример 1. Переход от внедрения переменных через конструктор и создания их в коде к использованию GetIt. В нашем случае настройки приложения, а именно — какой сервис для получения погодных данных был выбран пользователем — хранятся в стандартном платформенно-зависимом хранилище — SharedPreferences. При запуске приложения запускается тот сервис, который был выбран ранее. Если ничего не выбрано, запускается сервис по-умолчанию. 

До внедрения GetIt класс настроек создавался в main и передавался в остальные классы через конструктор:

Future<void> main() async {     WidgetsFlutterBinding.ensureInitialized();     final settings = SettingsRepository();     await settings.loaded;     runApp(MyApp(settingsRepository: settings)); }  class MyApp extends StatelessWidget {   const MyApp({Key? key, required this.settingsRepository}) : super(key: key);    final SettingsRepository settingsRepository;    @override   Widget build(BuildContext context) {     return MaterialApp(         home: HomePage(settingsRepository: settingsRepository),  ...       );   } }         class HomePage extends StatefulWidget {   const HomePage({Key? key, required this.settingsRepository}) : super(key: key);    final SettingsRepository settingsRepository; ...

После внедрения GetIt класс настроек регистрируется в нем и затем его получают только там, где он действительно нужен, промежуточные классы не загромождаются лишними переменными.

Future<void> main() async {   WidgetsFlutterBinding.ensureInitialized();   final settings = SettingsRepository();   await settings.loaded;   GetIt.instance.registerSingleton(settings);   runApp(const MyApp()); }  class HomePage extends StatefulWidget {   const HomePage({Key? key}) : super(key: key);    @override   _HomePageState createState() => _HomePageState(); }  class _HomePageState extends State<HomePage> {   SettingsRepository get settingsRepository => GetIt.instance.get<SettingsRepository>(); ...

И переход на внедрение зависимостей вместо создания переменной: получение позиции пользователя. Для этой операции в _HomePageState используется Geolocator. В дальнейшем при юнит-тестировании и виджет-тестировании мы не сможем обращаться к этой библиотеке, поэтому нужно заменить способ получения позиции пользователя на DI. Было:

Future<void> _loadWeather() async {     final knownPosition = await Geolocator.getLastKnownPosition();     ...   }

Стало:

//в main.dart Future<void> main() async {   ... 	final position = await geolocator.getLastKnownPosition();   if (position != null) {     GetIt.instance.registerSingleton<Position>(position);      //позицию можно было бы получать и из настроек пользователя, если он не согласен давать доступ к GPS   }   ...      //в home_page.dart  Future<void> _loadWeather() async {     final knownPosition = GetIt.instance.isRegistered<Position>() ? GetIt.instance.get<Position>() : null;     ...   }

Аналогично может быть зарегистрирована служба логирования, служба обработки ошибок, клиент http и т.д. Нет нужды упоминать экземпляр класса в конструкторе, а значит меньше параметров, которые необходимо задать при инициализации.

Пример 2. Использование одного из поставщиков данных. В приложении описан общий интерфейс получения погодных данных, тем не менее каждый раз определять, к какому конкретно классу надо обратиться для получения данных, — неудобно. Здесь тоже поможет GetIt. Вместо того, чтобы создавать экземпляр класса в виджете, который ответственен за показ погоды и пересоздавать виджет или экземпляр класса, в коде используется геттер, который получает всегда актуальный класс-поставщик погодных данных через GetIt. 

Было:

Future<void> _loadWeather() async {     final position = await Geolocator.getLastKnownPosition();     final Map<DateTime, WeatherCondition> predictions;     if (widget.settingsRepository.remoteServerName == YandexWeatherProvider.providerName) {       predictions = await YandexWeatherProvider().loadPredictions(position?.latitude ?? 0.0, position?.longitude ?? 0.0);     } else {       predictions = await VisualCrossingWeatherProvider().loadPredictions(position?.latitude ?? 0.0, position?.longitude ?? 0.0);     }     currentWeather = predictions[DateTime.now().hourStart];     setState(() {});   }

Стало:

// в main.dart Future<void> main() async {   ...   GetIt.instance.registerSingleton<WeatherProvider>(settings.remoteServerName == YandexWeatherProvider.providerName ? YandexWeatherProvider() : VisualCrossingWeatherProvider());   ... }  ... // при загрузке погоды WeatherProvider get weatherProvider => GetIt.instance.get<WeatherProvider>();  Future<void> _loadWeather() async {     final knownPosition = GetIt.instance.isRegistered<Position>() ? GetIt.instance.get<Position>() : null;     final predictions = await weatherProvider.loadPredictions(knownPosition?.latitude ?? 0.0, knownPosition?.longitude ?? 0.0);     currentWeather = predictions![DateTime.now().hourStart];     setState(() {});   }  ... // при переключении настроек пользователя void _changeServer(String? value) {     if (value == null) return;     settingsRepository.remoteServerName = value;     GetIt.instance.unregister<WeatherProvider>();     GetIt.instance.registerSingleton<WeatherProvider>(value == YandexWeatherProvider.providerName ? YandexWeatherProvider() : VisualCrossingWeatherProvider());     setState(() {});   }

Аналогично можно регистрировать онлайн/оффлайн поставщики данных, любые взаимозаменяемые модули.

Пример 3. Использование mock-объектов для тестирования. А теперь нам нужно написать виджет-тесты. Если бы проект был чуть более сложным и включал например BLoC для управления состоянием, то эти модули тоже нужно было бы тестировать с помощью юнит-тестов. И здесь мы сразу встречаемся с невозможностью сделать это без дополнительного изменения кода, потому что при юнит-тестировании и виджет-тестировании SharedPreferences и Geolocator недоступны, да и предсказать, что ответит бэкенд невозможно. С GetIt тест написать просто — создаем mock-класс для настроек или для поставщика данных, моделируем ответы бэкенда, отображение которых будет легко проверить, через Mockito и можно быть уверенным в корректности работы приложения.

class MockProvider extends Mock implements WeatherProvider {}  void main() {   final repository = MockProvider();    setUpAll(() {     final service = GetIt.instance;     service.registerSingleton<WeatherProvider>(repository);   });    testWidgets('отображение данных о погоде', (WidgetTester tester) async {     when(repository.loadPredictions(any, any)).thenAnswer((_) async {       return {DateTime.now().hourStart: WeatherCondition(windDirection: WindDirection.north, temperature: 10.0, windSpeed: 3.0, windGust: 8.0)};     });      await tester.pumpWidget(const MaterialApp(       home: HomePage(),     ));     await tester.pumpAndSettle();      final titleText = find.text('Current weather:');     expect(titleText, findsOneWidget);     final weather = find.text('Ветер: 3.0 N, порывами 8.0, температура: 10.0');     expect(weather, findsOneWidget);   }); }

В наших проектах в Россельхозбанке мы применяем GetIt в основном для тестирования и упрощения кода аналогично первому и третьему примеру. Бывают и ситуации, когда используются разные классы, реализующие одинаковый интерфейс, в зависимости от того, какой билд был создан. Например, для debug билда все логирование происходит в консоли, а для release билда используется другой класс логирования, который отправляет информацию о критических ошибках на бэкенд.

Подведу итоги. В применении библиотек для Dependency Injection есть неоспоримые преимущества: 

  • меньше параметров в классах, 

  • нет нужды менять код объекта, если зависимый класс поменял что-то в своей реализации или был заменен другим, 

  • возможность использования mock-объектов при тестировании.

ссылка на оригинал статьи https://habr.com/ru/company/rshb/blog/564158/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *