Руководство по использованию пакета GetIt

Если вы снова читаете эту статью, чтобы освежить в памяти воспоминания, прокрутите вниз до раздела Сводка.

В предыдущей статье я говорил о простом способе создания архитектуры приложения Flutter с использованием пакета stacked package от FilledStacks. Основная идея заключалась в использовании шаблона MVVM для удаления любой логики из макета пользовательского интерфейса путем помещения ее в модель представления. Это делает приложение более чистым и удобным в обслуживании.

Однако было бы ошибкой, чтобы всю работу выполняли модели представления. Их основная задача - просто подготовить данные для отображения в представлении. Тяжелую работу нужно переложить на службы. Это дальнейшее разделение проблем, которое является преимуществом для поддерживаемых приложений.

В той последней статье я упомянул сервисы, но особо не говорил о том, как их создавать. В этой статье я более подробно расскажу об услугах и покажу вам, как использовать их в вашем собственном приложении Flutter. Этот раздел не зависит от MVVM, и вы можете использовать шаблон службы, даже если используете другой архитектурный стиль.

Что такое услуги?

Сервисы - это обычные классы Dart. Не думайте о них в смысле этого слова для Android, то есть как о длительных фоновых задачах. Это просто классы, которые вы пишете для выполнения некоторых специализированных задач в вашем приложении. Вам даже не нужно называть их сервисами. Я называю их так, потому что так их называет FilledStacks. Их можно было бы назвать помощниками или репозиториями, но сервисы - хорошее название, и оно соответствует идее микросервисов. Однако вместо того, чтобы распространяться по сети, они содержатся в вашем приложении.

Вот несколько распространенных примеров, которыми вы могли бы написать сервис для обработки:

  • Чтение и запись в локальное хранилище (база данных, общие настройки, файлы)
  • Доступ к веб-API
  • Авторизоваться как пользователь
  • Выполните тяжелый расчет
  • Оберните Firebase или какой-либо другой сторонний пакет

Цель услуги

Цель службы - изолировать какую-то задачу и скрыть детали ее реализации от остальной части приложения.

Подумайте, почему это важно.

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

Вы обнаруживаете, что вам нужно сохранить довольно много данных, поэтому вы решаете использовать базу данных SQL вместо общих настроек. Вы просматриваете и заменяете все ссылки на общие настройки кодом базы данных. БОЛЬ. Но ты забываешь одно место. ОШИБКА.

Позже сопровождающий этого пакета Pub базы данных прекращает его обслуживание. В любом случае есть более популярный пакет баз данных, поэтому вы переключаете пакеты и обновляете все ссылки в коде. БОЛЬ. И в одном месте вы испортили параметр. ОШИБКА.

Ваше приложение становится все более популярным, но ваши пользователи начинают запрашивать синхронизацию с несколькими устройствами. Итак, вы решили, что вместо того, чтобы сохранять данные в локальном хранилище, вы должны сохранять их на своем сервере. Вы вносите изменения. По всему вашему приложению. БОЛЬ. Вам не нужна эта боль, поэтому вы поручаете ее новому парню. ОШИБКА.

Популярность вашего приложения растет, но нагрузка на ваш сервер начинает влиять на производительность. Вы хотите перейти на облачное решение, но ваша серверная часть построена в том же архитектурном стиле, что и ваш интерфейс. Стоимость изменений слишком болезненна, и существует слишком много ошибок. Итак, ваша компания высыхает и умирает. Конец.

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

Здесь на помощь приходят сервисы. Вы создаете новый класс и называете его как-то вроде StorageService. Остальные классы в приложении не знают, как оно работает. Они просто вызывают методы службы для сохранения и извлечения данных.

Это упрощает внесение изменений. Вы хотите переключиться с общих настроек на базу данных? Без проблем. Просто измените код внутри класса обслуживания. То же самое для переключения сторонних пакетов баз данных или даже перехода на веб-API. Обновление кода службы автоматически влияет на везде, где служба используется внутри приложения.

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

Это позволяет легко менять реализации. Вы также можете создать «фальшивую» реализацию, которая просто возвращает некоторые жестко закодированные данные. Это позволит вам сосредоточиться на остальной части приложения и оставить кодирование сервиса на потом. Это также способ разделить работу, если есть команда, разрабатывающая приложение.

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

Пример

Я покажу вам, как создать настоящую услугу. После того, как вы проделаете это пару раз, это станет довольно легко. Я буду иметь в виду тот же пример приложения, который я показал вам в Руководстве для начинающих по созданию архитектуры Flutter-приложения. В этом приложении мы использовали архитектуру в стиле MVVM. И снова архитектура:

