store_asset_packs: Play Asset Delivery, Background Assets and On-Demand Resources for Flutter
store_asset_packs is a Flutter plugin for store-delivered asset packs. It covers Google Play Asset Delivery (PAD) on Android, and Apple Background Assets (Apple-hosted asset packs) and On-Demand Resources (ODR) on iOS, with one Dart API and the same download statuses on both platforms.
Use it to keep large game levels, textures, audio, video, ML models or other content out of your app binary. Your Android App Bundle (AAB) and your iOS app stay small, and Google Play and the App Store host and download the extra content for you. You don't need your own CDN or download server.
| Platform | Delivery |
|---|---|
| Android | Play Asset Delivery: install-time, fast-follow and on-demand asset packs |
| iOS 26+ | Background Assets with Apple-hosted asset packs, falling back to On-Demand Resources |
| iOS 15–25 | On-Demand Resources (NSBundleResourceRequest) |
A pack is a plain folder in your project, for example assets/levels/world2/.
The same folder feeds every store, and nothing is re-encoded: once the pack is
on the device, getPackPath returns an absolute directory, and the files sit
under it at their paths relative to the folder.
Features
- Play Asset Delivery for Flutter: fast-follow and on-demand asset packs
through the Play Asset Delivery library (
AssetPackManager), including Play's Wi-Fi / cellular data confirmation dialog. - iOS Background Assets: Apple-hosted, managed asset packs on iOS 26+, with the downloader extension generated for you.
- iOS On-Demand Resources: ODR tags, initial-install and prefetch tags, and the Xcode project setup done for you.
- One API on both platforms:
getStates,fetch, a progress stream (updates), andgetPackPathto read the files. - Setup tools:
dart run store_asset_packs:androidand:ioswire the packs into Gradle (assetPacks) and Xcode (asset tags, Info.plist, entitlements, app group), and:ios packagebuilds the.aararchives for App Store Connect / TestFlight. - Fast iteration:
dart run store_asset_packs:pushcopies packs onto an emulator, a simulator or a debug device, soflutter runworks without the store. - Swift Package Manager and CocoaPods, Kotlin DSL Gradle, AGP 9.
Contents
Installation
flutter pub add store_asset_packs
Then declare your packs and run the setup tools for Android and iOS.
Dart API
import 'package:store_asset_packs/store_asset_packs.dart';
const packs = ['world2', 'world3'];
final states = await StoreAssetPacks.getStates(packs); // Map<String, AssetPackState>
StoreAssetPacks.updates.listen((s) async { // every pack, one stream
if (s.status == AssetPackStatus.waitingForWifi ||
s.status == AssetPackStatus.requiresUserConfirmation) {
await StoreAssetPacks.showCellularDataConfirmation(); // Play's dialog; true on iOS
}
});
await StoreAssetPacks.fetch(packs); // no-op for packs already on the device
final dir = await StoreAssetPacks.getPackPath('world2'); // null until available
final map = File('$dir/map_0.png');
| Member | Notes |
|---|---|
Future<Map<String, AssetPackState>> getStates(List<String> packs) |
One entry per pack asked about. Store errors (for example Play's -1 for an app not installed from Play) come back as failed with errorCode instead of being thrown. |
Future<void> fetch(List<String> packs) |
Starts downloads and returns once they're requested. Progress arrives on updates. |
Stream<AssetPackState> get updates |
Broadcast stream for every pack. |
Future<String?> getPackPath(String pack) |
Absolute directory path (not a URL), or null until the pack is completed. On iOS ODR this begins access to the tag, which lasts until release. |
Future<bool> showCellularDataConfirmation() |
Android: Play's confirmation dialog (showConfirmationDialog); returns whether the player agreed. iOS: true. |
Future<void> release(String pack) |
iOS ODR: ends access so iOS may purge the pack when it needs space. Does nothing elsewhere. |
Future<String?> getDevOverrideDirectory() |
Where debug builds look for pushed packs. |
AssetPackState has name, status, bytesDownloaded, totalBytes,
progress (0–1), errorCode and errorMessage.
AssetPackStatus values: notInstalled, pending, downloading, transferring,
waitingForWifi, requiresUserConfirmation, completed, failed, canceled,
unknown. Android maps Play's AssetPackStatus one to one. iOS uses
notInstalled, pending, downloading, completed, failed and canceled.
ODR reports progress but no byte counts.
errorCode is Play's AssetPackErrorCode
on Android (for example -6 network error, -10 insufficient storage) and the
NSError code on iOS.
Debug builds: the dev override
The stores never deliver packs to a flutter run install. So in debug builds
(kDebugMode), a pack folder at <dev dir>/<pack>/ counts as installed:
getStates reports it as completed, fetch emits completed without
calling the store, and getPackPath returns that folder. The dev directory is:
- Android:
<external files dir>/store_asset_packs_dev, i.e./sdcard/Android/data/<applicationId>/files/store_asset_packs_dev(orfilesDirif there's no external storage) - iOS:
<Documents>/store_asset_packs_dev
dart run store_asset_packs:push fills it (see push).
Release and profile builds ignore it.
Setup
1. Declare the packs
Add a store_asset_packs: section to your app's pubspec.yaml (or put the same
keys in store_asset_packs.yaml):
store_asset_packs:
packs:
world2: assets/levels/world2 # pack name → source folder
world3:
source: assets/levels/world3
android_delivery_type: on-demand # per-pack override
ios_download_policy: onDemand # per-pack override
android:
delivery_type: fast-follow # install-time | fast-follow (default) | on-demand
ios:
download_policy: prefetch # essential | prefetch (default) | onDemand
background_assets: true # also set up Apple-hosted Background Assets
# target: Runner # app target (default Runner)
# app_group: group.com.example.game # default group.<bundle id>
# extension_name: AssetPackDownloader
Rules:
- Pack names start with a letter and use only letters, digits and
_. - Each source folder must be named after its pack (
.../world2/forworld2). An iOS folder reference keeps its folder name in the bundle. - Don't list the pack folders under
flutter: assets:. That would ship them inside the app. The tools read the folders straight from disk.
Run the tools from the app's root. If you use fvm, run fvm dart run ….
2. Android
dart run store_asset_packs:android
For each pack, this:
- writes
android/<pack>/build.gradle.kts(plugincom.android.asset-pack,packName,deliveryType) and a.gitignore; - adds
include(":<pack>", …)toandroid/settings.gradle.kts; - adds
assetPacks += listOf(":<pack>", …)toandroid/app/build.gradle.kts.
Both edits sit between // store_asset_packs: begin/end markers. Run the
command again whenever the packs change. --check exits with status 1 if
anything is out of date, which is useful in CI.
Only the Kotlin DSL (.kts) is supported. Files aren't copied into git: a
syncPackAssets Gradle task copies each source folder into the module's
gitignored src/main/assets/<pack>/ before every build. They go under a
<pack>/ folder because Play requires every asset path to be unique across
all packs.
Notes:
getPackPathonly works for fast-follow and on-demand packs, which Play stores as files. Install-time packs live inside the APK (AssetManager), sogetPackPathreturns null for them.- Fast-follow packs start downloading right after the install, but call
fetchanyway as a fallback. It's a no-op for packs that are already there, and it covers packs that were evicted or whose download failed. - The Play library merges the
FOREGROUND_SERVICEandFOREGROUND_SERVICE_DATA_SYNCpermissions into your manifest. With targetSdk 34+, the Play Console asks you to declare that foreground service use (Play's pack extraction service). - The plugin ships consumer R8 rules for two release-build problems in the Play library under AGP 9: a missing annotation class, and WorkManager's Room database being stripped, which crashes the app at startup.
- Tested with AGP 9.0.1, Kotlin 2.3.20, Gradle 9.1.0, JDK 17, minSdk 24, and
android.newDsl=false/android.builtInKotlin=false.
3. iOS
The plugin needs iOS 15 and supports Swift Package Manager (CocoaPods works too).
dart run store_asset_packs:ios # odr, plus background-assets when ios.background_assets is true
dart run store_asset_packs:ios odr # On-Demand Resources only
dart run store_asset_packs:ios background-assets # Background Assets only
dart run store_asset_packs:ios package # build the Apple-hosted archives
All of these accept --check.
odr edits ios/Runner.xcodeproj:
- adds each source folder as a folder reference in a
StoreAssetPacksgroup, tagged with the pack's name (ASSET_TAGS), in the app target's Resources phase; - adds the tags to the project's
knownAssetTags; - maps
essentialpacks toON_DEMAND_RESOURCES_INITIAL_INSTALL_TAGSandprefetchpacks toON_DEMAND_RESOURCES_PREFETCH_ORDER; - sets
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = YESon Debug configurations, soflutter runinstalls carry the packs and iOS serves them locally. Release builds leave them out for the App Store to host.
Every object the tool adds has a deterministic ID starting with 5A55E7, so
reruns replace its own entries and leave everything else alone.
background-assets sets up Apple-hosted packs:
- adds
BAHasManagedAssetPacks,BAUsesAppleHostingandBAAppGroupIDtoios/Runner/Info.plist; - adds the app group to the app's entitlements, reusing
CODE_SIGN_ENTITLEMENTSif it's set and creatingRunner/Runner.entitlementsif not; - creates the downloader extension (
ios/AssetPackDownloader/, aStoreDownloaderExtension, as in Xcode's "Background Download Extension, Apple-Hosted, Managed" template) and its target. The target has an iOS 26 minimum, bundle id<app>.AssetPackDownloader, and copies the app'sDEVELOPMENT_TEAMandCODE_SIGN_STYLE. It's embedded before Flutter's "Thin Binary" phase.
The tool creates the target once and leaves it alone on later runs, so your own signing changes survive. Register the app group for both bundle ids in the Apple Developer portal, or let automatic signing do it. If you sign manually, the extension needs its own provisioning profile.
package runs xcrun ba-package for each pack and writes
build/store_asset_packs/ios/<pack>.aar and its manifest. Each archive's root
is a folder named after the pack, so the layout matches ODR. Upload the
archives to App Store Connect (Transporter or the App Store Connect API) for
each version that needs them, then attach them to the build.
At runtime the plugin picks the backend from Info.plist. On iOS 26+ with
BAHasManagedAssetPacks and BAUsesAppleHosting, it uses Background Assets.
For any pack Background Assets can't deliver, it falls back to ODR: App
Review disables Apple-hosted pre-launch downloads, and unsigned or development
builds get "No team ID was specified". A pack that Background Assets doesn't
start downloading within 20 seconds (some iOS versions accept the request and
then never begin) is served by ODR too, the same as after a failure; a pack
that is downloading, however slowly, is never timed out. That's why odr should always run, and
why ODR stays in the build even when you use Background Assets. Otherwise
(below iOS 26, or without those keys) it uses ODR.
Apple has deprecated ODR as of iOS 27. ODR keeps working where it exists, and Background Assets is the path forward.
Push packs onto a debug install
dart run store_asset_packs:push # the only connected device/simulator
dart run store_asset_packs:push -d emulator-5554 # adb serial, or a simulator UDID or name
dart run store_asset_packs:push --pack world2 # only some packs
dart run store_asset_packs:push --remove # delete them again, to test real delivery
This copies only what changed: adb push --sync on Android, and rsync into
the app container from xcrun simctl get_app_container on simulators.
- Install and run the app once first. Otherwise Android creates the folder as the shell user and the app can't read it.
- It reads the applicationId and bundle id from the project;
--app-idoverrides them. - Physical iPhones aren't supported.
Local testing
Android: bundletool local testing
flutter build appbundle --release
bundletool build-apks --bundle=build/app/outputs/bundle/release/app-release.aab \
--output=build/local.apks --local-testing
bundletool install-apks --apks=build/local.apks
With --local-testing, the packs are pushed to the device, and the Play
library "downloads" them from there, so the real pending → downloading →
transferring → completed flow runs. Fast-follow packs behave like on-demand
ones in local testing: they arrive only when you call fetch. Play's
confirmation dialogs never appear locally; use an internal testing track to
see them.
iOS: On-Demand Resources
Run the app from Xcode or with flutter run (Debug embeds the packs). Don't
push packs first, or the dev override will short-circuit the store. On the
simulator, both iOS 26 (through the fallback) and earlier versions use ODR.
iOS: Background Assets
Background Assets needs a build signed with your team. Then either:
- TestFlight: upload the
.aararchives fromios packagefor the build's version, or - Apple's mock server:
xcrun ba-serve serve build/store_asset_packs/ios/*.aar --host <your Mac's hostname>. It needs a TLS server identity for that host in your keychain that the device trusts. Point the device at the server withba-serve url-override https://<host>:<port>on the device, or with the Background Assets override in the device's Developer settings.
The example app
example/ has two packs (fast_pack fast-follow, ondemand_pack on-demand)
and an integration test that runs the whole flow against the real store
backend: flutter test integration_test -d <device>.
Store limits
All figures are current as of October 2026; check the linked pages for changes.
Google Play (size limits, compressed download):
- each asset pack: 1.5 GB;
- fast-follow and on-demand packs together: 30 GB;
- base, feature modules and install-time packs together: 4 GB;
- at most 100 asset packs per bundle; 34 GB per app in total;
- apps over 200 MB show players on mobile data a download warning. Downloads
can also stop at
waitingForWifiorrequiresUserConfirmation. Watch for both and callshowCellularDataConfirmation().
Apple-hosted asset packs (limits): 200 GB and 200 packs per app record, shared across platforms.
On-Demand Resources (limits):
| Before iOS 18 | iOS 18+ | |
|---|---|---|
| Per asset pack | 512 MB | 8 GB |
| Initial install + prefetched tags | 4 GB | no limit |
| In use at once | 2 GB | no limit |
| Hosted in total | 20 GB | 70 GB |
How it works
- Android:
com.google.android.play:asset-delivery-ktx2.3.0 (AssetPackManager:requestPackStates,requestFetch, a state listener forupdates,showConfirmationDialog(activity)andgetPackLocation).getPackPathreturns<assetsPath>/<pack>for packs stored as files. - iOS ODR: one
NSBundleResourceRequestper tag.conditionallyBeginAccessingResourceschecks whether a pack is present, andbeginAccessingResourcesdownloads it. The request is kept while the pack is in use, and the path is the folder reference's URL in the bundle. - iOS Background Assets:
AssetPackManager.shared.statusUpdatesfeedsupdates,ensureLocalAvailabilitydownloads, and the path isurl(for: "<pack>"). BackgroundAssets is weak-linked, so the app still runs on iOS 15.
FAQ
How do I use Play Asset Delivery in Flutter?
Declare the packs in pubspec.yaml, run dart run store_asset_packs:android,
build an app bundle with flutter build appbundle, and call
StoreAssetPacks.fetch and getPackPath from Dart. Google Play hosts the
packs and delivers them with the app bundle. See Android setup.
Does it support install-time, fast-follow and on-demand delivery?
Yes. All three Play Asset Delivery modes can be set per pack. getPackPath
returns a directory only for fast-follow and on-demand packs, because Play
keeps install-time packs inside the APK.
How do I use Apple Background Assets or Apple-hosted asset packs in Flutter?
Set ios.background_assets: true and run dart run store_asset_packs:ios.
That adds the Info.plist keys, the app group and the downloader extension.
Then run dart run store_asset_packs:ios package and upload the .aar
archives to App Store Connect. See iOS setup.
What about On-Demand Resources now that Apple has deprecated it? ODR still works on every iOS version that has it. The plugin uses Background Assets on iOS 26+ and ODR on earlier versions, or when Background Assets fails, so the same packs reach every supported device.
Can I test asset packs without uploading to the stores?
Yes. Use bundletool --local-testing on Android, Debug-embedded ODR on iOS,
Apple's ba-serve mock server for Background Assets, or push for any debug
build. See Local testing.
How big can an asset pack be? On Google Play, up to 1.5 GB per pack. On iOS 18+, up to 8 GB per ODR pack. See Store limits.
License
MIT. See LICENSE.
Libraries
- store_asset_packs
- Store-delivered asset packs for Flutter: Play Asset Delivery on Android, Background Assets and On-Demand Resources on iOS.