In Flutter, what do AssetBundle's load, loadString and loadStructuredData return, and which of their results does rootBundle cache?
answer
- ByteData, String, parsed T
- bytes are never cached
- strings cached for the bundle's lifetime
- over 50 KB: decoded on another isolate
- parser runs once; failures retried
basics
~20 sload returns ByteData and is never cached; loadString returns a UTF-8 String that rootBundle caches for the bundle's lifetime unless cache: false; loadStructuredData runs your parser once per key and caches the parsed result, but not failures.
solid answer
~40 s`load(key)` returns `Future<ByteData>`, the raw bytes, and a `CachingAssetBundle` such as `rootBundle` does not cache it. `loadString(key, {cache = true})` decodes UTF-8 and caches the resulting future for the bundle's lifetime, which is usually the app's; strings of 50 KB or more are decoded with `compute` on another isolate, except on the web. `loadStructuredData<T>(key, parser)` reads the string uncached, runs your async parser once, and caches the parsed `T`; later calls get a `SynchronousFuture`, and a failed load or parse is not cached, so the next call retries. `evict(key)` and `clear()` drop cached entries. For a large question pack I parse it with `loadStructuredData`, or pass `cache: false`, so the raw string does not stay in memory.
code
dart · 22 linesimport 'package:flutter/services.dart';
class Question {
const Question(this.prompt, this.answers, this.correctIndex);
final String prompt;
final List<String> answers;
final int correctIndex;
}
typedef PackParser = Future<List<Question>> Function(String json);
class QuestionPackRepository {
QuestionPackRepository(this._bundle, this._parse);
final AssetBundle _bundle;
final PackParser _parse;
Future<List<Question>> pack(String category) =>
_bundle.loadStructuredData('assets/questions/$category.json', _parse);
void forget(String category) => _bundle.evict('assets/questions/$category.json');
}go deeper
Know which method returns bytes and which returns text, and that both are asynchronous and fail for undeclared keys.
Explain rootBundle's caching rules: bytes never, strings for the session, parsed structured data once per key, and the 50 KB isolate threshold.
Control memory for large bundled data: pick loadStructuredData or cache: false, evict when finished, and keep binary assets out of repeated loads.
Set guidance on how much content ships as assets and how it is loaded, so memory and startup stay predictable as content grows.
## The methods on AssetBundle `AssetBundle` has one abstract method and several helpers built on it: | Method | Returns | Cached by `CachingAssetBundle` (`rootBundle`) | |---|---|---| | `load(key)` | `Future<ByteData>` | **no** | | `loadBuffer(key)` | `Future<ImmutableBuffer>` | no | | `loadString(key, {cache = true})` | `Future<String>` | yes, unless `cache: false` | | `loadStructuredData<T>(key, parser)` | `Future<T>` | the parsed `T`, per key | | `loadStructuredBinaryData<T>(key, parser)` | `Future<T>` | the parsed `T`, per key | Every method throws if the key is not in the bundle. ## load: raw bytes, never cached `load` returns a `ByteData`, a view over bytes. `Uint8List.sublistView(data)` turns it into a byte list for APIs that want one. The framework states it outright: **binary resources from `load` are not cached**, so calling `load('assets/sounds/effects/correct.mp3')` for every correct answer fetches the bytes every time. Keep the bytes yourself if you need them repeatedly, or give your audio plugin the asset path and let it manage caching. `loadBuffer` returns an `ImmutableBuffer`, the form the image decoder consumes; `rootBundle` creates it directly from the asset with `ImmutableBuffer.fromAsset`. Image widgets use it; application code rarely needs to. ## loadString: decoded text, cached `loadString` decodes the bytes as **UTF-8**. Two details matter for large data files: 1. **Isolate decoding.** Decoding a string of 50 KB or more is handed to `compute`, so it runs on another isolate and does not jank the UI; on the web, where that is not available, it decodes inline. 2. **Caching.** `CachingAssetBundle` stores the `Future<String>` per key, and the cache lives as long as the bundle, which for `rootBundle` means the whole app session. A 3 MB question pack loaded with `loadString` therefore stays in memory until the app exits, even after you have parsed it. Pass `cache: false` when you will keep your own parsed copy, or call `rootBundle.evict(key)` once you are done. ## loadStructuredData: parse once `loadStructuredData<T>(key, parser)` is designed for "read text, turn it into a model": - It reads the string with `cache: false`, so the raw text is **not** cached. - It runs `parser` (a `Future<T> Function(String)`) the **first** time for a key and caches the resulting `T`. - Later calls return a `SynchronousFuture<T>`, which completes immediately. - If loading or parsing fails, the entry is removed, so the **next call tries again**. For a trivia game this is a good fit: parse `assets/questions/history.json` into a list of question objects once, and every later screen that asks for the pack gets the same parsed list instantly. The parsing itself, turning JSON text into objects, is `dart:convert`'s or a generated model's job. `loadStructuredBinaryData` does the same for a parser that takes `ByteData`. ## Evicting and clearing - `evict(key)` drops the cached string, structured result and binary structured result for one key. - `clear()` drops all of them. The base `AssetBundle` implements both as no-ops; they only matter for caching bundles like `rootBundle` or your own `CachingAssetBundle` subclass. ## Errors All of these methods complete with an error when the key is missing. `rootBundle` reports a `FlutterError` whose summary is `Unable to load asset: "<key>".` and whose description is `The asset does not exist or has empty data.` Two consequences: - A **zero-byte** asset fails in the same way as a missing one, so an accidentally empty question pack looks like a missing file. - Because a failed `loadStructuredData` is not cached, a screen can offer a retry, and the next call really loads again. ## Memory at a glance | Asset | Loaded with | Stays in memory | |---|---|---| | 3 MB question pack | `loadString` | the string, for the session | | 3 MB question pack | `loadStructuredData` | only the parsed model | | 3 MB question pack | `loadString(cache: false)` | only what your code keeps | | 200 KB sound | `load` | only what your code keeps | ## Choosing a method for each asset 1. Small text you read once: `loadString`. 2. Large JSON you turn into a model: `loadStructuredData`, or `loadString(cache: false)` in a repository that keeps the model. 3. Audio, fonts you load yourself, or other binary data: `load`, holding the bytes if needed repeatedly. 4. Images: neither; use `Image.asset`, which loads through the image cache.
- What does a caller get from loadStructuredData on the second call for the same key?A `SynchronousFuture<T>` wrapping the cached parsed value, so `then` callbacks run immediately without waiting for the event loop. The parser does not run again. If the first attempt failed, nothing was cached, and the second call loads and parses afresh.
- Why might loading a 3 MB JSON pack with loadString raise the app's steady memory use?`rootBundle` caches the decoded string for the bundle's lifetime, the whole session, so the raw text stays in memory alongside whatever model you parsed from it. Use `loadStructuredData`, which caches only the parsed result, or `loadString(key, cache: false)`, or `evict(key)` after parsing.
saying these in an interview costs you the question
- rootBundle caches the ByteData returned by load().
- loadString always decodes on the UI isolate, however large the file.
- loadStructuredData re-runs the parser on every call.
- A failed loadStructuredData call is cached, so later calls fail too.
- The string cache is cleared automatically when memory runs low.