mini_program_ui 0.2.3
mini_program_ui: ^0.2.3 copied to clipboard
Pure-Dart authoring helpers for versioned Mp JSON mini-program screens.
mini_program_ui #
Pure-Dart authoring helpers for the Mp JSON mini-program engine.
This package intentionally has no Flutter, Stac, Material, Cupertino,
analyzer, or build_runner dependency. Mini-program authors write Mp.*
source, then mini_program_tooling runs tool/build_mp.dart and writes
versioned JSON for the SDK renderer.
Supported Import #
Mini-program source should use only the package barrel:
import 'package:mini_program_ui/mini_program_ui.dart';
Files below lib/src/ are implementation details. The implementation is
organized into dependency-free core values, program assembly, and
feature-owned node/action builders. The temporary legacy src compatibility
re-exports were removed in 0.2.0; direct internal imports are unsupported.
Program Shape #
import 'package:mini_program_ui/mini_program_ui.dart';
final miniProgram = MpProgram(
screens: <String, MpScreenBuilder>{
'coupon_home': buildCouponHome,
'coupon_details': buildCouponDetails,
},
);
The build script is small and deterministic:
import 'package:mini_program_ui/mini_program_ui.dart';
import '../mp/program.dart';
Future<void> main(List<String> arguments) {
return writeMpBuildOutput(miniProgram, arguments: arguments);
}
Basic UI #
MpNode buildCouponHome() {
return Mp.column(
children: <MpNode>[
Mp.heading('Coupon Center'),
Mp.text('Portable rewards for host apps.'),
Mp.image(src: 'https://example.com/reward.png'),
Mp.card(child: Mp.text('SDK-owned component styling')),
Mp.primaryButton(
label: 'Open details',
action: Mp.navigation.openScreen('coupon_details'),
),
],
);
}
Auth #
Mp.authBuilder(
loading: Mp.text('Checking session...'),
signedOut: Mp.card(
child: Mp.column(
children: <MpNode>[
Mp.text('Sign in to continue.'),
Mp.primaryButton(
label: 'Sign in with email',
action: Mp.auth.showEmailAuth(),
),
],
),
),
signedIn: Mp.card(
child: Mp.column(
children: <MpNode>[
Mp.text('Signed in as {{auth.user.email}}'),
Mp.secondaryButton(label: 'Sign out', action: Mp.auth.signOut()),
],
),
),
error: Mp.text('{{auth.message}}'),
);
Auth bindings never expose idToken, refreshToken, passwords, or backend
secrets.
Backend Data #
Mp.backendBuilder(
requestId: 'home',
endpoint: 'home/bootstrap',
loading: Mp.text('Loading...'),
error: Mp.text('{{backend.home.message}}'),
child: Mp.card(
child: Mp.column(
children: <MpNode>[
Mp.heading('{{backend.home.data.title}}'),
Mp.text('{{backend.home.data.message}}'),
],
),
),
);
Artifact-Local Data And Visualization #
Large immutable lookup data can stay under the mini-program's assets/
directory. The runtime validates it, persists it only in the host-approved
data cache, and keeps search indexes outside live state.
Mp.initialize(
actions: <MpAction>[
Mp.data.loadJsonAsset(
id: 'locations',
asset: 'data/locations.json',
statusState: 'location.resource_status',
errorState: 'location.resource_error',
),
],
child: Mp.column(
children: <MpNode>[
Mp.searchField(
stateKey: 'location.query',
hint: 'Dhaka',
onChanged: Mp.data.search(
resourceId: 'locations',
query: '{{state.location.query}}',
fields: const <String>['name', 'district'],
itemsPath: 'locations',
targetState: 'location.results',
),
),
Mp.lineChart(
source: '{{state.forecast.hourly}}',
valueField: 'temperature',
labelField: 'timeLabel',
),
],
),
);
JSON data paths are artifact-relative and cannot be remote URLs. Use
direction: 'horizontal' with an explicit height on Mp.listView or
Mp.repeat for horizontal collections. Mp.refreshIndicator is supported
only as a screen root.
Local Text Editing #
Use a state-bound field for local drafts and editors that are not submitted as a backend form. The SDK keeps its controller synchronized with live state, and the host's live-state limits still apply.
Mp.stateTextField(
stateKey: 'note.body',
hint: 'Enter text...',
maxLength: 4096,
minLines: 8,
maxLines: 30,
keyboardType: 'multiline',
textInputAction: 'newline',
);
Wrap custom visual content with Mp.tap when it needs one semantic action
without adopting button presentation.
Paged Lists #
Use Mp.lazy.chunk when repeated data is large, dynamic, comes from a
Publisher API, and needs pagination or manual Load more:
Mp.lazy.chunk(
id: 'rewards_chunk',
itemsState: 'rewards.items',
cursorState: 'rewards.next_cursor',
hasMoreState: 'rewards.has_more',
statusState: 'rewards.status',
cacheKeyPrefix: 'rewards_chunk',
placeholder: Mp.text('Loading rewards...'),
loadingMore: Mp.text('Loading more...'),
empty: Mp.text('No rewards yet.'),
end: Mp.text('No more rewards.'),
error: Mp.text('Rewards failed to load.'),
itemTemplate: Mp.card(child: Mp.text('{{item.title}}')),
initialActions: <MpAction>[
Mp.backend.loadMore(
requestId: 'rewards',
endpoint: 'coupons/page',
limit: 20,
),
],
loadMoreActions: <MpAction>[
Mp.backend.loadMore(
requestId: 'rewards',
endpoint: 'coupons/page',
limit: 20,
),
],
loadMore: Mp.secondaryButton(
label: 'Load more',
action: Mp.lazy.loadMore(id: 'rewards_chunk'),
),
);
Do not use Mp.lazy.chunk for login pages, small settings pages, static about
pages, single detail pages, payment forms, fixed menus, or small local JSON
lists.
Mp.pagedBackendBuilder remains available for direct backend-bound paged
lists:
Mp.pagedBackendBuilder(
requestId: 'rewards',
endpoint: 'coupons/page',
limit: 20,
loading: Mp.text('Loading rewards...'),
loadingMore: Mp.text('Loading more...'),
empty: Mp.text('No rewards yet.'),
end: Mp.text('No more rewards.'),
error: Mp.text('{{backend.rewards.message}}'),
itemTemplate: Mp.card(
child: Mp.column(
children: <MpNode>[
Mp.heading('{{item.title}}'),
Mp.text('{{item.description}}'),
],
),
),
loadMore: Mp.secondaryButton(
label: 'Load more',
action: Mp.backend.loadMore(requestId: 'rewards'),
),
);
Default provider-neutral response shape:
{
"items": [],
"nextCursor": null,
"hasMore": false
}
Navigation #
Mp.primaryButton(
label: 'Open details',
action: Mp.navigation.openScreen('coupon_details'),
);
Mp.secondaryButton(
label: 'Back',
action: Mp.navigation.popScreen(),
);
Control Flow And Timers #
Conditions are strict booleans or full bindings. Countdown state contains the remaining whole seconds, rounded up.
Mp.timer.countdown(
duration: const Duration(seconds: 10),
running: '{{state.screen.running}}',
restartToken: '{{state.screen.content_id}}',
remainingState: 'screen.remaining_seconds',
onComplete: Mp.action.ifElse(
condition: '{{state.screen.can_advance}}',
thenAction: Mp.state.set('screen.status', 'advanced'),
elseAction: Mp.state.set('screen.status', 'expired'),
),
child: Mp.condition(
condition: '{{state.screen.ready}}',
whenTrue: Mp.text('{{state.screen.remaining_seconds}} seconds'),
whenFalse: Mp.text('Waiting'),
),
);
Setting running to false pauses the countdown. Changing restartToken
resets it to the configured duration. Timers are cancelled when their node is
disposed.
Current Location #
Mini-programs can request one foreground, approximate location through a host provider. The host must separately accept the request and install the native provider.
Mp.location.getCurrent(
accuracy: 'approximate',
timeout: const Duration(seconds: 10),
targetState: 'location.current',
statusState: 'location.status',
errorState: 'location.error',
requestId: 'current-location',
);
This API does not support background tracking, continuous updates, or precise location.
Camera And Flashlight #
Camera capture delegates to the Android system camera. Bounds are optional; when omitted, the camera's native output size is retained. The host returns an opaque media reference and metadata rather than a path, URI, or image bytes.
Mp.camera.capturePhoto(
quality: 95,
maxWidth: 1920,
targetState: 'camera.photo',
statusState: 'camera.status',
errorState: 'camera.error',
);
Mp.camera.cancel(statusState: 'camera.status');
Mp.flashlight.toggle(
targetState: 'flashlight.status',
errorState: 'flashlight.error',
);
Camera and flashlight are separate host permissions. Camera does not provide a live camera feed, video recording, or arbitrary camera access. A captured photo can be rendered through the trusted host media provider without exposing its path or bytes to mini-program state:
Mp.image(
src: '{{state.camera.photo.mediaRef}}',
source: MpImageSource.hostMedia,
alt: 'Captured photo',
);
Publisher File Transfers #
File actions use relative routes on the artifact-declared Publisher API. The host must accept file policy and install a platform transfer provider. Files are streamed by the host; mini-program state receives only progress and sanitized result metadata.
Mp.file.upload(
endpoint: 'files/upload',
mimeTypes: const <String>['image/*', 'application/pdf'],
mediaRefs: const <String>['{{state.camera.photo.mediaRef}}'],
multiple: true,
metadata: const <String, Object?>{'folderId': 'inbox'},
progressState: 'files.progress',
targetState: 'files.uploadResult',
statusState: 'files.status',
errorState: 'files.error',
);
Mp.file.download(
endpoint: 'files/download',
request: const <String, Object?>{'fileId': 'file-1'},
destination: 'downloads',
suggestedName: 'report.pdf',
expectedMimeType: 'application/pdf',
progressState: 'files.progress',
targetState: 'files.downloadResult',
);
Mp.file.cancel(
transferId: '{{state.files.progress.transferId}}',
statusState: 'files.status',
);
Mp.media.release(
mediaRef: '{{state.camera.photo.mediaRef}}',
statusState: 'camera.status',
);
Upload and download are network operations, not aliases for local pick/save.
The publisher server owns file IDs, folders, ACLs, and business metadata.
When mediaRefs is empty, upload opens the platform document picker. When it
is present, the host streams already-owned temporary media. Release media only
after upload succeeds or when the user discards it; host lifecycle cleanup is
the final fallback.
Security Model #
mini_program_ui only serializes declarative JSON. It does not execute host
code and it does not contain renderer logic. Runtime validation,
auth sessions, runtime API headers, and bridge dispatch are owned by
mini_program_sdk.