Skip to main content

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​

Dynamic Asset Loading Process
  1. Local Cache Check: SDK checks whether the asset already exists in local device storage.
  2. Integrity Validation: The local file hash is verified against the expected digest.
  3. Download on Demand: Missing or updated assets are downloaded securely and cached.
  4. 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',
);
FieldTypeDescription
assetKeyStringLogical asset key (e.g., assets/images/promo.png or assets/i18n/fr_CM.json).
downloadUrlStringFull HTTP/HTTPS download URL on the on-premise backend.
expectedSha256StringExpected 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:

  1. Sanitizes assetKey by substituting any character outside [a-zA-Z0-9._/-] with _ (safeKey).
  2. Normalizes expectedSha256 to lowercase (cleanHash).
  3. 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​

  1. Local Cache Inspection:
    • If targetFile.existsSync() is true, getOrDownloadAsset reads local file bytes and recalculates the sha256 digest.
    • 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.
  2. HTTP Fetch:
    • Executes an HTTP GET against Uri.parse(descriptor.downloadUrl) with supplied headers.
    • If response.statusCode != 200, throws HttpException reporting the status code and URI.
  3. Cryptographic SHA-256 Validation:
    • Computes sha256.convert(response.bodyBytes).
    • If actualSha != expected, throws OtaAssetIntegrityException before writing any bytes to persistent storage.
  4. Atomic Disk Persistence:
    • Creates the parent directory hierarchy (targetFile.parent.createSync(recursive: true)), writes the verified byte buffer, and returns the File.

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.