Привет! Меня зовут Даниил, около трех лет я занимаюсь mobile-разработкой. В начале работы над коммерческими проектами с использованием Flutter и языка Dart мне приходилось тратить много часов на реализации методов обращения к REST API бэкенда проекта. Тогда я задумался, как можно оптимизировать написание кода сервисов, которые работают с удалённым сервером заказчика. Это позволило мне сократить трудозатраты и время на разработку почти в 10 раз, а клиенту — быстрее  получать готовый продукт.

В этой статье я рассмотрю целесообразность и практический опыт кодогенерации для клиентских приложений, написанных на Flutter, REST API с помощью таких библиотек, как openapi_generator и swagger_dart_code_generator

Практический смысл применения кодогенерации API

Существует несколько смыслов и полезных практик применения кодогенерации API на основе файла, описывающего архитектуру запросов (методы получения/отправки сообщений и схемы данных) к бэкенд-серверу. Обычно любой проект начинается с технических требований заказчика и проектирования архитектуры. Уже на данном этапе мы можем задуматься о том, как мы можем оптимизировать интеграцию бизнес-логики бэкенда приложения в его клиент.

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

Что предлагает подход проектирования архитектуры взаимодействия клиент-сервера и последующей генерации кода? 

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

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

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

Техническая реализация

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

Эта библиотека представляет собой реализацию SDK клиента openapi для генерации кода для приложения, которое разработано с применением языка программирования Dart. С помощью этой библиотеки можно создавать клиентские OpenAPI пакеты, написанные на Dart с помощью вашей спецификации OpenAPI (обычно yaml-файла) прямо внутри проекта, написанного с применением Dart/Flutter.

Для того чтобы воспользоваться данной библиотекой, вам нужно добавить зависимости к вашему проекту в pubspec.yaml.

В категорию dependencies необходимо добавить эти пакеты:

●	built_collection
●	openapi_generator_annotations

В категорию dev_dependencies необходимо добавить эти пакеты:

●	build_runner
●	openapi_generator
●	openapi_generator_cli

Далее нам необходимо добавить yaml-файл описания нашего API. Я поместил его в корневую папку проекта под именем petstore_api.yaml. И создал аннотационный файл в папке с моделями проекта, где определяются название и ссылки на yaml-файл и выходную папку, в которую будет помещены файлы генерации.

import 'package:openapi_generator_annotations/openapi_generator_annotations.dart';


/// This way we can add yaml files to generate API clients based on dio
@Openapi(
  additionalProperties: AdditionalProperties(pubName: 'petstore_api'),
  inputSpecFile: 'petstore_api.yaml',
  generatorName: Generator.dio,
  outputDirectory: 'api/petstore_api',
)
class PetStoreApi extends OpenapiGeneratorConfig {}

В качестве параметра генерации сети я выбрал Dio, так как он рекомендован создателями пакета.

После настройки проекта необходимо воспользоваться командой “flutter packages pub run build_runner build --delete-conflicting-outputs” для того чтобы отработал генератор. После выполнения данной команды в нашем репозитории проекта появиться новая папка api с содержанием пакета petstore_api. Чтобы далее её использовать в проекте, нам необходимо добавить зависимость в наш pubspec.yaml под категорией dependencies.

petstore_api:
    path: ./api/petstore_api

Для демонстрации дальнейшего возможного использования нашего API клиента я создаю сервис PetstoreNetworkService. В конструкторе я обращаюсь к PetstoreApi, чтобы получить рабочие объекты для обращения к API. Также на этом этапе можно добавить любые интерцепторы Dio. Я добавил Dio Logger.

Также ниже определил поля объектов API библиотеки:

  late final PetApi petApi;
  late final StoreApi storeApi;
  late final UserApi userApi;
