Start Debugging

Исправление: Unable to load asset во Flutter после добавления изображения в pubspec.yaml

Ключа ассета нет в собранном bundle, а не на диске. Исправьте отступы в pubspec, добавьте слэш, приведите ключ в точное соответствие и перезапустите приложение.

Файл лежит на диске, путь выглядит правильным, а Flutter всё равно говорит, что не может его загрузить. Дело в том, что сообщение не про диск: переданного вами ключа нет в собранном bundle ассетов. По убыванию частоты причина такая: блок assets: не имеет отступа под flutter:, у записи каталога отсутствует завершающий /, файл лежит в подкаталоге, который никогда не объявляли, ключ отличается от имени файла регистром, либо был сделан hot reload там, где нужен полный перезапуск. Исправьте pubspec.yaml, остановите приложение и запустите его заново.

======== Exception caught by image resource service ================================================
The following assertion was thrown resolving an image codec:
Unable to load asset: "assets/images/logo.png".
The asset does not exist or has empty data.

When the exception was thrown, this was the stack:
#0      PlatformAssetBundle.load (package:flutter/src/services/asset_bundle.dart:271:7)
<asynchronous suspension>
#1      AssetBundleImageProvider._loadAsync (package:flutter/src/painting/image_provider.dart:951:14)

Это руководство написано для Flutter 3.44.7 и Dart 3.12.2, канал stable по состоянию на 2026-07-20. Описанное поведение стабильно с тех пор, как Flutter 3.16 изменил формат манифеста ассетов, а правила pubspec не менялись годами.

Что на самом деле означает ошибка

Image.asset('assets/images/logo.png') не открывает файл. Этот вызов передаёт строковый ключ фреймворку, который запрашивает у движка байты, зарегистрированные под этим ключом в bundle ассетов приложения. PlatformAssetBundle.load выбрасывает исключение в тот момент, когда движок возвращает null или буфер нулевой длины:

// flutter/lib/src/services/asset_bundle.dart, Flutter 3.44.7
throw FlutterError.fromParts(<DiagnosticsNode>[
  _errorSummaryWithKey(key),
  ErrorDescription('The asset does not exist or has empty data.'),
]);

Этот bundle один раз собирает инструмент flutter из секции flutter: assets: файла pubspec.yaml. Всё перечисленное там копируется в build/flutter_assets/ и индексируется в манифесте AssetManifest.bin, который движок загружает при старте. Больше ничего в вашей файловой системе для запущенного приложения не существует.

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

  1. Объявление в pubspec должно поместить файл в bundle.
  2. Ключ в коде на Dart должен совпадать с ключом bundle побайтово.

Каждая причина ниже - это отказ одной из этих двух вещей.

Минимальный пример воспроизведения

my_app/
  pubspec.yaml
  assets/
    images/
      logo.png
  lib/
    main.dart
# pubspec.yaml, Flutter 3.44.7
name: my_app

flutter:
  uses-material-design: true
  assets:
    - assets/images/logo.png
// lib/main.dart, Flutter 3.44.7, Dart 3.12.2
import 'package:flutter/material.dart';

void main() => runApp(
      const MaterialApp(
        home: Scaffold(
          body: Center(child: Image.asset('assets/images/logo.png')),
        ),
      ),
    );

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

Причина 1: блок assets не вложен в flutter

Это самый частый и самый неприятный сбой, потому что никто не жалуется. flutter pub get отрабатывает успешно, сборка проходит, а приложение стартует с пустым bundle.

# Wrong. Valid YAML, silently ignored.
flutter:
  uses-material-design: true
assets:
  - assets/images/logo.png

assets: на верхнем уровне - это ключ, который инструмент Flutter не читает. Это не ошибка, для парсера это просто чужая конфигурация. Правильная форма даёт assets: ровно два пробела отступа под flutter:, а элементам списка - ещё два:

# Right.
flutter:
  uses-material-design: true
  assets:
    - assets/images/logo.png

Родственный случай: второй ключ flutter: ниже по файлу. В отображениях YAML не может быть дублирующихся ключей, и в зависимости от парсера один из них молча побеждает. Если ваш pubspec разрастался стихийно, найдите в нём все вхождения flutter: в нулевой колонке, прежде чем отлаживать что-либо ещё.

Причина 2: запись каталога без завершающего слэша или подкаталог, который не объявляли

Записи каталогов подключаются по одной и не работают рекурсивно. Из документации Flutter про добавление ассетов: “Only files located directly in the directory are included. Resolution-aware asset image variants are the only exception. To add files located in subdirectories, create an entry per directory.”

То есть вот это не объявляет ничего полезного, если ваши изображения лежат в assets/images/icons/:

flutter:
  assets:
    - assets/images/

а нужно вот это:

flutter:
  assets:
    - assets/images/
    - assets/images/icons/
    - assets/images/illustrations/

Именно завершающий слэш делает запись каталогом. - assets/images без него читается как единственный файл с именем images, и, поскольку такого файла нет, сборка падает на уровне инструмента с сообщением, которое действительно помогает:

Error: unable to find directory entry in pubspec.yaml: /path/to/my_app/assets/images/

