flutter_watchos 0.1.0-beta.9 copy "flutter_watchos: ^0.1.0-beta.9" to clipboard
flutter_watchos: ^0.1.0-beta.9 copied to clipboard

Platform detection and utilities for Flutter apps running on Apple Watch (watchOS). Provides runtime checks for watchOS, device info, capability queries, and Taptic Engine haptics.

flutter_watchos #

Platform detection and utilities for Flutter apps running on Apple Watch (watchOS), built for the flutter-watchos toolchain.

A small FFI package with zero-overhead, synchronous native calls — no method channels, no async.

Source & issues: https://github.com/flutterwatch/flutter-watchos

Features #

  • Platform detection — FlutterWatchosPlatform.isWatch disambiguates Apple Watch from iPhone/iPad. (Both report Platform.isIOS == true, because watchOS is an iOS-family OS — see the toolchain's platform-identity notes.) The FlutterWatchosPlatform getters are safe to call from shared code on every platform a Flutter app targets, Web included, where all of them are false.
  • Device info — WatchOSInfo exposes the watchOS version, device model, machine id (e.g. Watch7,18; resolves correctly in the Simulator too), simulator flag, and native screen size/scale.
  • Haptics — WatchHaptics.play(...) drives the Taptic Engine via WKInterfaceDevice.playHaptic.
  • Status bar — WatchStatusBar.hidden shows/hides the system clock the watch draws over every app (visible by default, per the HIG; hide it for games and full-bleed UIs — watchOS cannot reposition it, so a custom placement means hiding it and drawing your own).
  • Always-On — WatchAlwaysOn / WatchAlwaysOnBuilder tell you when the wrist is down and watchOS is showing your app dimmed, so you can pause animations, hide private content, and drop bright fills (the HIG expectation). It reflects SwiftUI's \.isLuminanceReduced, which is more precise than AppLifecycleState.inactive — that also fires for notification banners and Control Center.
  • Digital Crown — WatchCrownScroll gives scrollables the native feel: watch-tuned scroll physics (WatchScrollPhysics — a firm, live, shallow edge bounce instead of the iPhone-style deep stretch; no edge haptic, just like native watchOS 26). WatchCrownScrolling exposes the same knobs native developers get (sensitivity, detent haptics on/off). WatchCrown gives the crown as a raw input (a rotation stream, or a per-frame drain()) for games, value pickers, and custom controls — without it driving scroll.
  • Platform views — WatchPlatformView embeds a native SwiftUI view (a Gauge, a Toggle, a map, a video surface) at its slot in the Flutter layout, composited at its position in paint order like any other content.

Usage #

import 'package:flutter_watchos/flutter_watchos.dart';

if (WatchOSInfo.isWatchOS) {
  print('watchOS ${WatchOSInfo.watchOSVersion} on ${WatchOSInfo.deviceModel}');
  print('Screen: ${WatchOSInfo.screenResolution} @${WatchOSInfo.screenScale}x');
  WatchHaptics.play(WatchHapticType.success);
}

// Watch-only branch (excludes iPhone/iPad):
if (FlutterWatchosPlatform.isWatch) {
  // compact, crown-driven UI
}

Always-On #

When the wrist drops, watchOS keeps your app on screen at reduced luminance rather than blanking it. Your last frame stays visible with no work on your part — but the HIG expects you to react: stop animations that now burn battery for nobody, and hide anything a bystander shouldn't read.

WatchAlwaysOnBuilder(
  builder: (context, alwaysOn, _) => alwaysOn
      ? const DimmedFace()   // static, dark, no private data
      : const LiveFace(),    // the full UI
)

Outside the widget tree — to pause a controller or cancel a timer — listen to WatchAlwaysOn.state, or read WatchAlwaysOn.isActive once.

Don't reach for AppLifecycleState.inactive here: watchOS also resigns active for notification banners and Control Center, so it can't tell "wrist down" from "something is covering the app". This API reflects SwiftUI's \.isLuminanceReduced, which means exactly the former.

The two do fire together, in no guaranteed order — so don't read WatchAlwaysOn.isActive from inside your own didChangeAppLifecycleState. At that instant the watch host may not have reported yet, and a one-shot read can return the pre-transition value. Listen to WatchAlwaysOn.state and let it settle. Doing so doesn't disturb your own lifecycle observers.

An app that would rather blank than dim opts out in its Info.plist with WKSupportsAlwaysOnDisplay = false; isActive then never becomes true.

Digital Crown #

By default the crown scrolls. Wrap a scrollable (usually a whole screen) to give it the native watch feel — watch-tuned physics with a firm, live, shallow edge bounce (and, matching native watchOS 26, no haptic at the list edges):

WatchCrownScroll(child: ListView(children: const [/* ... */]));

App-wide instead: MaterialApp(scrollBehavior: const WatchScrollBehavior()), or pass physics: const WatchScrollPhysics() to a single scrollable.

Scroll behavior has the same options native (SwiftUI) developers get on .digitalCrownRotation — they apply app-wide, from the next crown movement:

WatchCrownScrolling.sensitivity = WatchCrownSensitivity.medium; // low/medium/high
WatchCrownScrolling.detentHaptics = false; // silent scrolling

For a game or custom control, take the crown as raw input instead. While a WatchCrown subscription (or enable()) is active, the crown stops scrolling and delivers rotation directly:

// Stream (frame-polled). Subscribing switches the crown to raw mode;
// cancelling the last listener returns it to scroll.
final sub = WatchCrown.instance.rotations.listen((e) {
  setState(() => paddleX += e.delta * sensitivity); // e.velocity also available
});
// ...later: await sub.cancel();

// Or, for an app with its own game loop — zero stream overhead:
WatchCrown.instance.enable();
final delta = WatchCrown.instance.drain(); // call each tick
WatchCrown.instance.disable();

On non-watchOS platforms the stream never emits and drain() returns 0, so it's safe to leave in cross-platform code.

Platform views #

Register a SwiftUI factory per viewType in the app's App.swift initializer (WatchPlatformViewRegistry comes with the FlutterWatchOS host module every app imports), then place the widget like any other box:

WatchPlatformViewRegistry.register("gauge") { params in
    AnyView(MyGaugeView(params: params))
}
SizedBox(
  height: 64,
  child: WatchPlatformView(
    viewType: 'gauge',
    creationParams: '{"value": 0.72}',
  ),
)

The native view is composited at the widget's position in paint order: Flutter content painted before the widget is below it, content painted after it — a badge in a Stack, a border in a foregroundDecoration, a dialog, a snackbar — draws over it. Ancestor clips, opacity and transforms apply, and the view hides whenever it is not painted (scrolled out of the viewport, covered by an opaque route).

layer: decides who gets the touches inside the view's rect — SwiftUI has no event forwarding, so whichever side takes the touch-down owns the whole gesture:

  • WatchPlatformViewLayer.aboveFlutter (default) — the native view gets them, unless Flutter content painted above it covers that point. Use for interactive controls (pickers, buttons, toggles).
  • WatchPlatformViewLayer.belowFlutter — Flutter always gets them; wrap the widget in a GestureDetector to handle taps in Dart. Use for display views (gauges, charts).

WatchPlatformView.isSupported is false off-watch and on engines that predate platform views (the widget then paints nothing), and WatchPlatformView.isComposited tells whether the engine composites from the layer tree — on older engines the widget falls back to an overlay/underlay model where layer: also picks the composition side (see the API docs).

This is an FFI plugin (ffiPlugin: true). The native C functions in watchos/Classes/flutter_watchos_ffi.{h,m} are statically linked into the watch app. Because FFI symbols have no compile-time caller, each one is listed under flutter.plugin.platforms.watchos.ffiSymbols in pubspec.yaml, marked used + default-visibility in the header, force-loaded into the app by the flutter-watchos CLI, and kept through the App Store strip, so they survive -dead_strip and remain resolvable via DynamicLibrary.process().

On non-Apple platforms (Web, Android, desktop) every API returns a safe default and performs no FFI lookup.

3
likes
130
points
160
downloads

Documentation

API reference

Publisher

verified publisherflutterwatch.dev

Weekly Downloads

Platform detection and utilities for Flutter apps running on Apple Watch (watchOS). Provides runtime checks for watchOS, device info, capability queries, and Taptic Engine haptics.

Topics

#watchos #apple-watch #wearable #ffi

License

BSD-3-Clause (license)

Dependencies

ffi, flutter

More

Packages that depend on flutter_watchos

Packages that implement flutter_watchos