PetstoreNetworkService() {
    // Getting a reference to the Generate API
    final generatedAPI = PetstoreApi();

    // Adding additional dio interceptors for data logging
    generatedAPI.dio.interceptors.addAll([
      dioLoggerInterceptor,
    ]);

    // Getting an instances to call the pet store API
    petApi = generatedAPI.getPetApi();
    storeApi = generatedAPI.getStoreApi();
    userApi = generatedAPI.getUserApi();
  }

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

Future<Pet?> getPetById(int id) async =>
      petApi.getPetById(petId: id).then((result) {
        return result.data;
      }).catchError((Object error) {
        // Do something on error
      });

Все модели данных были также сгенерированы, исходя из их описания в yaml-файле нашего Petstore API.

Swagger dart code generator

Пакет SwaggerDartCodeGenerator — это генератор кода, который ищет файлы *.swagger/*.yaml/*.json с описанием API клиента и создает Dart файлы на основе схем API. Генерация кода моделей в данной библиотеке построена на пакетах Chopper и JsonAnnotation, которые можно настроить под свои нужды.

Для того чтобы воспользоваться данной библиотекой, вам нужно добавить зависимости к вашему проекту в pubspec.yaml.

В категорию dependencies необходимо добавить пакет:

●	chopper: ^5.0.0

В категорию dev_dependencies необходимо добавить эти пакеты:

●	build_runner: ^2.1.10
●	  chopper_generator: 5.0.0+1
●	  json_annotation: ^4.4.0
●	  json_serializable: ^6.1.4
●	  swagger_dart_code_generator: ^2.8.1

Далее я создал папку api_files в корне проекта, где поместил файл описания API — petstore_api.swagger.

Если у вас нет .swagger файла, вы можете взять описание API в json формате и поменять разрешение файла на .swagger.

Для конфигурации генерации API посредством пакета SwaggerDartCodeGenerator необходимо добавить файл build.yaml в корень проекта. В своём примере я использовал подобную конфигурацию:

targets:
  $default:
    sources:
      - lib/**
      - swaggers/**
      - swaggers2/**
      - api_files/**
      - swagger_examples/**
      - $package$
    builders:
      chopper_generator:
        options:
          header: "//Generated code"
      swagger_dart_code_generator:
        options:
          input_folder: "api_files/"
          output_folder: "lib/swagger_generated_api/"
          override_equals_and_hashcode: true

Далее после настройки проекта необходимо воспользоваться командой “flutter packages pub run build_runner build --delete-conflicting-outputs” для того чтобы отработал генератор. После выполнения данной команды в нашем репозитории в папке проекта lib появится новая папка swagger_generated_api, с содержанием petstore_api, клиент которого реализован с помощью пакета Chopper и модели JsonSerializable. Чтобы далее использовать наш API в проекте, необходимо создать файл сервиса или репозитория, где мы будем обращаться к методам нашего API. Я создал файл с реализацией класса PetstoreNetworkService, в конструкторе которого я обращаюсь к PetstoreApi, чтобы получить объект реализации методов обращения к нашему API.

PetstoreNetworkService() {
    // Creating an instances to call the pet store API
    petApi = PetstoreApi.create();
  }

  late final PetstoreApi petApi;

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

Future<Pet?> getPetById(int id) async =>
      petApi.petPetIdGet(petId: id).then((result) {
        return result.body;
      }).catchError((Object error) {
        // Do something on error
      });

Выводы

Использование кодогенерации для API клиента может существенно сократить время разработки приложения и настраивание его работы с сетью. Я бы рекомендовал использование пакета Openapi generator в больших проектах, когда важно выполнить следующие требования:

  • API можно разделить по файлам на различные сервисы/репозитории с использованием Dio клиента.

  • реализацию вашего API можно отделить от основного проекта в отдельный пакет.

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

Полезные ссылки:

Спасибо за внимание!

Полезные материалы для разработчиков мы также публикуем в наших соцсетях – ВК и Telegram.

Комментарии (1)


  1. IL_Agent
    26.10.2022 10:54

    Спасибо.

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