Dynamic Asset Manager (OtaAssetManager)
The native C++/Rust engine applies differential binary patches to compiled Dart machine code (application binary on Android, patch artifact on iOS), but cannot rewrite the read-only flutter_assets/ archive packaged inside the original APK or IPA bundle distributed via the App Stores.
To deliver hot updates for static assets (JSON i18n translation catalogs, promotional banners, SVG icons, fonts, or localized configurations) without waiting for App Store reviews, the SDK provides OtaAssetManager (lib/src/assets/ota_asset_manager.dart).
Architecture and Workflow
- Local Cache Check: SDK checks whether the asset already exists in local device storage.
- Integrity Validation: The local file hash is verified against the expected digest.
- Download on Demand: Missing or updated assets are downloaded securely and cached.
- Widget Availability: Assets become instantly accessible to Flutter UI widgets.
1. Defining a Remote Asset (OtaRemoteAssetDescriptor)
Every downloadable asset is represented by an immutable instance of OtaRemoteAssetDescriptor:
const descriptor = OtaRemoteAssetDescriptor(
assetKey: 'assets/i18n/fr_CM.json',
downloadUrl: 'https://ota.saramoney.internal/assets/fr_CM_v2.json',
expectedSha256: '99e887f4f4b6bf9d3abb767d3b5f215e0009e4d26cb7b958497e8b291e1b96dc',
);
| Field | Type | Description |
|---|---|---|
assetKey | String | Logical asset key (e.g., assets/images/promo.png or assets/i18n/fr_CM.json). |
downloadUrl | String | Full HTTP/HTTPS download URL on the on-premise backend. |
expectedSha256 | String | Expected lowercase hexadecimal SHA-256 digest (case-insensitive). |
2. Downloading, Verifying, and Caching (OtaAssetManager)
Initialization
OtaAssetManager accepts a Directory cacheDirectory (where verified files will reside) and an optional http.Client? httpClient (useful for configuring custom timeouts, certificate pinning, or a MockClient during unit testing):
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:tk_ota_push/tk_ota_push.dart';
final cacheDir = Directory(p.join(Directory.systemTemp.path, 'ota_assets_cache'));
final assetManager = OtaAssetManager(cacheDirectory: cacheDir);
On-Disk Cache Structure (cachedFileFor)
cachedFileFor(OtaRemoteAssetDescriptor descriptor) constructs a deterministic disk path isolated by SHA-256 fingerprint:
- Sanitizes
assetKeyby substituting any character outside[a-zA-Z0-9._/-]with_(safeKey). - Normalizes
expectedSha256to lowercase (cleanHash). - Stores the file under:
<cacheDirectory.path>/<cleanHash>/<safeKey>.
When you publish a newer asset revision with an updated expectedSha256, its cache path changes deterministically, avoiding stale cache hazards.
Fetching with SHA-256 Verification (getOrDownloadAsset)
try {
final File localAssetFile = await assetManager.getOrDownloadAsset(
descriptor,
headers: const {'Accept': 'application/json'},
);
final String jsonContent = await localAssetFile.readAsString();
// Consume verified local asset
} on OtaAssetIntegrityException catch (e) {
// Received SHA-256 digest does not match expectedSha256
debugPrint('Asset integrity alert for ${e.assetKey}: expected=${e.expectedSha256}, received=${e.actualSha256}');
} on HttpException catch (e) {
// Remote server responded with non-200 HTTP status
debugPrint('Network error downloading asset: $e');
}
Detailed Behavior of getOrDownloadAsset
- Local Cache Inspection:
- If
targetFile.existsSync()is true,getOrDownloadAssetreads local file bytes and recalculates thesha256digest. - If the digest matches
expectedSha256, the file is immediately returned (0 network calls). - If the cached file was tampered with on flash storage (
cachedSha != expected), it is deleted (targetFile.deleteSync()) and refetched.
- If
- HTTP Fetch:
- Executes an HTTP
GETagainstUri.parse(descriptor.downloadUrl)with supplied headers. - If
response.statusCode != 200, throwsHttpExceptionreporting the status code and URI.
- Executes an HTTP
- Cryptographic SHA-256 Validation:
- Computes
sha256.convert(response.bodyBytes). - If
actualSha != expected, throwsOtaAssetIntegrityExceptionbefore writing any bytes to persistent storage.
- Computes
- Atomic Disk Persistence:
- Creates the parent directory hierarchy (
targetFile.parent.createSync(recursive: true)), writes the verified byte buffer, and returns theFile.
- Creates the parent directory hierarchy (
3. Embedded Assets Consistency Safeguard (computeAssetsManifestHash)
In addition to OtaAssetManager, the tk-ota-push CLI computes a canonical SHA-256 hash of all files declared in flutter.assets inside pubspec.yaml via FlutterProject.computeAssetsManifestHash() (lib/src/cli/project.dart).
This hash is transmitted to the TkushPatch backend under assetsManifestHash (assets_manifest_hash) to track asset changes between a baseline Store Release and its subsequent hot patches.