Это полезно знать и в обратную сторону: если сборка прошла успешно, а во время выполнения вы всё равно получаете Unable to load asset, значит, запись чему-то соответствовала. Тогда проблема в несовпадении ключа, а не в отсутствующем объявлении.

Единственное исключение из правила нерекурсивности - варианты под разное разрешение. Если вы объявили assets/images/logo.png, то assets/images/2.0x/logo.png и assets/images/3.0x/logo.png попадут в bundle автоматически, а AssetImage выберет нужный по device pixel ratio. Каталоги вариантов вы никогда не объявляете сами.

Причина 3: ключ в коде не совпадает с ключом в bundle

Ключи bundle - это точные строки. Три способа разойтись с тем, что вы написали:

Регистр. На вашей машине для разработки почти наверняка файловая система нечувствительна к регистру (APFS в macOS по умолчанию, NTFS в Windows). Image.asset('assets/images/Logo.png') локально находит файл logo.png и падает на устройстве Android, в iOS, в web и на любом Linux-раннере CI. Если сборка работает на ноутбуке и падает везде остальном, проверяйте регистр первым делом. Это самое вероятное объяснение ситуации, когда один и тот же код ведёт себя по-разному на разных машинах.

Ведущий ./ или случайный пробел. './assets/images/logo.png' - это другая строка, нежели 'assets/images/logo.png', а в bundle есть только вторая. Пробел в конце значения YAML в кавычках даёт тот же эффект.

Префикс packages/. Ассет, поставляемый внутри пакета, от которого вы зависите, имеет ключ packages/<package_name>/<path>, причём каталог lib/ пакета подразумевается и никогда не пишется явно. Чтобы загрузить lib/assets/bg.png из пакета fancy_backgrounds:

// Flutter 3.44.7. Either form works; they produce the same key.
Image.asset('packages/fancy_backgrounds/assets/bg.png');
Image.asset('assets/bg.png', package: 'fancy_backgrounds');

Если пакет писали вы, он тоже должен объявить эти файлы в собственном pubspec.yaml. Ассеты зависимости не попадают в bundle только потому, что файл существует в .pub-cache.

Причина 4: вы сделали hot reload там, где нужен перезапуск

Hot reload подменяет код на Dart в работающем изоляте. Bundle ассетов и его манифест создаёт инструмент при запуске приложения. Правка pubspec.yaml с добавлением новой записи меняет манифест, а работающее приложение сохраняет тот манифест, с которым стартовало.

Остановите сессию и запустите её заново. Ни r, ни R:

# Flutter 3.44.7
# Ctrl-C to end the current run, then:
flutter run

Изменение байтов уже объявленного ассета пересобирается при reload и в этом не нуждается. Изменение набора объявленных ассетов - нуждается.

Причина 5: устаревшие артефакты на диске

Редко бывает причиной, дёшево исключается и стоит первым пунктом в каждом ответе в интернете, из-за чего на неё списывают куда больше сбоев, чем она вызывает. Реальной причиной она бывает на iOS, где наполовину обновлённый bundle .app может пережить пересборку:

# Flutter 3.44.7
flutter clean
flutter pub get
flutter run

Если по пути падает сам flutter pub get, это проблема разрешения зависимостей, а не ассетов, и вывод решателя ограничений - отдельное упражнение: см. как читать ошибку version solving failed в pubspec.yaml.

Хватит гадать: выведите ключи, которые реально лежат в bundle

Каждый раздел выше - это гипотеза. Все их можно заменить одним измерением. AssetManifest - это поддерживаемый API для чтения манифеста во время выполнения, добавленный тогда, когда AssetManifest.json заменили на AssetManifest.bin:

// Flutter 3.44.7, Dart 3.12.2
import 'package:flutter/services.dart';

Future<void> dumpAssetKeys() async {
  final manifest = await AssetManifest.loadFromAssetBundle(rootBundle);
  for (final key in manifest.listAssets()..sort()) {
    debugPrint(key);
  }
}

Вызовите это из main под проверкой kDebugMode и прочитайте консоль. Всё, что напечатано, движок способен отдать. Если вашего пути там нет, проблема в Причине 1 или 2. Если присутствует что-то почти совпадающее с вашим путём, это Причина 3, и разница между двумя строками и есть исправление.

Не разбирайте AssetManifest.bin самостоятельно. Flutter документирует его как деталь реализации, формат которой может измениться без объявления, а AssetManifest.json больше вообще не генерируется, поэтому код, который до сих пор вызывает rootBundle.loadString('AssetManifest.json'), выбрасывает ровно эту ошибку с ключом AssetManifest.json.

Bundle можно осмотреть и вовсе ничего не запуская:

# Flutter 3.44.7. Writes the bundle the engine would load.
flutter build bundle
ls build/flutter_assets/assets/images/

# Or check what shipped inside a built APK:
unzip -l build/app/outputs/flutter-apk/app-debug.apk | grep flutter_assets

Варианты, которые приводят на эту страницу

Сквозная мысль: эта ошибка - промах поиска в структуре данных, которую создала ваша сборка. Так к ней и относитесь. Выведите listAssets(), сравните переданную строку с существующими строками, и исправление всегда окажется на одной из двух сторон этого сравнения.

Связанные материалы

Источники

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад