bare_flutter

bare_flutter is a small Android and iOS binding to Holepunch Bare Kit. It starts one Bare worklet and moves ordered raw bytes between Dart and the worklet.

It intentionally does not provide framing, serialization, RPC, Vite behavior, Web APIs, storage conventions, or a JavaScript compatibility layer.

Usage

Load the bundle bytes in your application and supply the logical filename Bare should see:

final data = await rootBundle.load('assets/application.bare');
final worklet = await BareWorklet.start(
  filename: '/application.bundle',
  source: data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes),
  arguments: const ['https://example.test'],
  options: const BareWorkletOptions(
    memoryLimitBytes: 64 * 1024 * 1024,
  ),
);

final subscription = worklet.ipc.incoming.listen(handleBytes);
await worklet.ipc.write(requestBytes);
final reply = await worklet.push(oneShotRequest);

await worklet.suspend();
await worklet.resume();
await worklet.terminate();
await subscription.cancel();

filename is independent of the Flutter asset name. A Bare Pack bundle must use the logical extension expected by Bare's module loader, normally .bundle. Asset loading remains the application's responsibility.

IPC contract

BareIpc is an ordered, single-subscription byte stream, not a message queue. Bytes received before the listener attaches are buffered. A write may be split across incoming chunks, and adjacent writes may be coalesced. The plugin serializes native writes and keeps retrying partial writes until every byte is accepted. It never adds length prefixes or another application protocol.

The caller owns message boundaries, framing, serialization, and RPC. A future returned by write completes only after Bare Kit accepts the entire supplied buffer. Closing IPC or terminating the worklet fails pending writes and closes the incoming stream.

push exposes Bare Kit's distinct one-shot host-to-worklet request/reply primitive. It is not used to reinterpret normal IPC as RPC.

Lifecycle and hot restart

Only one worklet can be active in a Flutter engine/process in v1. Native process-level state keeps that worklet alive through a Dart hot restart. A new start call reattaches to it and reports the same id and generation with reattached == true. Terminating and starting again creates a new id and generation.

Automatic lifecycle suspension is enabled by default with a 30-second linger. It uses native Android/iOS lifecycle notifications, so it does not depend on a Dart background timer. Returning to the foreground resumes only a worklet that the lifecycle integration suspended; a manually suspended worklet stays suspended.

Errors and exit observation

The public API maps platform failures to BareFlutterException subclasses. onExit is definitive for explicit termination and reports strong evidence from IPC EOF/errors, but it is best-effort for independent worklet exit. Bare Kit does not currently expose a perfect host callback for every exit path, and a native process crash can bypass Dart entirely.

Warning

Bare worklets run inside the application process. By default, an uncaught JavaScript exception or unhandled rejection can abort the entire app. A bundle used without a runtime shell such as vite-plugin-bare must install Bare.on('uncaughtException', ...) and Bare.on('unhandledRejection', ...) early if the host should survive application errors. bare_flutter never rewrites bundle source.

Bare Kit dependency

The native builds pin Bare Kit 2.4.3. The upstream prebuilds.zip SHA-256 is:

e152c1e186251e2fc944cb7c3e7508899d5de3acb1568e1a922e0ed96a135af3

Build integration downloads the archive on demand, verifies it before extraction, and keeps the large upstream binaries out of this repository. The package version and embedded Bare Kit version are separate and are available through BareFlutterVersionInfo.

iOS currently uses CocoaPods. Bare Kit's upstream archive contains multiple same-named platform XCFrameworks, so it cannot be used directly as a remote SwiftPM binary target; SwiftPM support is deferred until the artifact can be selected or repackaged deterministically.

Device fixture

The example contains small Android and iOS bundles built from integration_test/fixture/echo.mjs. They echo raw IPC bytes and push payloads without Vite or application RPC.

Run the real-host integration test from the example application:

cd example
flutter test integration_test/plugin_integration_test.dart -d <device-id>

For a wirelessly connected physical iPhone, use Flutter Drive so the VM service port can be published over mDNS:

flutter drive --publish-port -d <device-id> \
  --driver=test_driver/integration_test.dart \
  --target=integration_test/plugin_integration_test.dart

The test checks a 1 MiB byte-stream round trip, push, suspend/resume, and termination. Physical-device release, lifecycle, hot-restart, queued-write, and burst/stress validation remain required before describing a release as stable.

The optional Vite compatibility harness uses the existing sibling vite-plugin-bare checkout to build its real development shell, linked addon fixtures, and an offline production artifact:

BARE_FLUTTER_IOS_DEVICE=<simulator-id> node tool/validate_vite_bare.mjs

Set VITE_PLUGIN_BARE_ROOT when that repository is not at the default ../../alloc/vite-plugin-bare path. The addon pod is enabled only for this test run; neither the plugin nor its public API depends on Vite or those addons.

Compatibility boundary

bare_flutter and vite-plugin-bare have no code or package dependency. They meet only at Bare-compatible bundle bytes and raw host/worklet byte transport. bare-vite output can therefore pass directly to BareWorklet.start, while arbitrary non-Vite Bare bundles remain supported.

Android API 31+ and iOS 15+ are the v1 package targets. The Android floor comes from the official Bare Kit 2.4.3 prebuild. Desktop and web platforms are out of scope.

Libraries

bare_flutter