[Разработка мобильных приложений, Flutter] Внедрение зависимостей (Dependency Injection) с GetIt на примере Flutter-проекта
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Внедрение зависимостей - DI - Dependency injection - термин часто встречающийся на собеседованиях. Сам по себе концепт опирается на более объемный принцип инверсии зависимостей (буква D в SOLID), но намного проще и ближе к практике. Кратко можно сказать, что при внедрении зависимостей, мы задаем значения переменных объекта в момент выполнения программы, а не в момент компиляции.В этой статье я постараюсь показать, что использование специальных библиотек для DI - это легко и удобно, даже для небольших проектов и опишу три случая с кодом ДО и ПОСЛЕ. Надеюсь, даже в небольшом проекте сразу станет понятно, что код после применения внедрения зависимостей стал чуть-чуть лучше. Часто программисты не понимают, для чего им в их небольших проектах, которые далеки от тысяч файлов корпоративных громад, нужно внедрение зависимостей. В таких проектах не описываются интерфейсы, используются одни и те же классы, экземпляры которых можно передать всем, кому это необходимо. На самый крайний случай, используются синглтоны для получения единственного экземпляра класса во всем приложении.Работать мы будем с достаточно популярной библиотекой GetIt. Проект минималистичен: приложение показывает погоду в настоящий момент с использованием одного из двух сервисов: Yandex.Weather или VisualCrossing. Если пользователь разрешит, то учитывается его местоположение и погода будет актуальна для его города.Пример 1. Общий класс настроек. В нашем случае настройки приложения, а именно - какой сервис для получения погодных данных был выбран пользователем - хранятся в стандартном платформенно-зависимом хранилище - SharedPreferences. При запуске приложения запускается тот сервис, который был выбран ранее. Если ничего не выбрано, запускается сервис по-умолчанию. До внедрения DI класс настроек создавался в 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;
...
После внедрения DI класс настроек регистрируется в 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>();
...
Аналогично может быть зарегистрирована служба логирования, служба обработки ошибок, клиент http и т.д. Нет нужды упоминать экземпляр класса в конструкторе, а значит меньше параметров, которые необходимо задать при инициализации.Пример 2. Использование одного из поставщиков данных. В приложении описан общий интерфейс получения погодных данных, тем не менее каждый раз определять, к какому конкретно классу надо обратиться для получения данных, - неудобно. Здесь тоже поможет DI. Вместо того, чтобы создавать экземпляр класса в виджете, который ответственен за показ погоды и пересоздавать виджет или экземпляр класса, в коде используется геттер, который получает всегда актуальный класс-поставщик погодных данных через DI. Было:
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 position = await Geolocator.getLastKnownPosition();
final predictions = await weatherProvider.loadPredictions(position?.latitude ?? 0.0, position?.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 недоступны, да и предсказать, что ответит бэкенд невозможно. С DI тест написать просто - создаем 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);
});
}
В наших проектах в Россельхозбанке мы применяем Dependency Injection в основном для тестирования и упрощения кода аналогично первому и третьему примеру. Бывают и ситуации, когда используются разные классы, реализующие одинаковый интерфейс, в зависимости от того, какой билд был создан. Например, для debug билда все логирование происходит в консоли, а для release билда используется другой класс логирования, который отправляет информацию о критических ошибках на бэкенд.Подведу итоги. В применении Dependency Injection есть неоспоримые преимущества:
- объекты не зависят от конкретной реализации классов, которые им необходимы,
- нет нужды менять код объекта, если зависимый класс поменял что-то в своей реализации или был заменен другим,
- реализация принципа «Зависимость от интерфейса, а не от реализации»,
- возможность использования mock-объектов при тестировании.
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка мобильных приложений] Как потратить 2 млн. на разработку и получить приложение, а не невроз
- [Dart, Flutter] gRPC + Dart, Сервис + Клиент, напишем? Часть 2
- [Анализ и проектирование систем, Проектирование и рефакторинг, Хранение данных, Прототипирование] Что нам стоит дом построить? (часть 2)
- [Разработка мобильных приложений, Разработка под Android, Повышение конверсии] Персонализация инвайтов в приложении с использованием AppsFlyer
- [Тестирование IT-систем] 5 причин, которые заставят тебя использовать Kibana
- [Dart, Flutter] gRPC + Dart, Сервис + Клиент, напишем
- [Разработка под iOS, Разработка мобильных приложений, Тестирование мобильных приложений] За что App Store может отклонить приложение: чек-лист
- [] Готовим дома
- [Разработка веб-сайтов, Работа с видео, DevOps, Видеоконференцсвязь] Google Cloud Platform for WebRTC CDN with Balancing and Autoscaling
- [Разработка веб-сайтов, Работа с видео, DevOps, Видеоконференцсвязь] WebRTC CDN на Google Cloud Platform с балансировкой и автоматическим масштабированием
Теги для поиска: #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_flutter, #_dependency_injection, #_flutter, #_testing, #_mockito, #_blog_kompanii_rosselhozbank (
Блог компании Россельхозбанк
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_flutter
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:30
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Внедрение зависимостей - DI - Dependency injection - термин часто встречающийся на собеседованиях. Сам по себе концепт опирается на более объемный принцип инверсии зависимостей (буква D в SOLID), но намного проще и ближе к практике. Кратко можно сказать, что при внедрении зависимостей, мы задаем значения переменных объекта в момент выполнения программы, а не в момент компиляции.В этой статье я постараюсь показать, что использование специальных библиотек для DI - это легко и удобно, даже для небольших проектов и опишу три случая с кодом ДО и ПОСЛЕ. Надеюсь, даже в небольшом проекте сразу станет понятно, что код после применения внедрения зависимостей стал чуть-чуть лучше. Часто программисты не понимают, для чего им в их небольших проектах, которые далеки от тысяч файлов корпоративных громад, нужно внедрение зависимостей. В таких проектах не описываются интерфейсы, используются одни и те же классы, экземпляры которых можно передать всем, кому это необходимо. На самый крайний случай, используются синглтоны для получения единственного экземпляра класса во всем приложении.Работать мы будем с достаточно популярной библиотекой GetIt. Проект минималистичен: приложение показывает погоду в настоящий момент с использованием одного из двух сервисов: Yandex.Weather или VisualCrossing. Если пользователь разрешит, то учитывается его местоположение и погода будет актуальна для его города.Пример 1. Общий класс настроек. В нашем случае настройки приложения, а именно - какой сервис для получения погодных данных был выбран пользователем - хранятся в стандартном платформенно-зависимом хранилище - SharedPreferences. При запуске приложения запускается тот сервис, который был выбран ранее. Если ничего не выбрано, запускается сервис по-умолчанию. До внедрения DI класс настроек создавался в 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; ... 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>(); ... 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 position = await Geolocator.getLastKnownPosition(); final predictions = await weatherProvider.loadPredictions(position?.latitude ?? 0.0, position?.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(() {}); } 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); }); }
=========== Источник: habr.com =========== Похожие новости:
Блог компании Россельхозбанк ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_flutter |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:30
Часовой пояс: UTC + 5