В этом параграфе мы познакомимся с пакетом GetIt — мощным инструментом для внедрения зависимостей в Flutter-приложениях, который реализует паттерн Service Locator.
Вы узнаете, как настроить GetIt для управления зависимостями, разберёте основные методы регистрации и получения объектов, освоите работу со скоупами для структурирования сложных приложений и научитесь использовать подмену зависимостей для эффективного тестирования.
К концу параграфа вы сможете уверенно применять GetIt для упрощения архитектуры и повышения тестируемости ваших проектов.
Приступим!
Базовый сценарий использования
GetIt может выступать в роли глобального сервис-локатора, предоставляя доступ к зависимостям из любой части приложения.
Настройка и регистрация зависимости
Сначала создадим глобальный экземпляр GetIt в файле конфигурации (например, di.dart) или в main.dart, который будет доступен во всём приложении:
1import 'package:get_it/get_it.dart';
2
3final getIt = GetIt.instance;
Теперь зарегистрируем зависимость, например класс Api, отвечающий за сетевые запросы. Используем метод registerSingleton для создания единственного экземпляра:
1class Api {
2 Future<ApiData> fetchData() {
3 // Логика для получения данных
4 }
5}
6
7void setup() {
8 getIt.registerSingleton<Api>(Api());
9}
Здесь registerSingleton добавляет экземпляр Api как единственный экземпляр, доступный для всего приложения. Метод setup вызывается в начале работы приложения, обычно в функции main:
1void main() {
2 setup();
3 runApp(MyApp());
4}
Получение зависимости
После регистрации можно получить Api в любом месте кода с помощью метода get:
1void someFunction() {
2 final api = getIt<Api>();
3 api.fetchData();
4}
Вызов getIt<Api>() возвращает зарегистрированный экземпляр Api, что позволяет переиспользовать его в приложении.
Методы регистрации зависимостей
GetIt предоставляет несколько способов регистрации зависимостей, каждый из которых подходит для разных сценариев (registerSingleton, registerLazySingleton, registerFactory), и асинхронные варианты методов.
registerSingleton
registerSingleton создаёт и регистрирует единственный экземпляр класса, который используется во всём приложении. Это подходит для объектов, которые должны существовать в единственном экземпляре.
Пример:
1getIt.registerSingleton<ApiService>(ApiService());
Экземпляр ApiService создаётся сразу при регистрации и остаётся в памяти до завершения работы приложения.
Когда использовать: для объектов, которые нужно использовать в единственном экземпляре. Например, для работы с API или кешем приложения, хранящего данные, загруженные из сети.
registerLazySingleton
registerLazySingleton регистрирует синглтон, но создаёт его только при первом обращении. Это оптимизирует использование ресурсов, откладывая создание объекта.
Пример:
1getIt.registerLazySingleton<ApiService>(() => ApiService());
В данном случае ApiService будет создан в момент первого обращения к нему через вызов getIt<ApiService>().
Когда использовать: для зависимостей, которые могут не понадобиться сразу или требуют значительных ресурсов для инициализации. Например, для сервиса геолокации, который инициализируется только при открытии определённого экрана.
registerFactory
registerFactory создаёт новый экземпляр класса при каждом обращении. Это подходит для легковесных объектов, не сохраняющих состояние.
Пример:
1getIt.registerFactory<TempService>(() => TempService());
Каждый вызов getIt<TempService>() будет возвращать новый экземпляр TempService.
Когда использовать: когда требуется новый объект при каждом вызове. Например, для легковесных зависимостей без состояния, таких как объект для форматирования дат или временных вычислений. Фабрика эффективно управляет памятью, так как каждый экземпляр существует только в момент использования и затем может быть удалён сборщиком мусора. При следующем вызове создаётся новый экземпляр, что минимизирует потребление ресурсов.
Регистрация асинхронных зависимостей
Для зависимостей, требующих асинхронной инициализации, используйте метод registerSingletonAsync или registerLazySingletonAsync. Такой зависимостью может быть сервис для работы с базой данных, который загружает метаданные перед использованием.
Пример:
1getIt.registerSingletonAsync<ApiService>(() async {
2 final service = ApiService();
3 await service.init();
4 return service;
5});
Чтобы проверить, готова ли асинхронная зависимость, используйте метод isReady:
1Future<void> checkReady() async {
2 final bool isReady = await getIt.isReady<ApiService>();
3 if (isReady) {
4 final ApiService apiService = getIt<ApiService>();
5 apiService.fetchData();
6 }
7}
Когда использовать: для сервисов, которые требуют асинхронной инициализации, например подключения к базе данных или API.
После того как зависимости зарегистрированы в GetIt, их можно получить различными способами. Рассмотрим их.
Методы получения зависимостей
В GetIt существуют два основных метода для доступа к зависимостям: get и getAsync. Как следует из названий, метод get возвращает зависимость синхронно, а getAsync — асинхронно.
get
Пример:
1void someFunction() {
2 final apiService = getIt<ApiService>();
3 apiService.fetchData();
4}
Здесь getIt<ApiService>() возвращает экземпляр ApiService, зарегистрированный ранее. Этот метод предполагает, что экземпляр уже существует и не требует асинхронных операций для своей инициализации.
Когда использовать: для созданных через registerSingleton или registerLazySingleton зависимостей, которые не требуют асинхронной инициализации.
getAsync
Пример:
1Future<void> fetchDataAsync() async {
2 final apiService = await getIt.getAsync<ApiService>();
3 await apiService.fetchData();
4}
В этом случае getAsync возвращает зависимость, после того как она была инициализирована. Это может быть полезно при работе с объектами, которые загружают данные из внешних источников.
Когда использовать: для зависимостей, зарегистрированных через registerSingletonAsync или registerLazySingletonAsync.
Работа с множеством зависимостей одного типа
Иногда требуется зарегистрировать несколько экземпляров одного типа. Например, разные реализации интерфейса. Для этого используйте параметр instanceName и методы getAll или getAllAsync.
Для регистрации нескольких экземпляров используйте параметр instanceName:
1getIt.registerSingleton<ApiService>(ApiService(apiVersion: 'v1'));
2getIt.registerSingleton<ApiService>(ApiService(apiVersion: 'v2'), instanceName: 'v2');
В данном примере два экземпляра ApiService зарегистрированы с разными параметрами (разные версии API), и второй экземпляр зарегистрирован с помощью instanceName, чтобы отличать его от первого.
getAll
Метод getAll возвращает список всех зарегистрированных экземпляров одного типа.
Пример:
1List<ApiService> apiServices = getIt.getAll<ApiService>();
2for (var service in apiServices) {
3 service.fetchData();
4}
Здесь getAll<ApiService>() возвращает список всех зарегистрированных экземпляров ApiService, и мы можем использовать каждый из них по очереди.
Когда использовать: для работы с несколькими реализациями одного типа одновременно.
getAllAsync
getAllAsync возвращает асинхронный список экземпляров, если они требуют инициализации.
Пример:
1Future<void> fetchAllData() async {
2 List<ApiService> apiServices = await getIt.getAllAsync<ApiService>();
3 for (var service in apiServices) {
4 await service.fetchData();
5 }
6}
В этом примере getAllAsync<ApiService>() возвращает список всех асинхронно инициализированных экземпляров ApiService, и мы можем вызывать их методы асинхронно.
Когда использовать: для асинхронных зависимостей, зарегистрированных с помощью registerSingletonAsync или registerLazySingletonAsync.
Построение структуры вложенных DI-контейнеров (Scopes)
Для сложных приложений GetIt предлагает механизм областей (скоупов), который позволяет создавать вложенные DI-контейнеры. Скоупы упрощают управление зависимостями в локальных контекстах, таких как экраны или модули.
Зачем нужны скоупы
Скоупы позволяют:
- Управлять жизненным циклом зависимостей, автоматически очищая их при закрытии скоупа.
- Изолировать зависимости, делая их доступными только в нужной области.
- Упростить работу с временными зависимостями, снижая потребность в
getAll.
Примеры кода для работы со скоупами
Создание скоупа для экрана
Представим, что у нас есть UserSessionService, который нужен только для экрана профиля. Этот сервис не должен существовать за пределами экрана профиля, поэтому мы создаём скоуп и регистрируем его внутри:
Код
1void openProfileScreen() {
2 getIt.pushNewScope(); // Создаём новый скоуп перед переходом на экран профиля
3
4 getIt.registerSingleton<UserSessionService>(UserSessionService());
5
6 // Переходим на экран профиля, где будем использовать UserSessionService
7 Navigator.push(
8 context,
9 MaterialPageRoute(builder: (context) => ProfileScreen()),
10 ).then((_) {
11 getIt.popScope(); // Удаляем скоуп после закрытия экрана профиля
12 });
13}
Получение зависимости в скоупе
На экране профиля теперь можно легко получить UserSessionService:
1class ProfileScreen extends StatelessWidget {
2 @override
3 Widget build(BuildContext context) {
4 final userSessionService = getIt<UserSessionService>();
5 userSessionService.startSession();
6
7 return Scaffold(
8 appBar: AppBar(title: Text("Profile")),
9 body: Center(child: Text("Profile Screen")),
10 );
11 }
12}
Вложенные скоупы
Если у приложения несколько уровней иерархии (например, главный экран, экран профиля и экран настроек), каждый из них может иметь свои скоупы. Например, для экрана настроек:
Код
1void openSettingsScreen() {
2 getIt.pushNewScope(); // Новый скоуп для экрана настроек
3
4 getIt.registerSingleton<SettingsService>(SettingsService());
5
6 Navigator.push(
7 context,
8 MaterialPageRoute(builder: (context) => SettingsScreen()),
9 ).then((_) {
10 getIt.popScope(); // Удаляем скоуп после закрытия экрана настроек
11 });
12}
13
Очистка и управление зависимостями
Благодаря скоупам вы не только избегаете излишних вызовов getAll, но и легко управляете зависимостями. Например, если UserSessionService или SettingsService требует ресурсов (вроде сетевых подключений), скоупы позволяют автоматизировать их удаление по завершении использования.
Как видите, скоупы в GetIt — мощный инструмент, позволяющий эффективно управлять жизненным циклом временных зависимостей и структурировать приложение на логические части.
Они обеспечивают большую гибкость по сравнению с глобальными регистрациями, позволяя организовать DI-контейнеры, которые работают только на необходимом уровне иерархии приложения.
Разберём ещё одну важную тему: как тестировать компоненты приложения с зависимостями.
Тестирование с использованием подмены зависимостей
GetIt упрощает тестирование, позволяя подменять зависимости на мок-объекты. Метод reset очищает контейнер, обеспечивая изоляцию тестов.
Инициализация контейнера перед тестами
Перед запуском тестов нужно убедиться, что GetIt не содержит зависимостей из других тестов или из основного приложения. Это достигается с помощью вызова getIt.reset().
1setUp(() {
2 getIt.reset(); // Сброс всех зарегистрированных зависимостей
3});
Замена зависимости на мок-объект
Допустим, у нас есть класс ApiService, который взаимодействует с сетью. Вместо него мы создаём тестовый мок-объект MockApiService, чтобы проверить поведение кода без выполнения реальных запросов.
Код
1class MockApiService extends Mock implements ApiService {}
2
3void main() {
4 setUp(() {
5 getIt.reset();
6 getIt.registerSingleton<ApiService>(MockApiService());
7 });
8
9 test('Fetch data from MockApiService', () {
10 final apiService = getIt<ApiService>();
11
12 // Определяем поведение метода fetchData
13 when(apiService.fetchData).thenReturn(Future.value("Mock data"));
14
15 // Проверка
16 expect(apiService.fetchData(), completion(equals("Mock data")));
17 });
18}
Использование registerFactory для замены временных зависимостей
Для тестирования временных зависимостей, которые создаются каждый раз заново, удобно использовать registerFactory. Например, для проверки работы с классом TempService, который требует новый экземпляр для каждого вызова:
1class MockTempService extends Mock implements TempService {}
2
3setUp(() {
4 getIt.reset();
5 getIt.registerFactory<TempService>(() => MockTempService());
6});
Теперь каждый вызов getIt<TempService>() в тесте будет возвращать новый экземпляр MockTempService, не влияя на поведение других тестов.
Очистка контейнера после теста
После завершения каждого теста рекомендуется очищать контейнер, чтобы последующие тесты не зависели от текущих настроек и были изолированными.
1tearDown(() {
2 getIt.reset(); // Сброс контейнера после теста
3});
Примеры
Рассмотрим пример теста для компонента, который использует ApiService для загрузки данных:
Код
1class DataFetcher {
2 final ApiService apiService;
3
4 DataFetcher(this.apiService);
5
6 Future<String> fetchData() async {
7 return await apiService.fetchData();
8 }
9}
10
11void main() {
12 setUp(() {
13 getIt.reset();
14 getIt.registerSingleton<ApiService>(MockApiService());
15 });
16
17 test('DataFetcher fetches data successfully', () async {
18 final mockApiService = getIt<ApiService>() as MockApiService;
19 final dataFetcher = DataFetcher(mockApiService);
20
21 when(mockApiService.fetchData).thenReturn(Future.value("Mock data"));
22
23 final result = await dataFetcher.fetchData();
24 expect(result, "Mock data");
25 });
26
27 tearDown(() {
28 getIt.reset();
29 });
30}
В этом тесте:
- Мы заменили
ApiServiceнаMockApiService. - Настроили метод
fetchDataдля возвращения тестового значения. - Проверили, что
DataFetcherкорректно работает с данным значением.
Поддержка подмены зависимостей в GetIt делает DI-контейнер отличным выбором для тестирования, позволяя легко заменять реальные сервисы на мок-версии. Это снижает вероятность побочных эффектов и упрощает тестирование логики без реальных данных или сетевых вызовов.
Преимущества и недостатки GetIt
GetIt — мощный инструмент, обладающий рядом преимуществ и некоторыми ограничениями, для внедрения зависимостей в Flutter-приложениях.
Преимущества
- Простота. Интуитивный интерфейс для регистрации и получения зависимостей.
- Гибкость. Поддержка различных способов регистрации и скоупов.
- Удобство тестирования. Лёгкая подмена зависимостей на мок-объекты.
Недостатки
- Сложность в крупных проектах. Множество зависимостей и скоупов могут усложнить архитектуру.
- Ошибки в рантайме. Регистрация и доступ к зависимостям происходят во время выполнения, что может привести к ошибкам, если зависимости запрашиваются до их регистрации или в неправильном скоупе. Это требует строгого контроля порядка вызовов и усложняет отладку.
- Ограниченная проверка на этапе компиляции. В отличие от некоторых других DI-фреймворков,
GetItне предоставляет строгой типизации или проверок на этапе компиляции, что увеличивает риск ошибок.
Вот и всё!
В этом параграфе мы рассмотрели основы работы с пакетом GetIt для внедрения зависимостей в Flutter-приложениях.
Вы узнали, как настроить и использовать GetIt в качестве сервис-локатора, изучили различные методы регистрации и получения зависимостей, включая синглтоны, ленивые синглтоны, фабрики и асинхронные зависимости.
Мы также разобрали, как использовать скоупы для управления жизненным циклом зависимостей в сложных приложениях и как GetIt упрощает тестирование через подмену зависимостей на мок-объекты. Теперь вы можете применять GetIt для структурирования кода, оптимизации ресурсов и упрощения тестирования в своих проектах.
А в следующем параграфе поговорим про Injectable — генератор кода для GetIt.
