voo_watch_ui 0.4.0
voo_watch_ui: ^0.4.0 copied to clipboard
Cross-platform UI DSL for smartwatches. Define your watch UI in Dart and render it identically on Apple Watch (native SwiftUI) and Wear OS (Flutter).
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:voo_watch_ui/voo_watch_ui.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const _App());
}
class _App extends StatelessWidget {
const _App();
@override
Widget build(BuildContext context) => MaterialApp(
title: 'voo_watch_ui example',
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
home: const WatchUiHotReloadBinding(child: WatchUiThemeBinding(child: _Home())),
debugShowCheckedModeBanner: false,
);
}
class _Home extends StatefulWidget {
const _Home();
@override
State<_Home> createState() => _HomeState();
}
class _HomeState extends State<_Home> {
int _count = 0;
bool _toggle = true;
double _intensity = 0.5;
String _lastTap = '— none —';
@override
void initState() {
super.initState();
final ui = VooWatchUi.instance
..onEvent('increment', (_) => setState(() {
_count += 1;
_lastTap = 'increment';
}))
..onEvent('decrement', (_) => setState(() {
_count = (_count - 1).clamp(0, 999);
_lastTap = 'decrement';
}))
..onEvent('toggle', (payload) => setState(() {
_toggle = payload['value'] as bool? ?? !_toggle;
_lastTap = 'toggle → $_toggle';
}))
..onEvent('intensity', (payload) => setState(() {
_intensity = (payload['value'] as num?)?.toDouble() ?? _intensity;
_lastTap = 'intensity → ${_intensity.toStringAsFixed(2)}';
}));
// Push the initial tree once the renderer is wired up.
WidgetsBinding.instance.addPostFrameCallback((_) => ui.render(_buildTree()));
}
WatchView _buildTree() => WatchView.column(
crossAxisAlignment: WatchCrossAxisAlignment.stretch,
children: <WatchView>[
WatchView.text('Counter', style: WatchTextStyle.headline),
const WatchSizedBox(height: 4),
WatchView.card(
child: WatchView.padding(
padding: const WatchEdgeInsets.all(12),
child: WatchView.row(
mainAxisAlignment: WatchMainAxisAlignment.spaceBetween,
children: <WatchView>[
WatchView.iconButton(icon: 'minus', onTap: 'decrement'),
WatchView.text('$_count', style: WatchTextStyle.title),
WatchView.iconButton(icon: 'plus', onTap: 'increment'),
],
),
),
),
const WatchSizedBox(height: 12),
WatchView.row(
mainAxisAlignment: WatchMainAxisAlignment.spaceBetween,
children: <WatchView>[
WatchView.text('Notifications', style: WatchTextStyle.body),
WatchView.toggle(value: _toggle, onChanged: 'toggle'),
],
),
const WatchSizedBox(height: 8),
WatchView.text('Intensity', style: WatchTextStyle.caption),
WatchView.slider(value: _intensity, onChanged: 'intensity', activeColor: WatchColor.accent),
const WatchSizedBox(height: 12),
WatchView.progressIndicator(value: _intensity, kind: WatchProgressKind.linear, color: WatchColor.success),
const WatchSizedBox(height: 12),
WatchView.chip(label: 'Tap: $_lastTap', color: WatchColor.muted),
],
);
@override
Widget build(BuildContext context) {
final tree = _buildTree();
// Rebuild the watch tree on every state change. `render` coalesces calls
// within a 32ms window so a slider drag won't flood the bridge.
VooWatchUi.instance.render(tree);
const renderer = WatchUiFlutterRenderer();
return Scaffold(
appBar: AppBar(title: const Text('voo_watch_ui')),
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
const Padding(
padding: EdgeInsets.all(12),
child: Text(
'Phone preview (the same renderer Wear OS uses):',
style: TextStyle(fontWeight: FontWeight.w600),
),
),
Center(
child: Container(
width: 240,
height: 280,
decoration: BoxDecoration(
color: const Color(0xFF000000),
borderRadius: BorderRadius.circular(28),
border: Border.all(color: Colors.white24, width: 2),
),
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.all(8),
child: Theme(
data: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorSchemeSeed: Colors.indigo,
),
child: renderer.render(tree),
),
),
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(12),
child: Wrap(
alignment: WrapAlignment.center,
spacing: 8,
children: <Widget>[
FilledButton.tonalIcon(
onPressed: () => VooWatchUi.instance.haptic(WatchHapticKind.success),
icon: const Icon(Icons.vibration),
label: const Text('Haptic: success'),
),
FilledButton.tonalIcon(
onPressed: () => VooWatchUi.instance.haptic(WatchHapticKind.warning),
icon: const Icon(Icons.vibration),
label: const Text('Haptic: warning'),
),
],
),
),
],
),
),
);
}
}