Experimental project:
flutter_mini_appis still evolving rapidly. The goal is to turn it into the most performance-focused, AI-native app orchestration solution for Flutter. Contributions and feedback are very welcome.
A Flutter Mini App framework that supports both native and HTML rendering.
MiniAppRuntime: unified runtime entry point for registration, lifecycle, rendering, and native schema/tool-definition exportMiniAppCatalog: factory registration, dependency validation, instance caching, and state managementMiniAppRenderer: synchronous build entry point with async boundaries handled internallyMiniAppHost: provides runtime access to the widget tree through scope- HTML cards: template versioning, schema validation, bridge communication, and session-level hot updates without rebuilding the WebView
- Realtime pipeline:
MiniAppRealtimeControllerhandles snapshot diffs, self-ack deduplication, and write-back orchestration
dependencies:
flutter_mini_app: ^0.1.0You can still use a local path: dependency during local development, but a hosted version is recommended after publishing to pub.dev.
import 'package:flutter/material.dart';
import 'package:flutter_mini_app/flutter_mini_app.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final runtime = MiniAppRuntime(
config: const MiniAppRuntimeConfig(
maxCachedInstances: 10,
idleTimeout: Duration(minutes: 5),
enablePreload: true,
enableDebugLog: false,
),
);
runtime.registerAll([
MyMiniAppFactory(),
]);
await runtime.initialize();
runApp(
MiniAppHost(
runtime: runtime,
child: const MyApp(),
),
);
}MiniAppWidget(
jsonData: {
'ID': 'my_component',
'title': 'Hello',
},
)final runtime = MiniAppHost.of(context);
final widget = runtime.buildFromJson(
{
'ID': 'my_component',
'title': 'Hello',
},
isMaximized: false,
);HtmlCardBuild: build record defined bymoduleName + cardVersion + schema + htmlHtmlTemplateRepository: template resolution and cachingHtmlMiniAppFactory: HTML factory with mapper, validation strategy, and bridge callbacksHtmlHotUpdatePayload: minimal hot-update payload with module, version, and normalized data
import 'package:flutter/services.dart';
import 'package:flutter_mini_app/flutter_mini_app.dart';
final builds = <int, HtmlCardBuild>{
1: HtmlCardBuild(
moduleName: 'todo_html',
cardVersion: 1,
schemaContent: {
r'$schema': 'https://json-schema.org/draft/2020-12/schema',
'type': 'object',
'required': ['title', 'items'],
'properties': {
'title': {'type': 'string'},
'items': {
'type': 'array',
'items': {'type': 'string'},
},
},
},
htmlObjectPath: 'assets/index.html',
),
};
final htmlFactory = HtmlMiniAppFactory(
id: 'todo_html',
defaultCardVersion: 1,
templateRepository: HtmlTemplateRepository(
buildResolver: ({required moduleName, int? cardVersion}) async {
if (moduleName != 'todo_html') return null;
return cardVersion == null ? builds[1] : builds[cardVersion];
},
objectPathLoader: (path) => rootBundle.loadString(path),
),
validateData: true,
securityPolicy: HtmlSecurityPolicy.unsafe,
);
runtime.registerHtml(htmlFactory);final htmlWidget = runtime.buildFromJson({
'ID': 'todo_html',
'cardVersion': 1,
'title': 'This Week',
'items': ['Book hotel', 'Buy tickets'],
});
final htmlWidget2 = runtime.buildFromCardBuild(
moduleName: 'todo_html',
cardVersion: 1,
data: {
'title': 'This Week',
'items': ['Book hotel', 'Buy tickets'],
},
);- Cold path: resolve template, validate, interpolate, and load the WebView
- Hot path: within the same
module + version + isMaximizedsession, only inject data throughminiapp:data-updatewithout rebuilding the WebView
Templates can listen to:
miniapp:data-readyminiapp:data-update
Symptom: business data, such as a Supabase realtime stream, is updated and Flutter card state or labels also refresh, but the HTML card content only changes after a full page refresh.
Common root causes:
- The template listens only to
miniapp:data-readyand never handlesminiapp:data-update. - The content returned by
objectPathLoaderdid not actually change, or stale HTML is still being served by external storage, a signed URL, a CDN, orHtmlTemplateRepositorycache.
Recommended order of investigation:
- Make the template listen to both
miniapp:data-readyandminiapp:data-update, then redraw the DOM fromevent.detail.dataorwindow.__CARD_DATA__. - If templates are loaded via
htmlObjectPathandobjectPathLoader, make sure the underlying file, signed URL, or CDN response has been refreshed. Updating only build metadata is not enough if the loader still resolves stale HTML. - Evict the affected template cache before retesting, for example with
htmlFactory.templateRepository.evictModule('todo_html')orclearCache().
When HTML cards are placed inside PageView or PageView.builder, off-screen pages may be disposed. In that case, the WebView inside MiniAppWidget is destroyed and recreated when the user scrolls back, which usually appears as flicker, lost state, and repeated JavaScript initialization.
Common triggers:
- The page is lazily built with
PageView.builderand the card state is not kept alive - Each page does not have a stable
Key, so Flutter may reuse or rebuild elements unpredictably
Recommended approach:
class _MiniAppCardState extends State<_MiniAppCard>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context); // Required for keepAlive
return MiniAppWidget(
jsonData: jsonData,
isMaximized: false,
);
}
}
PageView.builder(
itemCount: htmlKeys.length,
itemBuilder: (context, index) {
final key = htmlKeys[index];
return _MiniAppCard(
key: ValueKey<String>('html-$key'),
dataKey: key,
);
},
);Notes:
AutomaticKeepAliveClientMixinhelps preserve off-screen pagesValueKeygives each card page a stable identity and avoids unnecessary rebuilds- This works with the framework hot-update flow so data changes go through
miniapp:data-updateinstead of a full reload
The framework provides three abstractions and one controller so application code can adapt different backends such as Supabase, Firebase, or custom services:
MiniAppRealtimeRecord: normalized record model withkey/appId/json/updatedAtMiniAppRealtimeSource: produces aStream<List<R>>snapshot streamMiniAppWriteSink: exposespatch/deletewrite capabilitiesMiniAppRealtimeController: handles diffing, write-back deduplication, and self-ack suppression
final controller = MiniAppRealtimeController<MyRealtimeRecord>(
repository: runtime.data,
source: myRealtimeSource,
sink: myWriteSink,
onSnapshotReport: (report) {
// report.kind: externalUpdate / selfAck / noOp
},
);
await controller.start();
await controller.patch(
key: 'card-1',
patch: {'title': 'Updated title'},
);
await controller.delete(key: 'card-1');
await controller.dispose();MiniAppRealtimeController.patch(...) returns:
writtennoRecordnoSemanticChangeduplicatePending
final repo = runtime.data;
repo.put('card-1', {
'ID': 'todo_html',
'title': 'Travel Checklist',
});
final json = repo.get('card-1');
repo.patch('card-1', {'title': 'Updated title'});final errors = await runtime.validateJson({
'ID': 'my_component',
'title': 'hello',
});
final schemasJson = runtime.exportSchemasAsJson();
final toolDefs = runtime.generateToolDefinitions();
final toolDefsJson = runtime.generateToolDefinitionsJson();Notes:
exportSchemasAsJson(),generateToolDefinitions(), andvalidateJson()operate on schemas exposed by registered nativeMiniAppFactoryinstances.- HTML build schemas are resolved separately through
validateDataWithBuild(...). validateDataWithBuild(...)validates the data you pass to it directly. During actual HTML rendering, the framework validates normalized data afterHtmlMiniAppFactory.dataMapperis applied.
| API | Description |
|---|---|
register/registerAll |
Register native factories |
registerHtml/registerHtmlAll |
Register HTML factories |
initialize() |
Initialize the runtime |
getApp(id) |
Get an initialized native instance |
buildFromJson(...) |
Synchronous build entry point with automatic native or HTML dispatch |
buildFromDataKey(...) |
Build from a data repository key |
buildFromCardBuild(...) |
Build an HTML card from a specific module and version |
describeHtmlHotUpdate(...) |
Produce an HTML hot-update descriptor |
validateDataWithBuild(...) |
Validate input data against a specific HTML build version |
validateJson(...) |
Validate native schema data |
stats |
Runtime statistics |
| API | Description |
|---|---|
MiniAppRealtimeRecord |
Standard realtime record interface with key/appId/json/updatedAt |
MiniAppRealtimeSource.watch() |
Produce snapshot list streams |
MiniAppWriteSink.patch/delete |
Application write-back interface |
MiniAppRealtimeController.start/stop/dispose |
Lifecycle control |
MiniAppRealtimeController.patch(...) |
Write back after semantic deduplication and return the patch result |
MiniAppRealtimeController.delete(...) |
Execute delete write-back |
| API | Description |
|---|---|
MiniAppHost(runtime, child) |
Inject the runtime into scope |
MiniAppHost.of(context) |
Read the runtime from scope |
The current release is based on the Runtime architecture:
MiniAppRegistryandMiniAppRegistryConfigwere removed- Async
render*entry points were replaced by synchronousbuild*entry points MiniAppHostis the recommended way to provide the runtime at the app root