Dart-пакетом считается любая папка с pubspec.yaml; Flutter-пакетом называют пакет, зависящий от Flutter SDK; плагин представляет собой Flutter-пакет, который вдобавок несёт платформенный код (Kotlin, Java, Swift, Objective-C, C/C++), подключаемый к приложению при сборке.
Dart-пакет (collection, http): чистый Dart, работает везде, где работает Dart, включая серверы и CLI
Flutter-пакет (provider, go_router): импортирует package:flutter, поэтому требует Flutter SDK, но нативного кода в нём нет
Плагин (camera, shared_preferences): объявляет блок flutter: plugin: platforms:; тулинг генерирует registrant, который подключает нативную часть, поэтому после добавления плагина нужна полная пересборка, а не hot restart
FFI-плагин: нативный код вызывается через dart:ffi, а не через платформенные каналы (flutter create --template=plugin_ffi)
На практике: плагин должен поддерживать каждую платформу, на которую вы выпускаетесь; список платформ стоит проверить на pub.dev до добавления
FlutterMiddle
Пакеты и зависимости
Ограничения версий, pubspec.lock, разрешение версий, overrides, публикация, melos, апгрейды
Закрыто вопросов
0 / 19
- 0Повторить
- 2Часто спрашивают
Вопросы
В dependencies лежит то, что нужно вашему коду для работы, и это наследует каждый, кто зависит от вас; в dev_dependencies лежат инструменты для работы над самим пакетом (тесты, кодогенерация, линты), они не наследуются; dependency_overrides навязывают версию всему графу, и только в корневом пакете.
Простое правило: если это импортирует lib/, место в dependencies; если это нужно только test/, tool/ или командной строке, место в dev_dependencies
Типичное разделение: json_annotation в dependencies (его импортируют ваши модели), json_serializable и build_runner в dev_dependencies (они работают только на вашей машине)
Почему это важно для пакета: каждая запись в dependencies становится ограничением, которое должен удовлетворить солвер ваших потребителей; build_runner там затягивает analyzer в их разрешение версий
Дело не в размере APK: AOT-компиляция выбрасывает Dart-код, недостижимый из main(), так что dev-инструмент не в той секции почти не меняет бинарник; а вот плагин из dependencies попадает в сборку со своим нативным кодом
Overrides: игнорируются, когда от вашего пакета кто-то зависит; это временный обход, а не исправление
Приложение всегда коммитит pubspec.lock. Публикуемый пакет обычно нет: когда от него зависят, граф целиком разрешает приложение, а lock-файл пакета игнорируется.
Что в нём лежит: точная версия и хеш содержимого (sha256) каждого пакета, прямого и транзитивного, по итогам последнего разрешения версий
**pub get воспроизводит его: без lock-файла каждая машина берёт самое свежее в рамках ограничений на день запуска, и ноутбук с CI могут собрать разный код
В CI**: flutter pub get --enforce-lockfile падает, если lock не совпадает с pubspec или с хешем скачанного архива, вместо того чтобы тихо его переписать
CI библиотеки: проверяйте оба края диапазонов: dart pub downgrade и тесты, затем dart pub upgrade и тесты
Конфликт слияния в lock-файле: не правьте руками; возьмите любую сторону и снова запустите pub get
pub get берёт версии из pubspec.lock и разрешает только новое; pub upgrade заново подбирает самые свежие версии в рамках ваших ограничений; --major-versions вдобавок переписывает ограничения в pubspec.yaml, чтобы перейти через major.
**flutter pub get**: то, что запускают сборка и CI; уже зафиксированный пакет никогда не сдвигается, поэтому свежий dio сам не появится
**flutter pub upgrade dio**: ^5.7.0 доходит до самой свежей 5.x и останавливается; меняется только pubspec.lock
**--major-versions**: правит pubspec.yaml (^5.7.0 превращается в ^6.0.0); changelog придётся читать вам, pub этого не делает
**flutter pub outdated: посмотрите, прежде чем двигать: Current, Upgradable, Resolvable и Latest рядом
--tighten**: поднимает нижние границы до реально выбранных версий, чтобы pubspec не обещал поддержку версий, которые никто не проверял
Каретка разрешает всё до следующей ломающей версии, а ниже 1.0.0 ломающим считается minor: ^5.7.0 означает >=5.7.0 <6.0.0, а ^0.20.2 означает только >=0.20.2 <0.21.0.
**dio: ^5.7.0: любая 5.x начиная с 5.7.0, но никогда 6.0.0
intl: ^0.20.2**: только 0.20.x; версия 0.x означает нестабильный API, поэтому 0.21.0 считается новым major. Аналогично ^0.0.3 означает >=0.0.3 <0.1.0
**uuid: 4.5.1: без оператора это жёсткий пин, а не минимум; в приложении терпимо, в публикуемом пакете вредно, потому что не оставляет солверу пространства
web_socket_channel**: >=3.0.0 <3.0.3 разрешает 3.0.0–3.0.2; длинная форма нужна, чтобы исключить плохую 3.0.3, ведь в pub нет !=. В YAML её нужно брать в кавычки
**crypto: any: любая когда-либо опубликованная версия; солвер может откатиться к древней, чтобы удовлетворить чужую границу
На практике**: каретка на всё, а точные версии держит pubspec.lock
Hosted-зависимость задаёт диапазон версий, о котором солвер может договориться; path и git задают одну фиксированную версию, а пакет с любой из них нельзя опубликовать на pub.dev. Ветка допустима лишь ненадолго, с закреплением на коммите, пока ждёте исправление в апстриме.
Hosted (pub.dev или приватный сервер): версионируется, кешируется и воспроизводится через хеш содержимого в lock-файле
**path:: подходит для пакетов внутри одного репозитория (а ещё лучше pub workspace); плохо, как только путь уходит за пределы репозитория: тогда это работает только на вашей машине
git:**: каждому разработчику и раннеру CI нужен доступ к репозиторию; lock фиксирует найденный коммит, но pub upgrade уезжает за веткой ref на её новую вершину
**Фиксируйте ref: на коммите или теге**: или, начиная с Dart 3.9, используйте tag_pattern: с ограничением version:, чтобы pub подбирал версию по тегам репозитория
Когда ветка допустима: ненадолго: указывайте свой форк по коммиту, пока открыт PR в апстрим, с комментарием со ссылкой на PR и условием удаления
Либо git-зависимостями, закреплёнными на тегах (это не требует инфраструктуры), либо через приватный pub-репозиторий, который сохраняет полноценное разрешение версий. Когда общих пакетов больше нескольких, обычно выигрывает приватный репозиторий.
Git с тегами: ноль настройки, работает с SSH-ключами, которые уже есть в CI; с tag_pattern (Dart 3.9+, и в вашем pubspec нужен sdk: ^3.9.0) pub даже подбирает версию по тегам, но каждой машине нужен доступ к каждому репозиторию
Приватный pub-репозиторий: Cloudsmith, JFrog Artifactory или свой сервер, реализующий API pub-репозитория; каждый пакет указывает этот URL в publish_to:, чтобы его нельзя было случайно опубликовать на pub.dev
Авторизация: dart pub token add <url> сохраняет токен для хоста; в CI добавьте --env-var, чтобы секрет оставался в переменной окружения
Потребители: hosted: <url> плюс version: у каждой приватной зависимости
Или один репозиторий: если все приложения живут в одном монорепозитории, pub workspace полностью убирает шаг публикации
Дисциплина: semver и changelog, как на pub.dev, иначе потребители перестают обновляться
Читайте снизу: последнее предложение называет ваш пакет и две зависимости, которые не уживаются, а первая строка Because называет общий пакет, о версии которого они спорят. Затем двигайте ту сторону, которая может двигаться.
Почему так много текста: солвер печатает доказательство: каждая строка Because… обосновывает следующую; первая называет общий пакет, и только последнее предложение касается вашего пакета
Соберите факты: flutter pub deps --style=compact показывает, кто тянет общий пакет; flutter pub outdated показывает, разрешается ли хоть какой-то релиз
Варианты, от лучшего: обновить отстающую зависимость, откатить ту, что убежала вперёд, ослабить своё ограничение или обновить Flutter, если пин держит пакет из SDK
Пакеты SDK не двигаются: flutter_localizations и flutter_test фиксируют точные версии пакетов вроде intl, так что уступать придётся другой стороне
**requires SDK version в цепочке означает конфликт с самим Dart SDK, а не с другим пакетом
Крайняя мера**: dependency_overrides с комментарием и тикетом на удаление
Он велит солверу использовать выбранную вами версию пакета везде в графе и игнорировать все ограничения, которые на него наложили другие пакеты, и pub get зеленеет на комбинации, которую никто не тестировал.
Ломается поздно: автор поставил <0.21.0 потому, что там что-то ломается, и часто в рантайме, а не при компиляции
Только в корне: overrides внутри зависимости игнорируются, так что overrides опубликованного пакета никого не защищают; в pub workspace overrides любого участника действуют на весь workspace, причём каждый пакет можно переопределить только один раз
Переживает проблему: его легко пропустить на ревью, и через несколько месяцев он блокирует настоящее обновление
Честные альтернативы: сдвинуть пакет, который может двигаться, или отправить в апстрим PR, расширяющий ограничение, а пока зависеть от своего форка по коммиту
Локальные эксперименты: pubspec_overrides.yaml рядом с pubspec, добавленный в .gitignore, чтобы указать на локальную копию
Если без него никак: комментарий со ссылкой на issue и условием удаления
Диапазон environment: sdk: этого пакета исключает вашу версию Dart, и pub выбрасывает из разрешения все его версии. Нижняя граница ниже 2.12 означает, что пакет написан до null safety, и Dart 3 вообще не может его запустить.
Dart поставляется внутри Flutter: отдельно его не выбирают; обновление Flutter означает обновление Dart
Исключение Dart 3: null-safe пакет с >=2.12.0 <3.0.0 по-прежнему разрешается: pub читает эту верхнюю границу как <4.0.0; отсекаются только пакеты без null safety
Нижняя граница задаёт версию языка: пакет с sdk: ^3.0.0 компилируется с семантикой Dart 3.0 даже на новом SDK, поэтому для новой фичи вроде null-aware элементов коллекций нужна ^3.8.0
Варианты, от лучшего: обновиться на новый релиз (flutter pub outdated покажет, есть ли он), перейти на замену или поддерживаемый форк, названные на его странице pub.dev, самим мигрировать форк либо отказаться от пакета
Зеркальная ловушка: опубликовав свой пакет с узкой верхней границей вроде <3.9.0, блокером станете вы; ^3.4.0 ломается только на Dart 4
Major, 2.0.0: изменения 1 и 2 по отдельности могут сломать компиляцию кода потребителя, а это и есть определение ломающего изменения. Изменение 3 само по себе было бы minor.
Новый обязательный параметр: каждый существующий вызов Toast(message: …) перестаёт компилироваться
Новое значение enum: в Dart 3 switch по enum исчерпывающий, поэтому switch потребителя без default перестаёт компилироваться
Тоже ломает, хотя выглядит как добавление: новый член класса, который другие могут implements, или новый подтип sealed-класса
Minor: новые необязательные параметры, новые классы, функции и расширения, новые члены у final class
Patch: исправления багов без изменения API
Ниже 1.0.0: каретка сдвигается на разряд: после 0.4.2 ломающим релизом будет 0.5.0
Чтобы остаться на 1.x: сделайте icon необязательным, а warning отложите до 2.0.0
Публичный API кладите в lib/, реализацию кладите в lib/src/, добавьте README.md, CHANGELOG.md, LICENSE и example/, запустите dart pub publish --dry-run, затем публикуйте. Опубликованная версия вечна: её можно отозвать (retract) в течение семи дней, но не удалить.
Структура: lib/acme_result.dart экспортирует только публичное; lib/src/ приватна по соглашению, и линт implementation_imports предупреждает потребителей, которые её импортируют
Pubspec: name, description, version, repository и ограничение environment: sdk:; зависимости только с pub.dev или из SDK, то есть никаких git: и path:
Dry run: проверяет pubspec и структуру и показывает список файлов, которые уйдут в архив; ваши тесты он не запускает. pana локально показывает, сколько pub points вы получите
Версионирование: semver и одна запись в CHANGELOG.md на каждый релиз; пока API меняется, оставайтесь на 0.x
Что навсегда: имя закрепляется за вами; отозванную версию новые разрешения пропускают, но в существующих lock-файлах она работает; пакет целиком можно только пометить как discontinued. Секрет в архиве утёк навсегда
Автоматизация: публикуйте из CI по git-тегу через GitHub OIDC, а не с ноутбука
Workspace (Dart 3.6+) превращает весь репозиторий в одно разрешение версий: один pubspec.lock, один pub get в корне и одна и та же версия каждой зависимости во всех пакетах. Melos добавляет то, чего pub не делает: скрипты по всем пакетам, версионирование и changelog.
Настройка: корневой pubspec перечисляет участников в workspace:; каждый участник добавляет resolution: workspace и требует sdk: ^3.6.0 или выше
Участники зависят друг от друга по версии: acme_core: ^1.0.0 автоматически разрешается в локальный пакет, без path:; вне workspace используется hosted-пакет
Согласованность по построению: две фичи не могут тихо использовать разные версии dio; расхождение валит pub get, а не всплывает в рантайме
Overrides: разрешены в любом участнике и действуют на весь workspace, каждый пакет один раз; держите их в корне. Старые pubspec_overrides.yaml времён Melos 6 по-прежнему действуют, так что удалите их
Melos 7: его конфиг живёт в корневом pubspec.yaml под ключом melos:; связывание пакетов теперь работа workspace
Чего не решает ни то, ни другое: один участник, застрявший на старой зависимости, теперь блокирует обновление всему репозиторию
Проверьте, может ли это вообще до вас дойти: при закоммиченном pubspec.lock и CI с pub get --enforce-lockfile плохой релиз в апстриме ничего не меняет, пока кто-то сознательно не обновится. Если он уже попал к вам, ограничьте его из своего pubspec.
Найдите, что сдвинулось: git diff pubspec.lock показывает, какие версии изменились; flutter pub deps --style=compact показывает, кто тянет пакет
Вернитесь к рабочему состоянию: восстановите pubspec.lock из последнего рабочего коммита и запустите pub get --enforce-lockfile
Ограничьте сами: добавьте транзитивный пакет в свои dependencies: с диапазоном, исключающим плохую версию, например '>=1.4.0 <1.4.3'. Это штатный способ ограничить транзитивную зависимость
Не жёсткий пин: broken_json: 1.4.2 сегодня говорит то же самое, а завтра загоняет солвер в угол
Сообщите в апстрим: автор может отозвать версию в течение семи дней, и тогда ни одно новое разрешение её не выберет
Приберитесь: уберите своё ограничение, когда выйдет исправленный релиз
Это федеративный плагин: Dart API, платформенный интерфейс и реализация под каждую платформу живут в отдельных пакетах, поэтому платформу можно добавить или починить без владельца плагина. Приложение зависит только от app-facing пакета.
App-facing (path_provider): API, который вы импортируете, логики в нём почти нет
Platform interface (path_provider_platform_interface): абстрактный класс плюс статический instance, через который идут все вызовы
По реализации на платформу (path_provider_android, path_provider_foundation, …): каждая наследует этот интерфейс
Как подхватывается реализация
Рекомендация (endorsement): app-facing пакет зависит от реализации и указывает её в default_package:, поэтому pub get её подтягивает, а сгенерированный registrant вызывает её registerWith(), который выставляет instance
Нерекомендованная (unendorsed) реализация: сторонний платформенный пакет добавляется в ваши собственные dependencies:
**extends, а не implements**: реализации наследуют интерфейс, и PlatformInterface.verify это проверяет, поэтому интерфейс может добавить метод с телом по умолчанию в minor-релизе, не сломав все реализации
Что это значит для вашего pubspec
Не перечисляйте рекомендованные (endorsed) реализации сами: вы зафиксируете версии, на которые app-facing пакет не соглашался
Падение разрешения версий обычно означает, что две реализации расходятся в версии platform interface, а не в самом плагине
В Dart-программе есть ровно одна копия каждой библиотеки, определяемая её package: URI, поэтому pub выбирает одну версию каждого пакета на всё приложение. Разрешение версий падает, и любое настоящее решение заканчивается тем, что одно из двух ограничений сдвигается.
Почему одна версия
Две копии analytics дали бы два разных типа Event, которые нельзя присвоить друг другу, плюс два набора глобальных переменных и синглтонов; pub отказывает сразу, а не даёт этому упасть в рантайме
Вложенность как в npm невозможна: импорт называет пакет, а не путь, и вторую копию просто некуда положить
В pub workspace падение блокирует весь репозиторий, а не только эти два пакета
Выходы, от лучшего
Обновите отстающего: обычно одна сторона просто старая, а свежий релиз уже принимает новый major
Мигрируйте свой пакет: переведите его ограничение на новый major отдельным PR
Расширьте в апстриме: PR, который только расширяет ограничение, легко принимают; пока ждёте, зависьте от своего форка по коммиту
Изолируйте: поставьте перед зависимостью свой интерфейс, чтобы её импортировал только один пакет
Override: в последнюю очередь и только с тестами для пакета, который теперь работает на major, которого он не объявлял
Вместо редких больших обновлений нужен ровный поток маленьких проверенных: бот каждую неделю открывает сгруппированные PR, CI проверяет каждый, а полное внимание человека нужно только для major-обновлений.
**Читайте flutter pub outdated: Upgradable выше Current достаётся даром; Resolvable выше Upgradable значит, что пакет держит ваше же ограничение; Latest выше Resolvable значит, что держит другой пакет, и исправление живёт в апстриме
Пусть PR открывает бот**: Dependabot (экосистема pub) или Renovate, раз в неделю и с группировкой: dev-зависимости одним PR, патчи другим, иначе двадцать PR в неделю никто не ревьюит
Automerge только за настоящим CI: анализ, тесты и релизная сборка, с pub get --enforce-lockfile, чтобы влилось ровно то, что проверено
Фиксируйте осознанно: сам Flutter SDK, который двигают отдельным PR; генераторы, чей вывод закоммичен; нативные SDK, которые CI не может проверить
Major обновляют руками: PR с --major-versions читают построчно с открытым changelog и никогда не вливают автоматически
Держите долг на виду: короткий ежемесячный разбор outdated; пакет, застрявший дольше квартала, становится тикетом с владельцем
Обновляйтесь в отдельной ветке с версией, зафиксированной для всей команды, отдайте механические переименования dart fix --apply, затем пройдитесь по заметкам о breaking changes, анализатору, тестам и нативной сборке и выпускайте через бета-трек.
До
Прочитайте release notes и страницу breaking changes для каждого пропускаемого релиза; прыжок через несколько версий сразу умножает работу
Сначала уберите предупреждения о deprecated на текущей версии: следующие релизы удаляют то, что эта пометила устаревшим
Зафиксируйте версию в одном месте, которое читают все ноутбуки и CI, например в .fvmrc от FVM
Во время
Обновите SDK, затем flutter pub upgrade: плагинам часто нужны релизы под новый SDK
dart fix --dry-run, затем --apply: Flutter поставляет data-driven fixes, которые сами переписывают переименованные API
flutter analyze и полный прогон тестов; golden-тесты, скорее всего, поменяются из-за обновлений Material
Соберите релизные APK и iOS: новые версии Flutter поднимают минимальные Android Gradle Plugin, Gradle, Kotlin и iOS deployment target, и эти правки в android/ и ios/ входят в тот же PR
После
Выпускайте сначала во внутреннее тестирование и TestFlight и следите за crash-free rate
Держите предыдущий SDK в ветке, чтобы откат был одним revert
Сначала спрячьте пакет за своим интерфейсом, затем выбирайте по масштабу: замените его, если есть поддерживаемая альтернатива, завендорьте в packages/, если используете небольшую часть чистого Dart, или форкните через git: с закреплённым коммитом, если он нужен почти целиком, а замена слишком велика.
Сначала загляните на pub.dev: на странице discontinued-пакета указана предлагаемая замена. Потом посчитайте места вызова и поищите папки android/ или ios/; двенадцать вызовов чистого Dart представляют совсем другую задачу, чем плагин с половиной на Kotlin.
Зафиксировать и ждать: даёт несколько недель, потом блокирует все остальные обновления, так что назначьте срок
**Форк через git: с закреплённым коммитом: быстрее всего, годится для приложения, но тупик для всего, что вы публикуете: у опубликованных пакетов не может быть git-зависимостей
Завендорить в packages/**: лучше всего, когда вы используете небольшую часть; сохраните LICENSE и смиритесь с тем, что исправлений вам больше никто не пришлёт
Заменить: единственный вариант, который уменьшает объём вашего кода, и единственный, который является настоящим проектом
Держите изменения ревьюируемыми: сначала тесты, фиксирующие текущее поведение, затем один PR, прячущий пакет за вашим интерфейсом, и второй, меняющий реализацию