2.2. Flutter: зависимости проекта

Библиотеки во Flutter-проекте играют важную роль в разработке приложений. Они представляют собой совокупность предопределённых функций, классов и компонентов, которые могут быть использованы разработчиками для упрощения и ускорения процесса создания приложений.

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

Коротко о библиотеках

Библиотеки бывают двух типов:

  • пакеты (англ. packages) — содержат только код, написанный на Dart и Flutter;
  • плагины (англ. plugins) — помимо Dart и Flutter имеют платформо-специфичный код, также называемый нативным. Хоть Flutter и позволяет писать сразу под несколько платформ, в некоторых случаях может потребоваться использование специфической функциональности, которая доступна только на одной из платформ. Например, это может быть интеграция с уведомлениями операционной системы, использование специфических интерфейсов пользователя, доступ к аппаратным функциям устройства (камере, датчикам, Bluetooth и проч.) или взаимодействие с другими приложениями, доступными только на конкретной платформе.

Официальный репозиторий пакетов для Dart и Flutter — ресурс pub.dev. На нём размещены пакеты, созданные сообществом Dart и Flutter, а также пакеты, разработанные и поддерживаемые самой командой Google.

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

Ниже — скриншот страницы библиотеки firebase_messaging версии 14.6.1. Можно увидеть, что она была выбрана как Flutter Favorite — это одна из «наград» на сайте, которая говорит о высочайшем качестве библиотеки.

Screenshot

Для работы с pub.dev мы можем использовать инструмент pub, о котором мы говорили в предыдущем парагафе.

А сейчас — разберёмся, как добавлять зависимости в проек.

Зависимости

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

Зависимости бывают четырёх видов с точки зрения источников.

Зависимость из pub.dev

Зависимость, которая была опубликована на сайте pub.dev, официальном публичном репозитории библиотек для Dart и Flutter.

1dependencies:
2    dependency_name: <version>

Git-зависимость

Зависимость из Git-репозитория.

1dependencies:
2  <dependency_name>:
3    git: https://github.com/<repo_url> 
4    ## Зависимость лежит по указанному пути в корне проекта

Помимо указания ссылки мы можем добавлять параметры ref и path:

  • ref — позволяет указывать номер коммита, название ветки или тега, чтобы тянуть зависимость не из ветки master или main, а исходя из идентификатора;
  • path — позволяет указывать пути до библиотеки, если пакет лежит не в корне проекта.
1dependencies:
2  <dependency_name>:
3    git:
4        url: https://github.com/<repo_url> 
5        ref: some-branch
6        path: some-path

Локальная зависимость

Зависимости, которые лежат в локальной файловой системе, могут устанавливаться локально. Для этого нужно прописать полный или относительный путь до этой зависимости.

1dependencies:
2  <dependency_name>:
3      path: <your_local_path>

Hosted-зависимость

Зависимость, которая находится на pub.dev, публична и доступна для всех разработчиков. Не всегда разработчики готовы делиться своими наработками, так что существует возможность загружать пакет с удалённого хранилища пакетов, который имеет такой же API, что и pub.dev. Для установки таких зависимостей требуется указывать URL в параметре hosted.

1dependencies:
2 <dependency_name>:
3    hosted: https://some-package-server.com
4    version: <version>

Такое приватное хранилище пакетов можно создать с помощью специального пакета unpub, о котором можно прочитать по ссылке.

Версионирование зависимостей

Как мы узнали в предыдущем параграфе, у библиотек есть обязательное поле version в файле pubspec.yaml.

Для определения версии пакета в Dart используется семантическое версионирование (англ. semantic versioning), которое состоит из трёх чисел, разделённых точками: мажор.минор.патч (англ. major.minor.patch). Каждое из чисел обозначает различные изменения в пакете:

  • мажор (основная версия) — изменения в API, которые обратно несовместимы (англ. breaking changes). Такие изменения могут повлиять на существующий код, который использует предыдущую версию библиотеки, и могут потребовать изменений или модификаций в этом коде, чтобы он продолжал работать с новой версией;
  • минор (англ. minor) — добавление новой функциональности в существующее API, которое не нарушает обратную совместимость;
  • патч (англ. patch) — исправление ошибок в пакете, которые не влияют на API.

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

  • any — используется для указания того, что проект совместим со всеми версиями зависимости. Это означает, что проект может использовать любую версию зависимости, которая доступна на момент установки;
  • x.y.z — установится версия x.y.z, где x — мажор, y — минор, а z — патч;
  • >x.y.z — установится максимально возможная версия из тех, что выше x.y.z;
  • >=x.y.z — установится максимально возможная версия из тех, что выше, или равная версии x.y.z;
  • <x.y.z — установится максимально возможная версия из тех, что ниже x.y.z;
  • <=x.y.z — установится максимально возможная версия из тех, что ниже, или равная версии x.y.z;
  • ^x.y.z — расшифровывается как >=x.y.z <(x+1).0.0 ^1.2.3 эквивалентно >=1.2.3 <2.0.0. Однако для версий с мажором, равным нулю, работает по-другому: ^0.1.2 эквивалентно >=0.1.2 <0.2.0. Правило простое: если мажор равен нулю, то библиотека ещё в бета-версии и любые минорные изменения могут быть обратно несовместимы.

