<!-- Skal docs — markdown mirror of https://skal.run/docs/native.html -->

# Wrapping pub.dev packages

The point of rendering with Flutter isn't just pixels — it's [pub.dev](https://pub.dev): tens of thousands of maintained native plugins. Skal's codegen turns any Flutter widget into a JSX component and any plugin API into an awaitable JS call. No platform channels, no per-plugin JS glue.

## Widgets — one line of config

List the package in your app's `skal_codegen.yaml`:

```jsx
# flutter-host/lib/skal_codegen.yaml
packages:
  - qr_flutter
  - flutter_map
```

Then run the generator:

```jsx
$ bun run codegen
```

The builder introspects each package's public widgets with Dart's analyzer — constructors, named constructors, parameter types — and emits adapters plus a manifest. On the JS side, typed components appear under `skal-flutter`:

```jsx
import { QrImageView, FlutterMap, TileLayer } from 'skal-flutter';

<QrImageView data="https://skal.dev" size={220} />
```

Named constructors surface as their own components — shimmer's `Shimmer.fromColors` becomes `<ShimmerFromColors>`. Prop types round-trip: enums, colors, durations, edge insets, gradients, text styles, value classes, even `List<Marker>`\-style value children as JSX children.

## Headless capabilities — services

Most native capabilities have no widget: geolocation, biometrics, share, clipboard, permissions. Declare a **service** and the class's static methods become awaitable RPCs; a `$`\-suffixed call subscribes a Dart `Stream` and returns an unsubscribe function:

```jsx
# skal_codegen.yaml
services:
  geo:
    package: geolocator
    class: Geolocator
```

```jsx
import { createSkalService } from 'skal/runtime';

const geo = createSkalService('geo');
const pos = await geo.getCurrentPosition();
const stop = geo.getPositionStream$((p) => setPos(p));
```

geolocator wraps with **zero hand-written Dart** — its API is already static. A plugin with an instance-shaped API (local\_auth) needs one tiny static forwarder class, and that forwarder is walked by codegen like any package:

```jsx
// flutter-host/lib/adapters/auth_service.dart — the ENTIRE integration
class AuthService {
  static final _auth = LocalAuthentication();
  static Future<bool> authenticate(String reason) =>
      _auth.authenticate(localizedReason: reason);
}
```

And services aren't just plugin wrappers — they're the general "run this in Dart, call it from JS" hatch for _your own_ logic. Crypto is the canonical case: native-speed hashing, heavy work on a background isolate, keys and file bytes that never enter JS (big inputs cross as **paths**, never payloads):

```jsx
// flutter-host/lib/adapters/crypto_service.dart   deps: crypto: ^3.0.0
class CryptoService {
  static String sha256Hex(String input) =>
      sha256.convert(utf8.encode(input)).toString();
  static Future<String> sha256File(String path) => Isolate.run(
      () async => (await sha256.bind(File(path).openRead()).last).toString());
}
```

## Stateful widgets — hosts

Widgets whose constructor needs a live controller (camera preview, webview, video) use the **host pattern**: you write a ~15-line factory function; its parameters become JSX props, and the controller's public methods become imperative calls on a ref — one RPC op out, the reply back through the reply heap:

```jsx
import { createSkalRef } from 'skal/runtime';

const cam = createSkalRef();
<Camera ref={cam} cameraName="back" />
await cam.takePicture();
cam.frames$((f) => ...);   // Dart Streams subscribe with $
```

## The skip report

After every run, anything codegen could not map is listed in `lib/skal_codegen.json` under `"skipped"` — with the reason and the exact remedy. Never diff generated output to find out what's missing; read the report. Many skips are rescued with zero Dart via `overrides:` in the same yaml — pin a generic widget's type arguments, supply a `const:` expression for one unmappable parameter, mark an indexed builder with `builder: true`, or hold a controller as an opaque JS-side handle with `handle: true`.

## The escape-hatch ladder

When codegen can't map a library, escalate one rung at a time — every rung is a shipped pattern, and the line counts below are from real plugins:

1.  **`overrides:` in the yaml** — one bad param, generic type args. _0 lines of Dart._
2.  **Static forwarder + `services:`** — instance-shaped plugin APIs. _~10–25 lines_ (local\_auth: 24).
3.  **Platform-quirk shim** — a platform needs an argument codegen can't encode (share\_plus wants a `sharePositionOrigin` rect on iPad/iOS 26). _~8 lines._
4.  **`hosts:` factory** — live controller, async init. _~15–20 lines._
5.  **A plain local widget class** — write normal Flutter in `lib/adapters/`; codegen walks _your_ code like any pub package. Same manifest, same `'skal-flutter'` import.
6.  **Raw registry** — `SkalRegistry.registerWidget(name, (n, bridge) => …)` for full manual control. Last resort; props read untyped off the node.

Hand-written Dart is codegen **input**, not a parallel system — a forwarder or local widget gets the same typed prop readers, stream plumbing, and JSX import synthesis as any pub package. Zero JS-side glue at every rung except the last.

## Permissions

Declare intent once in `flutter-host/skal-permissions.json`:

```jsx
{ "camera": "Scan QR codes", "location": "Show places near you" }
```

`bun run link` translates it into every platform dialect — iOS + macOS `Info.plist` usage strings, macOS App Sandbox entitlements, `AndroidManifest` entries, and permission\_handler's `PERMISSION_*` Podfile macros (without which every request answers `permanentlyDenied`). Idempotent, add-only; your hand-edited wording is never reverted.

## When codegen isn't enough

-   **Non-void callbacks** (`bool Function(T)`) need a synchronous JS→Dart return, which would block the frame — permanently out of scope. Restructure around a prop or a ref method.
-   **Builders keyed by a domain object** (not an index) have nothing to key a subtree by — indexed builders work via `builder: true`.
-   **Payloads are for data, not blobs** — strings beyond the heap sizes truncate. Pass paths and handles, not file contents.

[Previous← State & the Store](state.html) [NextHot reload & dev loop →](tooling.html)
