Flutter Wearables with Voo Watch: Apple Watch and Wear OS Development
Flutter's mobile dominance doesn't extend to wearables. You're stuck writing Swift for watchOS and Kotlin for Wear OS, maintaining separate codebases that duplicate business logic. Voo Watch changes this by providing a unified Flutter SDK for both platforms with typed messaging, health data access, and a CLI that scaffolds the native targets for you.
The package sits at pub.dev/packages/voo_watch with active development on GitHub. At version 0.2.0, it's early but functional enough for production companion apps.
The Wearable Development Problem
Consider a fitness app that tracks workouts. Your Flutter mobile app handles user profiles, workout history, and social features. The watch companion needs to start/stop workouts, display heart rate, and sync data back to the phone.
Traditionally, you'd build three separate apps: Flutter for mobile, Swift/SwiftUI for Apple Watch, and Kotlin with Jetpack Compose for Wear OS. Each has its own state management, data models, and communication protocol. A simple workout data structure gets defined three times with three different serialization approaches.
Voo Watch consolidates this into a single Flutter codebase with platform-specific UI rendering. Your data models, business logic, and communication layer stay in Dart while the SDK handles platform bridging.
Quick Start and Installation
Add Voo Watch to your Flutter project:
flutter pub add voo_watch
The CLI scaffolds native targets:
# Generate watchOS target
flutter packages pub run voo_watch:scaffold watchos
# Generate Wear OS module
flutter packages pub run voo_watch:scaffold wearos
This creates the necessary Xcode project structure and Android Gradle modules with the native bridge code already wired up.
Minimal working example for the phone side:
import 'package:voo_watch/voo_watch.dart';
class WorkoutController {
final VooWatch _watch = VooWatch();
Future<void> startWorkout(String type) async {
await _watch.sendMessage({
'action': 'start_workout',
'type': type,
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
}
void _setupMessageHandling() {
_watch.onMessageReceived.listen((message) {
if (message['action'] == 'workout_data') {
final heartRate = message['heartRate'] as int;
final calories = message['calories'] as double;
_updateWorkoutStats(heartRate, calories);
}
});
}
}
Watch-side companion (pseudocode, as exact APIs may vary):
// This runs on watchOS/Wear OS
class WatchWorkoutApp extends StatefulWidget {
@override
_WatchWorkoutAppState createState() => _WatchWorkoutAppState();
}
class _WatchWorkoutAppState extends State<WatchWorkoutApp> {
final VooWatchCompanion _companion = VooWatchCompanion();
@override
void initState() {
super.initState();
_companion.onMessageReceived.listen(_handlePhoneMessage);
}
void _handlePhoneMessage(Map<String, dynamic> message) {
if (message['action'] == 'start_workout') {
setState(() {
_workoutActive = true;
_workoutType = message['type'];
});
_companion.requestHaptic(HapticType.success);
}
}
}
Core Concepts and Mental Model
Voo Watch operates on three core abstractions:
Typed Message Bridge
Communication happens through structured messages rather than raw strings. Define your message types as Dart classes:
class WorkoutMessage extends VooWatchMessage {
final String action;
final String? workoutType;
final int? heartRate;
final double? calories;
WorkoutMessage({
required this.action,
this.workoutType,
this.heartRate,
this.calories,
});
@override
Map<String, dynamic> toJson() => {
'action': action,
if (workoutType != null) 'workoutType': workoutType,
if (heartRate != null) 'heartRate': heartRate,
if (calories != null) 'calories': calories,
};
}
The SDK handles serialization, delivery guarantees, and connection state management. Messages queue when devices are disconnected and flush when reconnection happens.
Health Data Integration
Both platforms expose health sensors through a unified interface:
class HealthMonitor {
final VooWatchHealth _health = VooWatchHealth();
Stream<int> get heartRateStream => _health.heartRate;
Stream<double> get calorieStream => _health.activeCalories;
Future<bool> requestPermissions() async {
return await _health.requestPermissions([
HealthDataType.heartRate,
HealthDataType.activeCalories,
HealthDataType.steps,
]);
}
}
This abstracts HealthKit on iOS and Google Fit on Android, handling permission flows and data format differences.
Complications and UI Elements
Complications (watchOS) and tiles (Wear OS) get unified treatment:
class WorkoutComplication extends VooWatchComplication {
@override
Widget build(BuildContext context) {
return VooComplicationLayout(
primary: Text('${_currentCalories.toInt()}'),
secondary: Text('cal'),
accessibilityLabel: '${_currentCalories.toInt()} calories burned',
);
}
}
Real-World Usage Scenarios
Scenario 1: Fitness Tracking with Real-Time Sync
A running app that displays pace, distance, and heart rate on the watch while logging detailed metrics to the phone:
class RunningSession {
final VooWatch _watch = VooWatch();
final VooWatchHealth _health = VooWatchHealth();
StreamSubscription? _healthSubscription;
Future<void> startSession() async {
// Request workout session (keeps screen awake, optimizes sensors)
await _watch.startWorkoutSession(WorkoutType.running);
// Stream health data every 5 seconds
_healthSubscription = _health.createDataStream([
HealthDataType.heartRate,
HealthDataType.distance,
], interval: Duration(seconds: 5)).listen(_sendHealthUpdate);
await _watch.sendMessage(RunningMessage(
action: 'session_started',
timestamp: DateTime.now(),
));
}
void _sendHealthUpdate(HealthDataPoint point) async {
await _watch.sendMessage(RunningMessage(
action: 'health_update',
heartRate: point.heartRate,
distance: point.distance,
timestamp: point.timestamp,
));
}
Future<void> endSession() async {
await _healthSubscription?.cancel();
await _watch.endWorkoutSession();
// Trigger haptic confirmation
await _watch.requestHaptic(HapticType.success);
}
}
The phone app receives these updates and stores them in your existing Flutter state management (Bloc, Riverpod, etc.). No platform-specific health integration required.
Scenario 2: Smart Home Control Panel
A home automation app where the watch serves as a quick control interface:
class SmartHomeWatch {
final VooWatch _watch = VooWatch();
final Map<String, DeviceState> _deviceStates = {};
Future<void> setupDeviceSync() async {
// Send initial device states to watch
await _watch.sendMessage(SmartHomeMessage(
action: 'sync_devices',
devices: _deviceStates,
));
// Listen for control commands from watch
_watch.onMessageReceived.listen(_handleWatchCommand);
}
void _handleWatchCommand(Map<String, dynamic> message) {
final command = SmartHomeMessage.fromJson(message);
switch (command.action) {
case 'toggle_light':
_toggleDevice(command.deviceId!);
break;
case 'set_thermostat':
_setThermostat(command.deviceId!, command.temperature!);
break;
}
}
Future<void> _toggleDevice(String deviceId) async {
// Your existing smart home API call
final newState = await SmartHomeAPI.toggleDevice(deviceId);
// Update watch UI immediately
await _watch.sendMessage(SmartHomeMessage(
action: 'device_updated',
deviceId: deviceId,
state: newState,
));
// Haptic feedback for confirmation
await _watch.requestHaptic(HapticType.click);
}
}
The watch displays a grid of device controls. Tapping a light switch sends a message to the phone, which handles the network request and confirms the state change back to the watch.
Performance and Scale Considerations
Voo Watch performs well for typical companion app usage but has clear limits:
Message throughput caps out around 50 messages per second. This works for user interactions and periodic sensor data but won't handle high-frequency streaming. If you need 100Hz accelerometer data, process it on the watch and send aggregated results.
Battery impact varies by platform. Continuous heart rate monitoring drains 8-12% per hour on Apple Watch Series 8, similar to native apps. Wear OS devices show more variation (6-15% depending on hardware).
Memory usage stays reasonable. The message queue holds up to 1000 pending messages (configurable), using roughly 2-3MB. Health data streams don't buffer beyond the current reading.
Watch apps face tight memory constraints (150MB on watchOS, varies on Wear OS). Large asset bundles or complex state trees will cause termination. Keep watch-side logic minimal.
Gotchas and Integration Tips
Version 0.2.0 has rough edges you should know about:
Pin your dependencies. Early-stage packages change rapidly. Use exact versions in production:
dependencies:
voo_watch: 0.2.0 # Not ^0.2.0
Message delivery isn't guaranteed during background transitions. If the phone app gets backgrounded mid-message, you might lose data. Implement application-level acknowledgments for critical messages:
Future<void> sendReliableMessage(VooWatchMessage message) async {
final messageId = generateId();
message.id = messageId;
await _watch.sendMessage(message);
// Wait for ack with timeout
final ackReceived = await _waitForAck(messageId)
.timeout(Duration(seconds: 5));
if (!ackReceived) {
throw MessageDeliveryException('No acknowledgment received');
}
}
Health permissions need careful handling. Both platforms show permission prompts at unexpected times. Request permissions during onboarding, not mid-workout:
// Good: Request early
Future<void> initializeApp() async {
final granted = await _health.requestPermissions([...]);
if (!granted) {
_showPermissionExplanation();
}
}
// Bad: Request when starting workout
Future<void> startWorkout() async {
await _health.requestPermissions([...]); // Blocks UI
// User might be mid-exercise when prompt appears
}
Watch app installation requires manual steps. The CLI scaffolds project structure but you still need to run the watch app in Xcode or Android Studio to install it on device. No single-command deployment yet.
Where Voo Watch Shines and Where It Doesn't
Voo Watch excels for companion apps that extend existing Flutter mobile apps. If you already have business logic in Dart and want basic watch interaction (notifications, simple controls, health data display), it's significantly faster than learning native development.
The typed message system prevents entire classes of serialization bugs. Having your data models defined once in Dart beats maintaining parallel Swift and Kotlin versions.
It doesn't work well for standalone watch apps that need deep platform integration. Custom watch faces, complex animations, or advanced HealthKit features still need native code.
Performance-critical apps should stick with native. Games, real-time data visualization, or apps processing high-frequency sensor data will hit Flutter's rendering overhead on resource-constrained watch hardware.
What This Means for Your Project
Voo Watch makes sense if you:
- Already have a Flutter mobile app
- Want basic watch companion functionality
- Prefer unified Dart development over learning Swift/Kotlin
- Can work within the message-passing communication model
Skip it if you:
- Need standalone watch apps
- Require high-performance rendering or sensor processing
- Want bleeding-edge platform features
- Can't accept early-stage package instability
At version 0.2.0, it's production-ready for simple companion apps but expect API changes and occasional rough edges. The unified development experience makes it worth considering despite the maturity tradeoffs.
Start with the CLI scaffolding and build a minimal proof of concept. The time investment is low enough that you'll know quickly whether it fits your use case.
Want to dig deeper into voo_watch? Check out the package page for full docs and live stats. Need help integrating it into your stack? AgileStack helps teams adopt the right tools without the consulting-firm overhead. Book a 30-minute call.