Модель представления будет использовать службу для получения некоторых данных, а затем уведомить своих слушателей (представление), чтобы пользовательский интерфейс обновился. Тот же принцип действует, если вы используете шаблон BLoC вместо моделей просмотра.

Вот шаги, которые вы должны выполнить при настройке службы:

1. Определите свою службу с помощью абстрактного класса

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

Создайте файл с именем storage_service.dart в папке /lib. Вставьте следующий код:

Поскольку данные приложения счетчика являются целыми числами, у нас есть два метода: один для сохранения int, а другой - для его получения. Это план того, что будет делать наша реальная реализация сервиса.

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

2. Реализуйте абстрактный класс обслуживания.

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

Поддельная реализация

Вот фальшивая реализация. На самом деле он ничего не сохраняет и возвращает фиктивные данные, когда вы их запрашиваете.

Создайте файл с именем storage_service_fake.dart. Вставьте следующий код:

Реализация общих настроек

Эта реализация будет сохранять и извлекать данные из общих настроек. Прочтите этот пост о деталях использования пакета shared_preferences.

Создайте новый файл с именем storage_service_shared_pref.dart. Вставьте следующий код:

Это сохранит значение счетчика в общих настройках, то есть в локальном хранилище на устройстве пользователя.

Реализация базы данных Sqflite

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

Эта реализация будет сохранять и извлекать данные из базы данных SQLite. Прочтите этот пост о деталях использования пакета sqflite.

Создайте новый файл с именем storage_service_database.dart. Вставьте следующий код:

Как я уже сказал, это было перебором.

Сетевая реализация

Эта реализация будет сохранять и получать данные с удаленного сервера. Мне не хотелось создавать удаленный сервер только для этого примера, поэтому следующий код не тестировался. Однако, если вы заинтересованы в создании HTTP-сервера с помощью Dart, прочтите эту вводную статью и посмотрите мою серию статей Создание сервера Dart с нуля.

Для выполнения HTTP-запросов в Dart можно использовать пакет http. Прочтите эту статью, чтобы узнать, как это сделать.

Создайте новый файл с именем storage_service_web.dart. Вставьте следующий код:

Несомненно, существует гораздо больше вариантов реализации абстрактного класса StorageService, который мы определили выше. Я надеюсь, что приведенные здесь примеры дали вам представление о том, как это делается.

3. Создайте локатор услуг.

Локатор сервисов - это центральное место для регистрации всех сервисов, которые вы используете в своем приложении. Используя его, вы можете получить доступ к своим услугам из любого места в вашем коде. Обычно это альтернатива внедрению зависимостей.

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

Однако мне очень нравятся локаторы сервисов, потому что они очень просты в использовании. Попытка выполнить внедрение зависимостей с помощью чего-то вроде ProxyProvider может сбить с толку. Я большой поклонник того, чтобы все было легко понять. Что касается антипаттерна, прочтите эту статью Мартина Фаулера, который оценивает это как жизнеспособный вариант. (Но чтобы сбалансировать это, вы также можете прочитать эту статью.) Что касается сложности тестирования, это не так. Через минуту я покажу вам, как протестировать класс, использующий локатор сервисов.

Самым популярным пакетом локатора услуг для Flutter является GetIt. Вы можете получить это, добавив зависимость к pubspec.yaml.

dependencies:
  get_it: ^6.0.0

Затем создайте новый файл с именем service_locator.dart в папке /lib. Вставьте следующий код:

Здесь вы создаете глобальную переменную getIt, к которой вы можете получить доступ из любого места в вашем приложении. Это локатор услуг. Вы регистрируете службы в методе ниже, который мы будем запускать при запуске приложения.

Обратите внимание, что мы регистрируем StorageService как ленивый синглтон. Он инициализируется только при первом использовании. Если вы хотите, чтобы он инициализировался при запуске приложения, используйте вместо этого registerSingleton(). Поскольку это синглтон, у вас всегда будет один и тот же экземпляр службы.

Также обратите внимание, что мы зарегистрировали StorageServiceFake как конкретную реализацию StorageService. Вот где проявляется красота использования абстрактного класса. Если мы хотим использовать одну из других реализаций, которые мы написали выше, все, что нам нужно сделать, это изменить эту единственную строку кода.

Я зарегистрировал здесь только одну службу (StorageService), но вы можете так же легко зарегистрировать несколько служб. Например, служба входа в систему или служба Firebase.

4. Инициализируйте локатор сервисов.

Вам необходимо зарегистрировать службы при запуске приложения, поэтому вы можете сделать это в main.dart.

Заменить стандартный

void main() => runApp(MyApp());

со следующим:

import 'service_locator.dart';
void main() {
  setupServiceLocator();
  runApp(MyApp());
}

