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.