О семантическом версионировании можно прочесть в официальной документации. В ней же, в последнем абзаце, рассказано о библиотеках с нулевой мажорной версией.

Также можно указывать диапазон версий с помощью комбинирования операторов <, <=, >, >=. Например, конструкция >=1.12.8 <=3.0.0 означает, что мы хотим установить версию от 1.12.8 включительно до 3.0.0 включительно.

Максимально возможная версия

Выше мы упоминали этот термин, давайте разберёмся, что он означает. Но сперва — поговорим ещё об одном варианте разделения зависимостей на:

  • прямые;
  • транзитивные;
  • и совместно используемые.

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

fluttern

Транзитивная (англ. transitive) же зависимость подтягивается из пакета, который мы устанавливаем как зависимость. Это зависимость нашей зависимости.

fluttern

А общая (англ. shared) зависимость — зависимость, которую подтягиваем и мы, и используемый нами пакет.

fluttern

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

Для иллюстрации выше можно заметить, что мы пытаемся установить пакет в версии ^1.0.0 и одновременно версии ^2.0.0 — так как пересечения нет, возможной для установки версии тоже не существует.

Такая ситуация называется конфликтом, и при вызове команды flutter pub get в консоли появится следующее сообщение:

1Because Package A depends on Package B ^2.0.0 and no version of Package A match, Package A requires Package B ^2.0.0.
2So, because app depends on both Package B ^1.0.0 and Package A, version solving failed.

Существует три способа решения данной проблемы:

  1. Поменять версию пакета B на ^2.0.0 в нашем приложении.
  2. Установить версию пакета A, которая использует версию пакета B ^1.0.0.
  3. Переопределить зависимости с помощью dependency_overrides. Как следует из названия, мы перезаписываем версию библиотеки. Можно указать пакет, версию и источник в свойстве dependency_overrides, и pub будет использовать эти значения вместо тех, которые указаны в dependencies или подтягиваются транзитивно.

В нашем случае это работает так, что мы используем пакет A, который устанавливает транзитивно пакет B версии 2.0.0, но мы хотим использовать версию 1.0.0, и для этого можно указать в файле pubspec.yaml следующее:

1dependency_overrides:
2    package_B: ^1.0.0

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

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

Помимо зависимостей, указывающихся в pubspec.yaml как dependencies, существуют dev_dependencies. Это зависимости, которые нужны только на этапе разработки, они не попадают в сборку приложения. К ним относится линтер, различные зависимости для кодогенерации и аннотаций, зависимости для тестов и другие зависимости, которые не используются для сборки приложения.

1dev_dependencies:
2    flutter_lints: ^2.0.2

Если команда  pub get выполнится успешно, то в нашем проекте автоматически сгенерируется файл pubspec.lock.

Давайте рассмотрим его подробнее.

pubspec.lock

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

1# Generated by pub
2# See https://dart.dev/tools/pub/glossary#lockfile
3packages: # Тут содержится список всех зависимостей
4  dependency_name:
5    dependency: transitive # Тут содержится тип зависимости: transitive, shared, direct main, direct overridden
6    description:
7       ... # Тут содержится короткое описание зависимости, для зависимости с pub.dev тут будут содержаться поля name и url с названием и путём до зависимости соответственно
8    source: ... # Тут содержится источник зависимости, для зависимости с pub.dev тут будет стоять hosted
9    version: ... # Тут — версия зависимости, которая установилась после прогона команды flutter pub get
10sdks: # В этой части есть констрейнты для dart и flutter
11 dart: ">=2.15.0 <3.0.0" # Устанавливаем Dart версии выше или равной 2.15.0, но меньше 3.0.0
12  flutter: ">=2.8.0" # Устанавливаем Flutter версии выше или равной 2.8.0

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

Это можно объяснить на таком примере: в команду может прийти новый разработчик, запустить команду pub get — и у него не установятся новые мажорные версии библиотек, ведь lock-файл уже существует и не поменяется.

Однако для библиотек этот файл в репозитории следует игнорировать. Библиотеки должны содержать актуальные версии своих зависимостей, чтобы разработчик библиотеки не использовал старый сгенерированный lock-файл, с которым у него всё работает, а пользователи не устанавливали новые версии транзитивных библиотек, с которыми проект несовместим.

Как структурировать свою библиотеку

Для создания Flutter-проекта используется команда flutter create. Для создания библиотеки или плагина нужно указать флаг --template.

Команда flutter create --template=package package_name используется для создания пакета, а flutter create --template=plugin plugin_name — для создания плагина.

Для библиотек принято создавать вот такую структуру:

image

