freight 0.1.1
freight: ^0.1.1 copied to clipboard
Ship large assets outside your app bundle: iOS Managed Background Assets and Google Play Asset Delivery behind one Dart API.
freight #
Ship large assets outside your app bundle — iOS Managed Background Assets and Google Play Asset Delivery behind one Dart API.
Status: early development. Published, but the API is not stable — expect it to move before 1.0. See PLAN.md for the design and release plan.
iOS 26+ only.
freighttargets Managed Background Assets, introduced in iOS 26. Apps supporting iOS 25 and earlier need the legacyBADownloaderExtensionpath, which this package deliberately does not implement.iOS is proven end to end, on an iPhone: a pack declared in
freight.yaml, packaged withba-package, downloaded by the device, read back throughFreight.read, and drawn from the pack withFreightImage.Android is not. The runtime reads and downloads Play Asset Delivery packs through the same API, and
freight_cligenerates the Gradle asset pack modules — an app bundle built from the example carries both packs with the right paths and delivery types — but none of it has run against Play services.
The problem #
Games, offline maps and on-device ML models do not fit in an app bundle. Both stores solve this and neither is reachable from Flutter: iOS 26 replaced the old downloader-extension dance with declarative asset packs, and Android has had Play Asset Delivery for years. As of iOS 26 the two work the same way — declare a pack, let the store host it, request it at runtime — which is what makes one API possible.
Install #
dependencies:
freight: ^0.1.0
dev_dependencies:
freight_cli: ^0.1.0
freight_cli builds the packs and sets the project up; it is a dev dependency
so that nothing an app ships carries a YAML parser it never runs.
Usage #
Declare packs once:
# freight.yaml
packs:
tutorial:
delivery: essential # downloaded during install
root: assets/tutorial # logical paths are relative to this
files: ["**"]
maps_europe:
delivery: onDemand # downloaded when you ask
root: assets/maps/europe
files: ["**"]
root decides what the app reads back: a file at <root>/berlin.tiles is read
as berlin.tiles, wherever the sources sit in your repository.
Build them with freight_cli, a separate dev dependency so
this package carries no build-time tooling of its own:
dev_dependencies:
freight_cli: ^0.1.0
dart run freight_cli:freight build
That resolves each pack's globs, generates the manifests and packages them with
Apple's ba-package. Add --base-url https://cdn.example.com/packs to also
write the download manifest a self-hosting server needs.
Set the platform projects up once, and check them:
dart run freight_cli:freight setup
dart run freight_cli:freight doctor
setup adds the Background Assets downloader extension on iOS and a Gradle
asset pack module per pack on Android. doctor then looks for the mistakes both
stores otherwise report late — iOS by crashing on a device, Android by failing a
bundle build.
Then, at runtime:
final pack = Freight.pack('maps_europe');
await pack.ensureDownloaded();
pack.watch().listen((status) {
if (status case PackDownloading(:final fraction)) {
print('${(fraction * 100).round()}%');
}
});
Asset packs are a virtual filesystem, not a folder — you address logical paths:
final bytes = await Freight.read('maps/berlin.mbtiles');
final file = await Freight.resolve('maps/berlin.mbtiles');
A pack's logical path is exactly what an asset key is, so downloaded assets can behave like ordinary Flutter assets:
Image(image: FreightImage('maps/pin.png'))
Install FreightBundle and widgets that take an asset key work against packs
without knowing it. Keys no pack contains fall through to the app's own assets,
so one bundle serves both:
DefaultAssetBundle(
bundle: Freight.bundle(),
child: const MapScreen(),
)
Delivery policies #
delivery |
iOS | Android | Behaviour |
|---|---|---|---|
essential |
essential | install-time | Downloaded during install; app cannot open until done |
prefetch |
prefetch | fast-follow | Starts during install, may finish afterwards |
onDemand |
onDemand | on-demand | Never automatic — the app requests it |
iOS setup #
Managed Background Assets needs an app group and an embedded downloader
extension, and the system crashes rather than returning an error when either
is missing. freight checks both first and throws MissingAppGroupException or
MissingExtensionException instead, but the app still has to provide them:
- File → New → Target → Background Download Extension in Xcode, choosing one of the Managed options. The template writes the whole extension — four lines of Swift.
- Add the App Groups capability to the app target with the same group, and
set
BAAppGroupIDin the app'sInfo.plist.
The setup guide walks through all of it, including
building packs with ba-package, self-hosting, and what each failure message
actually means.
Note that Background Assets does not work on the iOS Simulator — it needs a real signing identity. Test asset packs on a device.
Android setup #
freight setup generates a Gradle asset pack module per pack, declares the
com.android.asset-pack plugin, includes each module in settings.gradle.kts
and lists them on the app. freight build then stages each pack's files into
its module, which is the only place Play reads them from.
Asset packs only exist in an app bundle, so flutter build appbundle is what
carries them — an APK has none.
Two differences from iOS are worth knowing rather than discovering:
Freight.allPackslists only packs already on the device. Play has no API for the ones an app merely declares.requireLatestdoes nothing. Play versions asset packs with the app, so there is never a newer one for the installed build.
Requirements #
- Flutter 3.29+, Dart 3.7+
- iOS 26.0+ (26.4+ recommended; the 26.0 APIs are already deprecated by Apple)
- For building iOS packs: Xcode 26, or the Managed Background Assets developer tools for Linux. The Android half needs neither.
License #
MIT