← Back to posts Cover image for Flutter for Wearables: Building Smartwatch Apps for watchOS and Wear OS

Flutter for Wearables: Building Smartwatch Apps for watchOS and Wear OS

· 6 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

Building an app that lives on your wrist is a compelling idea. It’s the ultimate in accessibility and context-aware computing. But as Flutter developers, we’re used to a single codebase conquering mobile, web, and desktop. How does that paradigm hold up when the screen shrinks to the size of a postage stamp? Let’s explore the current state of building smartwatch apps for watchOS and Wear OS with Flutter.

The Unique Challenge of Wearables

Smartwatch development is fundamentally different. The interaction model is glanceable—users look at your app for seconds, not minutes. The hardware is constrained, with smaller batteries, less memory, and lower-powered CPUs. The UI must be radically simplified, information-dense, and navigable with a tap or a crown. Flutter’s strength in expressive, custom UI is a huge asset here, but we must wield it with extreme discipline.

The Big Decision: Companion vs. Standalone

Before writing a line of code, you must decide on your app’s architecture.

  • Companion App: This is an extension of your primary phone app. The watch app relies on the phone for data, heavy computation, and often for its very existence (it’s installed as a companion). Communication happens via platform channels, using protocols like the Wear OS WearableDataLayer or watchOS’s WatchConnectivity framework. This is the most common and often most sensible path, especially if your watch app displays notifications, provides quick actions, or shows simplified data from the phone.
  • Standalone App: This app runs independently on the watch, with its own logic, data storage, and potentially even its own App Store listing. It might only sync with a phone occasionally. This is necessary for apps that need to function without the phone nearby, like a fitness tracker or a standalone music player. The complexity and amount of required native code increase significantly here.

For most Flutter developers, starting with a companion app is the recommended and most supported path.

Flutter’s Role: The Hybrid Approach

Let’s be clear: as of now, you cannot build a 100% pure Flutter app that runs directly on watchOS or Wear OS. The Flutter engine isn’t embedded directly into the watch’s native app runtime. Instead, the practical approach is a hybrid one:

  1. You create a minimal native watch app for each platform (a WatchKit app for watchOS, a Wear OS activity for Android). This native shell is responsible for launching and hosting the UI.
  2. This native shell then displays your Flutter UI by using FlutterViewController (iOS) or FlutterFragment (Android) embedded within it.
  3. Your core business logic and UI are written in Dart/Flutter and packaged as a module that the native shells depend on.

This means you are writing some native code—specifically, the project scaffolding and the communication layer. However, the bulk of your UI and logic remains shared Dart code.

Building a Simple Companion App: A Code Example

Imagine a simple companion app that shows a heart rate reading from the phone. The phone app (Flutter) fetches the data, and the watch app (Flutter UI in a native shell) displays it.

First, your Flutter module (shared code) would have a simple watch-focused UI:

import 'package:flutter/material.dart';

// This widget is designed for a small, round watch face.
class HeartRateWatchScreen extends StatelessWidget {
  final int? heartRate;

  const HeartRateWatchScreen({super.key, this.heartRate});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(
              Icons.favorite,
              color: Colors.red,
              size: 30,
            ),
            const SizedBox(height: 16),
            Text(
              heartRate != null ? '$heartRate' : '--',
              style: const TextStyle(
                fontSize: 42,
                fontWeight: FontWeight.bold,
                color: Colors.white,
              ),
            ),
            const SizedBox(height: 8),
            const Text(
              'BPM',
              style: TextStyle(
                fontSize: 16,
                color: Colors.grey,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

The communication from the phone to the watch happens via Platform Channels and the native wearable communication APIs. This is where you write platform-specific code.

On the Phone (Flutter) side, you’d use a method channel to trigger the native phone code to send data via WatchConnectivity (iOS) or the Wearable Data Layer (Android).

On the Watch (Native Shell) side, you need to set up the FlutterViewController (watchOS) or FlutterFragment (Wear OS) and configure it to receive method calls from the Flutter module and from the native watch connectivity APIs. This data is then passed into the Flutter UI, perhaps via an InheritedWidget or state management solution like Provider.

Best Practices & Common Pitfalls

  1. Start Simple: Begin with a read-only, companion display app. Trying to build a standalone, sensor-heavy app as your first foray is a recipe for frustration.
  2. Design for Glances: Your UI should convey its primary information in under 2 seconds. Use large, legible fonts, high-contrast colors, and minimal interactive elements.
  3. Beware of Performance: Profiling is non-negotiable. A janky animation on a phone is bad; on a watch, it’s app-killing. Keep your widget trees shallow and avoid unnecessary rebuilds.
  4. Navigation is Different: Deep navigation stacks don’t work. Prefer a single screen or a carousel-like horizontal page view (PageView). Use the WatchShape (wear) or Cupertino (watchOS) widgets from packages like flutter_wear_os or community libraries to get platform-appropriate shapes and scrolling.
  5. Testing is Crucial and Hard: You must test on real hardware. Watch simulators are poor substitutes for testing performance, battery impact, and the actual feel of crown/touch interactions.

Is Flutter Ready for Your Watch App?

The answer is a qualified yes, for the right use case. If your goal is to extend your existing Flutter mobile app to the wrist with a companion that shows notifications, controls media, or displays key data, Flutter’s hybrid model is a powerful and efficient choice. It allows you to leverage your existing Dart code and Flutter UI expertise.

However, if you need a deeply integrated, sensor-reliant, fully standalone watch app where every millisecond of battery life counts, you may still find that fully native development (Swift for watchOS, Kotlin for Wear OS) gives you the finer control and performance guarantees you need—at the cost of maintaining two separate codebases.

The landscape is evolving. The Flutter team and community are increasingly looking at embedded devices. For now, Flutter for wearables is a powerful tool for companion experiences, letting you bring a slice of your app to the user’s wrist with remarkable efficiency.

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Flutter for High-Performance Desktop: Is it Ready for CAD, Image Processing, and Complex GUIs?

Flutter for High-Performance Desktop: Is it Ready for CAD, Image Processing, and Complex GUIs?

Developers are curious about Flutter's capabilities beyond typical business apps, especially for demanding desktop applications like CAD/CAM or image/video processing. This post will explore Flutter's suitability for high-performance, viewport-based desktop GUIs, discussing Dart's memory model, the 60fps update loop, and real-world examples to gauge its readiness for 'serious' complex software.

Cover image for Debugging Flutter Web Navigation: Solving the Deep Link Refresh Bug

Debugging Flutter Web Navigation: Solving the Deep Link Refresh Bug

Flutter web applications often suffer from a frustrating 'deep link refresh bug' where refreshing the browser on a nested route (e.g., /home/details) bounces the user back to the root or an incorrect path. This post will diagnose the common causes of this issue, explain how Flutter's router handles web URLs, and provide practical solutions and best practices for building robust, refresh-proof navigation in your Flutter web apps.

Cover image for Mastering Internationalization in Flutter: Centralized Strings for Scalable Apps

Mastering Internationalization in Flutter: Centralized Strings for Scalable Apps

As Flutter applications grow, managing strings for multiple languages or just keeping text consistent becomes a challenge. This post will guide developers through effective strategies for centralizing strings, implementing robust internationalization (i18n) and localization (l10n), and leveraging tools to streamline the process for small to large-scale projects.