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

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

Типы библиотек

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

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

pub.dev

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

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

Screenshot

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

Зависимости

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

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

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

dependencies:
    dependency_name: <version>

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

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

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

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

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

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

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

dependencies:
  <dependency_name>:
      path: <your_local_path>

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

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

dependencies:
 <dependency_name>:
    hosted: https://some-package-server.com
    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 в консоли появится следующее сообщение:

Because Package A depends on Package B ^2.0.0 and no version of Package A match, Package A requires Package B ^2.0.0.
So, 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 следующее:

dependency_overrides:
    package_B: ^1.0.0

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

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

dev_dependencies:
    flutter_lints: ^2.0.2

pubspec.lock

pubspec.lock — файл, который автоматически генерируется при успешном запуске команды pub get в проекте Flutter или Dart. Он содержит информацию о фактических версиях зависимостей, которые были установлены для проекта, включая транзитивные зависимости.

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

# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages: # Тут содержится список всех зависимостей
  dependency_name:
    dependency: transitive # Тут содержится тип зависимости: transitive, shared, direct main, direct overridden
    description:
       ... # Тут содержится короткое описание зависимости, для зависимости с pub.dev тут будут содержаться поля name и url с названием и путём до зависимости соответственно
    source: ... # Тут содержится источник зависимости, для зависимости с pub.dev тут будет стоять hosted
    version: ... # Тут — версия зависимости, которая установилась после прогона команды flutter pub get
sdks: # В этой части есть констрейнты для dart и flutter
	dart: ">=2.15.0 <3.0.0" # Устанавливаем Dart версии выше или равной 2.15.0, но меньше 3.0.0
  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 используется в файле, который находится в корне библиотеки и который будет необходим для импорта в других частях проекта, зависимого от библиотеки. Часто названия экспорт-файлов совпадают с названием библиотеки.

// my_library.dart
library my_library; // Уникальный тег, должен совпадать с названием файла

export 'src/file1.dart'; // Экспортируем весь функционал файла file1.dart
export 'src/file2.dart' show ClassA; // Экспортируем только класс ClassA из файла file2.dart
export 'src/file3.dart' hide ClassB; // Экспортируем всё, кроме класса ClassB, из файла file3.dart

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

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

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

CHANGELOG.md

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

# 2.0.2

Fixed 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, которая показывает уровень библиотеки, актуальность зависимостей, добавлена ли документация, следует ли библиотека гайдлайнам и совместима ли она с Dart версии 3.

Что такое совместимость с Dart 3?

Null safety — это концепция, которая обеспечивает безопасную работу с опциональными типами значений. Она стала доступной в Dart начиная с версии 2.12, требуя от проектов и библиотек поддержки безопасности от null-значений. До Dart версии 3 проект можно было запускать с флагом --no-sound-null-safety, игнорируя предупреждения о небезопасной работе с null-значениями. С третьей мажорной версии так не сделаешь — все библиотеки и сам проект обязаны безопасно работать с nullable-значениями. Новый пункт Dart 3 compatibility в Scores показывает баллы, которые дают за полную поддержку null safety.

Screenshot

Источник: https://pub.dev/packages/dio/score

Полезные советы

  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.

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

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

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