Это зарегистрирует все ваши службы в GetIt до того, как будет построено дерево виджетов.

5. Получите свои услуги

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

Давайте воспользуемся примером класса модели представления из Руководство для начинающих по созданию приложения Flutter. Он назывался counter_viewmodel.dart. Замените его следующим кодом:

Все, что вам нужно сделать, чтобы найти службу, - это сообщить локатору службы тип службы: locator<StorageService>(). Этого достаточно, чтобы GetIt получил его. После этого ваша служба может делать свою работу. Обратите внимание, что в приведенном здесь коде нет ссылки на конкретную реализацию службы. Это внутренняя деталь, не имеющая значения.

6. Реализации службы обмена

Ваш пользовательский интерфейс должен подключаться к модели представления для загрузки данных при запуске. Если вы следовали примеру из Руководство для начинающих по созданию приложения Flutter, замените код пользовательского интерфейса в counter_screen.dart следующим кодом:

Запустите приложение, оно должно начинаться со значения 11.

Это потому, что вы используете фальшивую службу хранения, которая всегда дает значение 11. Увеличьте счетчик в несколько раз. Затем перезапустите приложение. Он по-прежнему имеет значение 11:

Это потому, что ваша поддельная служба хранения на самом деле ничего не сохранила.

Теперь откройте storage_service.dart и замените его следующим кодом:

Здесь вы заменили StorageServiceFake на StorageServiceSharedPreferences, чтобы использовать решение с общими настройками для хранения данных.

Теперь перезапустите приложение. Он начнется со счета 0. Увеличьте счетчик несколько раз. Затем остановите приложение и перезапустите его. Он запомнит старый счетчик, потому что теперь вы используете реализацию общих настроек StorageService.

И так же легко переключиться на любую другую конкретную реализацию, такую ​​как SQLite или Интернет.

7. Тестовые классы, которые используют ваш сервис (необязательно)

Это дополнительный раздел, если вам нужны тестовые классы, использующие ваши сервисы.

Давайте протестируем приведенный выше класс модели представления. В тесте будет использоваться пакет mockito, поэтому см. Эту статью, если вам нужна помощь в настройке.

В папке /test создайте файл с именем counter_viewmodel_test.dart. Вставьте следующий код:

Ключ к имитации локатора сервисов - установить allowReassignment в true в методе setUpAll(). После этого вы можете заменить сервис в модели представления на макет.

8. Смотрите полный код на GitHub.

На случай, если вы заблудились по дороге, вот полный проект на GitHub.

Резюме

Вот краткое изложение без всяких объяснений. Добавьте эту страницу в закладки и возвращайтесь сюда, когда вам понадобится что-то освежить.

Абстрактный класс обслуживания

Создайте файл с именем my_service.dart. В нем создайте абстрактный класс, который определяет функциональность вашего сервиса.

abstract class MyService {
  Future<int> getSomeValue();
  Future<void> doSomething(int value);
}

Внедрение конкретных услуг

Создайте файл с именем my_service_impl.dart. Расширьте свой класс MyService и реализуйте необходимые методы.

import 'my_service.dart';

class MyServiceImpl extends MyService {
  @override
  Future<int> getSomeValue() async {
    // do something
    return someValue;
  }

  @override
  Future<void> doSomething(int value) async {
    // do something
  }
}

Поиск сервисов

Добавьте пакет локатора сервисов GetIt в pubspec.yaml.

dependencies:
  get_it: ^6.0.0

Создайте файл с именем service_locator.dart. Зарегистрируйте реализацию вашего сервиса.

import 'package:get_it/get_it.dart';
import 'my_service_impl.dart';
import 'my_service.dart';
GetIt getIt = GetIt.instance;
setupServiceLocator() {
  getIt.registerLazySingleton<MyService>(() => MyServiceImpl());
}

Инициализировать локатор сервисов

Инициализируйте локатор служб перед запуском приложения в main.dart.

void main() {
  setupServiceLocator();
  runApp(MyApp());
}

Воспользуйтесь услугой

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

class MyClass {

  MyService _myService = locator<MyService>();

  ...
}

Затем вы можете использовать его в этом классе следующим образом:

  • _myService.getSomeValue()
  • _myService.doSomething(someValue)

Заключение

Службы - отличный инструмент для изоляции функциональности от остальной части вашего приложения, особенно от сторонних пакетов, которые являются нестабильными или которые вы, возможно, захотите изменить в будущем. Использование локатора сервисов, такого как GetIt, является удобным способом предоставления этих сервисов по всему вашему коду.

Спасибо Дейну Маккеру из FilledStacks за то, что впервые познакомил меня с GetIt и локаторами сервисов. Ознакомьтесь с его учебником здесь.