Отличие от структуры проекта в том, что библиотека содержит папку lib/src. Все реализации будут храниться в lib/src и будут приватны, то есть недоступны для внешних пользователей.

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

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

1// my_library.dart
2library my_library; // Уникальный тег, должен совпадать с названием файла
3
4export 'src/file1.dart'; // Экспортируем весь функционал файла file1.dart
5export 'src/file2.dart' show ClassA; // Экспортируем только класс ClassA из файла file2.dart
6export 'src/file3.dart' hide ClassB; // Экспортируем всё, кроме класса ClassB, из файла file3.dart

Для использования функционала из файлов проекта или из библиотеки следует использовать импорты.

1import 'package:my_package/my_package.dart'; // Импортируем весь функционал, экспортированный в файле my_package.dart библиотеки my_package
2import 'src/some_file.dart' show SomeClassThatOurClientWillUse; // Импортируем только класс SomeClassThatOurClientWillUse
3import 'src/some_file.dart' hide SomeClassThatOurClientDoesNotNeed; // Импортируем всё, кроме класса SomeClassThatOurClientDoesNotNeed
4import 'src/some_file.dart' as someName; // Импортируем файл как someName. Потом к функционалу можно будет обращаться с префиксом someName: someName.<ClassName>.<MethodToCall>()

Помимо экспорт-файлов у библиотек есть ещё одна особенность — файл CHANGELOG.md, который позволяет познакомить пользователей с изменениями. Давайте рассмотрим его подробнее.

CHANGELOG.md

Мы крайне рекомендуем актуализировать файл CHANGELOG.md.

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

Вот так выглядит часть файла для разработчика:

1# 2.0.2
2
3Fixed an assert error if a `family` depends on itself while specifying `dependencies`.

А вот так — для того, кто читает информацию о последней версии:

image

Как выбрать хорошую библиотеку

К сожалению, единой инструкции нет, но можно обратить внимание на несколько параметров.

Насколько давно библиотека была обновлена и поддерживается ли она сейчас

На pub.dev это можно увидеть сразу под названием библиотеки.

Например, на момент создания статьи библиотека firebase_messaging была опубликована последний раз пять дней назад.

Screenshot

Насколько подробен CHANGELOG

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

Сколько обращений в issue_tracker

И насколько быстро разработчик правит все замечания и отвечает на новые открытые вопросы.

Различные метрики популярности

Можно смотреть на звёздочки на GitHub или на секцию Scores на pub.dev — в этой секции показаны многие метрики, включая популярность библиотеки, лайки и собственную метрику pub points, которая показывает уровень библиотеки, актуальность зависимостей, добавлена ли документация, следует ли библиотека гайдлайнам.

Screenshot

Отдельный пункт — “Dart 3 compatibility”, то есть, совместима ли библиотека с  Dart версии 3.

Это важный нюанс. Дело в том, что в Dart начиная с версии 2.12 от проектов и библиотек требуется поддержки безопасности от null-значений (англ. Null Safety). До Dart версии 3 проект можно было запускать с флагом --no-sound-null-safety, игнорируя предупреждения о небезопасной работе с null-значениями.

С третьей мажорной версии так не сделаешь — все библиотеки и сам проект обязаны безопасно работать с nullable-значениями. В пункте “Dart 3 compatibility” секции Scores как раз и показываются баллы за полную поддержку Null Safety.

Полезные советы по работе с зависимостями

  1. Не забывайте запускать команду flutter pub get перед запуском проекта, чтобы установить все необходимые зависимости. Если проект не собирается по непонятным причинам, может помочь команда flutter clean — она очистит .dart_tools и удалит возможно повреждённый кеш. После неё снова предстоит переустановить зависимости с помощью команды flutter pub get.
  2. Можно использовать команду flutter pub get --offline для установки библиотек из кеша, если в данный момент нет соединения с интернетом.
  3. Когда мы запускаем команду flutter pub get, создаётся папка .pub_cache, она содержит закешированные библиотеки, которые мы установили. Бывает такое, что кеш слетает. К примеру, когда в него добавилась кодогенерация или мы сами непреднамеренно изменили код одной из библиотек. Ни в коем случае не стоит модифицировать файлы из кеша. Вместо этого стоит запустить flutter pub cache clean для полной очистки кеша и flutter pub cache repair для переустановки каждой библиотеки.
  4. В некоторых случаях возникают ошибки, связанные с платформо-специфичными библиотеками. Это может произойти, если указанные библиотеки не были установлены. Для очистки нативных зависимостей с целью их переустановки следует запустить pod deintegrate && pod install для iOS и gradlew clean для Android.

На этом всё! С зависимостями разобрались. Советуем пройти квиз, чтобы закрепить знания.

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

Чтобы добавить в заметки выделенный текст, нажмите Ctrl + E

Отмечайте параграфы как прочитанные, чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф2.1. Flutter: структура проекта
Следующий параграф2.3. Ресурсы приложения