flutter_watch_bridge 0.1.0
flutter_watch_bridge: ^0.1.0 copied to clipboard
One API for messages, state sync and file transfer between a Flutter phone app and its paired Apple Watch or Wear OS watch, with pairing and reachability streams.
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_watch_bridge/flutter_watch_bridge.dart';
import 'package:flutter_watch_bridge/testing.dart';
/// `flutter run --dart-define=DEMO=true` starts the simulated watch and plays
/// a short scripted exchange (used for the README screenshots).
const _demo = bool.fromEnvironment('DEMO');
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Watch Bridge',
debugShowCheckedModeBanner: false,
theme: ThemeData(colorSchemeSeed: Colors.teal, useMaterial3: true),
home: const HomePage(),
);
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late WatchBridge _bridge = WatchBridge.instance;
final _subs = <StreamSubscription<Object?>>[];
final _log = <String>[];
WatchStatus? _status;
bool _simulated = false;
SimulatedWatchLink? _link;
WatchBridge? _simWatch;
@override
void initState() {
super.initState();
if (_demo) {
_playDemo();
} else {
_start();
}
}
Future<void> _playDemo() async {
await _toggleSimulator(true);
final watch = _simWatch!;
await _run('sendMessage', () => _bridge.sendMessage({'hello': 'watch'}));
await _run('request', () => _bridge.request({'ping': 42}));
await _run('syncState', () => _bridge.syncState('phone', {'units': 'metric'}));
await watch.syncState('watch', {'heartRate': 72});
await watch.sendMessage({'tapped': 'start workout'});
await _run('enqueue', () => _bridge.enqueue({'event': 'login'}));
await _run('sendReliable', () async => (await _bridge.sendReliable({'event': 'sync'})).name);
}
Future<void> _start() async {
for (final s in _subs) {
await s.cancel();
}
_subs.clear();
try {
await _bridge.initialize();
} on WatchBridgeException catch (e) {
_add('init failed: $e');
}
_subs
..add(_bridge.status.listen((s) => setState(() => _status = s)))
..add(_bridge.messages.listen((m) => _add('message: ${m.data}')))
..add(_bridge.queuedItems.listen((m) => _add('queued item: $m')))
..add(_bridge.stateStream('watch').listen((m) => _add('watch state: $m')))
..add(_bridge.incomingFiles.listen((f) => _add('file: ${f.path}')));
_bridge.setRequestHandler((m) => {'pong': m.data['ping']});
}
void _add(String line) => setState(() => _log.insert(0, line));
Future<void> _run(String label, Future<Object?> Function() action) async {
try {
final r = await action();
_add('$label ok${r == null ? '' : ': $r'}');
} on WatchBridgeException catch (e) {
_add('$label failed: ${e.runtimeType} ${e.message}');
}
}
Future<void> _toggleSimulator(bool on) async {
if (on) {
_link = SimulatedWatchLink();
_bridge = WatchBridge.forPlatform(_link!.phone);
_simWatch = WatchBridge.forPlatform(_link!.watch);
await _simWatch!.initialize();
_simWatch!.setRequestHandler((m) => {'watch_echo': m.data});
_simWatch!.messages.listen((m) => _add('(sim watch got) ${m.data}'));
} else {
_bridge = WatchBridge.instance;
_link = null;
_simWatch = null;
}
setState(() => _simulated = on);
await _start();
}
@override
void dispose() {
for (final s in _subs) {
s.cancel();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final s = _status;
return Scaffold(
appBar: AppBar(title: const Text('Watch Bridge')),
body: ListView(padding: const EdgeInsets.all(16), children: [
SwitchListTile(
title: const Text('Simulated watch'),
subtitle: const Text('In-memory watch, no hardware needed'),
value: _simulated,
onChanged: _toggleSimulator,
),
if (_link != null)
SwitchListTile(
title: const Text('Simulated watch reachable'),
value: _link!.reachable,
onChanged: (v) => setState(() => _link!.reachable = v),
),
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: s == null
? const Text('Waiting for status...')
: Wrap(spacing: 8, children: [
for (final e in {
'supported': s.supported,
'paired': s.paired,
'app installed': s.appInstalled,
'reachable': s.reachable,
'activated': s.activated,
}.entries)
Chip(
avatar: Icon(e.value ? Icons.check_circle : Icons.cancel,
color: e.value ? Colors.green : Colors.red),
label: Text(e.key),
),
]),
),
),
const SizedBox(height: 8),
Wrap(spacing: 8, runSpacing: 8, children: [
FilledButton(
onPressed: () => _run('sendMessage', () => _bridge.sendMessage({'hello': 'watch'})),
child: const Text('Send message'),
),
FilledButton(
onPressed: () => _run('request', () => _bridge.request({'ping': DateTime.now().millisecond})),
child: const Text('Request / reply'),
),
FilledButton(
onPressed: () => _run('syncState', () => _bridge.syncState('phone', {'counter': _log.length})),
child: const Text('Sync state'),
),
FilledButton(
onPressed: () => _run('enqueue', () => _bridge.enqueue({'queued': _log.length})),
child: const Text('Queue item'),
),
FilledButton(
onPressed: () => _run('sendReliable', () async => (await _bridge.sendReliable({'r': 1})).name),
child: const Text('Send reliably'),
),
FilledButton(
onPressed: () => _run('sendFile', () async {
final file = File('${Directory.systemTemp.path}/hello.txt')..writeAsStringSync('hello watch');
final t = await _bridge.sendFile(file, metadata: {'name': 'hello.txt'});
t.progress.listen((p) => _add('transfer ${(p.fraction * 100).round()}%'));
await t.done;
return 'done';
}),
child: const Text('Send file'),
),
]),
const Divider(height: 32),
for (final line in _log) Text(line, style: const TextStyle(fontFamily: 'monospace')),
]),
);
}
}