Perché Wear OS con Flutter
Wear OS è a tutti gli effetti Android, quindi il motore Flutter ci gira senza modifiche al framework: una build android standard viene installata ed eseguita sull'orologio. Quello che cambia è il contesto d'uso: schermi da 1,2" a 1,5" (spesso rotondi), interazioni di pochi secondi, batteria ridotta, input rotativo e una modalità a basso consumo (ambient) in cui il display resta acceso ma deve consumare pochissimo.
In questa guida vediamo come configurare un progetto Flutter per Wear OS, adattare la UI alla forma dello schermo, gestire l'ambient mode, supportare la corona/bezel rotativo e scambiare dati con l'app companion sul telefono.
Configurare il progetto Android
Partiamo da un normale progetto Flutter e interveniamo sulla parte Android.
AndroidManifest.xml
Il manifest deve dichiarare che l'app è destinata a un dispositivo indossabile e che funziona in modo standalone (cioè senza richiedere obbligatoriamente l'app sul telefono):
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.type.watch" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:label="wear_demo"
android:icon="@mipmap/ic_launcher">
<!-- true = l'app funziona anche senza companion sullo smartphone -->
<meta-data
android:name="com.google.android.wearable.standalone"
android:value="true" />
<uses-library
android:name="com.google.android.wearable"
android:required="false" />
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|uiMode|density"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
build.gradle
Wear OS 3 e successivi richiedono API 30; se vuoi coprire anche i vecchi Wear OS 2 puoi scendere a 25, ma la maggior parte del parco installato oggi è su 3/4/5.
android {
namespace = "com.example.wear_demo"
compileSdk = 35
defaultConfig {
applicationId = "com.example.wear_demo"
minSdk = 30 // Wear OS 3+
targetSdk = 35
}
}
dependencies {
implementation("androidx.wear:wear:1.3.0")
}
Un dettaglio spesso dimenticato: imposta lo sfondo della finestra di avvio su nero. Su un display OLED di un orologio uno splash bianco è fastidioso e consuma di più. Modifica android/app/src/main/res/values/styles.xml e la variante values-night.
Adattare la UI allo schermo dell'orologio
Il pacchetto di riferimento è wear_plus, erede del vecchio wear:
dependencies:
wear_plus: ^1.3.0
Espone tre strumenti fondamentali: WatchShape (forma dello schermo), AmbientMode (modalità a basso consumo) e InheritedShape.
import 'package:flutter/material.dart';
import 'package:wear_plus/wear_plus.dart';
class WatchApp extends StatelessWidget {
const WatchApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: Colors.black,
useMaterial3: true,
),
home: WatchShape(
builder: (context, shape, child) {
final isRound = shape == WearShape.round;
return Padding(
// su schermo rotondo gli angoli non sono utilizzabili
padding: EdgeInsets.all(isRound ? 24 : 12),
child: child!,
);
},
child: const HomeScreen(),
),
);
}
}
Linee guida pratiche per la UI:
- Sfondo nero: sui pannelli OLED i pixel neri sono spenti, quindi è una scelta di design e di autonomia.
- Un'informazione per schermata: niente dashboard, niente tabelle. Testo grande, target touch da almeno 48dp.
- Niente AppBar classica: lo spazio verticale è prezioso; usa un titolo in cima alla lista che scorre via.
- Attenzione agli angoli: su schermo rotondo il contenuto va inscritto in un cerchio, con padding generoso.
- Evita input testuali: la tastiera su un orologio è un'esperienza pessima; preferisci scelte predefinite o dettatura vocale.
Input rotativo: corona e bezel
Quasi tutti gli orologi Wear OS hanno una corona rotante o un bezel. Il motore Flutter traduce questi eventi in pointer scroll, quindi un ListView con ScrollController reagisce già di suo. Per aggiungere la classica scrollbar curva laterale che compare durante la rotazione, il pacchetto rotary_scrollbar è la soluzione più rapida:
class RotaryList extends StatefulWidget {
const RotaryList({super.key});
@override
State<RotaryList> createState() => _RotaryListState();
}
class _RotaryListState extends State<RotaryList> {
final _controller = ScrollController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RotaryScrollbar(
controller: _controller,
child: ListView.builder(
controller: _controller,
padding: const EdgeInsets.symmetric(vertical: 32),
itemCount: 20,
itemBuilder: (context, index) => ListTile(
title: Text('Elemento $index'),
onTap: () {},
),
),
);
}
}
Se usi un PageView verticale, imposta pageSnapping: true e una ScrollPhysics che renda lo snap deciso: sull'orologio la navigazione a "schede" verticali è il pattern più naturale.
Ambient mode: l'app sempre visibile
Quando l'utente abbassa il polso, Wear OS porta l'app in ambient mode: lo schermo resta acceso ma il sistema si aspetta una UI monocromatica, con pochi pixel accesi e aggiornamenti al massimo una volta al minuto.
import 'package:intl/intl.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return AmbientMode(
builder: (context, mode, child) {
final isAmbient = mode == WearMode.ambient;
return Container(
color: Colors.black,
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
DateFormat('HH:mm').format(DateTime.now()),
style: TextStyle(
fontSize: 40,
// in ambient: solo grigio chiaro, niente colori saturi
color: isAmbient ? Colors.white70 : Colors.tealAccent,
fontWeight: isAmbient ? FontWeight.w300 : FontWeight.bold,
),
),
if (!isAmbient) ...[
const SizedBox(height: 8),
const Text('Tocca per iniziare'),
],
],
),
);
},
);
}
}
Regole d'oro per l'ambient:
- Stoppa animazioni, timer e stream: in ambient ogni frame costa batteria. Se hai un
Ticker, mettilo in pausa. - Niente aree bianche piene: usa testo sottile su nero.
- Aggiorna al massimo ogni minuto: Flutter non riceve il tick di sistema "ambient update" senza codice nativo; in pratica conviene ridisegnare solo sui cambi di stato.
- Valuta se l'always-on serve davvero: per molte app è meglio lasciare che lo schermo si spenga.
Parlare con lo smartphone
Molte app orologio sono un satellite di un'app sul telefono. Per scambiare messaggi e stato si usa la Data Layer API di Wear OS, esposta a Dart dal pacchetto watch_connectivity (che su iOS copre anche watchOS, utile se hai già un'app companion):
import 'package:watch_connectivity/watch_connectivity.dart';
class WatchBridge {
final _watch = WatchConnectivity();
Future<bool> get isReady async =>
await _watch.isSupported && await _watch.isPaired;
/// Messaggi "fire and forget", richiedono che il peer sia raggiungibile.
Future<void> inviaComando(String azione) async {
if (await _watch.isReachable) {
await _watch.sendMessage({'action': azione, 'ts': DateTime.now().toIso8601String()});
}
}
/// Stato condiviso e persistente: viene sincronizzato appena possibile.
Future<void> aggiornaStato(Map<String, dynamic> stato) =>
_watch.updateApplicationContext(stato);
Stream<Map<String, dynamic>> get messaggi => _watch.messageStream;
}
In generale: usa sendMessage per azioni immediate (es. "metti in pausa la riproduzione") e updateApplicationContext per sincronizzare lo stato (es. token, preferenze, ultimo valore noto), perché viene consegnato anche se il peer è temporaneamente offline.
Se invece l'orologio ha connettività autonoma (Wi-Fi o LTE), progetta l'app come standalone: chiama direttamente le tue API e tieni una cache locale, evitando di dipendere dal telefono.
Permessi, sensori e salute
Sull'orologio sono particolarmente rilevanti:
- Battito cardiaco e sensori corporei: richiedono
BODY_SENSORS(eBODY_SENSORS_BACKGROUNDper l'uso in background). Dal lato Flutter si accedono tramite platform channel o pacchetti dedicati comehealth. - Posizione: il GPS dell'orologio consuma molto; usa
geolocatorcon accuratezza ridotta quando non serve il massimo. - Vibrazione: usa
HapticFeedback.lightImpact()sui tap, sull'orologio è il feedback principale.
Limiti da conoscere prima di partire
Flutter copre bene la schermata dell'app, ma non tutte le superfici di Wear OS:
- Tiles e Complications (le schede scorrevoli e i widget sul quadrante) devono essere scritte in Kotlin con ProtoLayout: non sono renderizzate dal motore Flutter. Puoi comunque convivere, aggiungendo un modulo nativo al progetto Android.
- Watch face: stesso discorso, serve l'API nativa Watch Face Format.
- Dimensione dell'APK: il motore Flutter pesa qualche MB; su un orologio con storage limitato conviene usare
--split-per-abio un App Bundle e rimuovere asset e font inutilizzati. - Memoria e GPU: gli orologi hanno hardware modesto. Evita blur, ombre pesanti e liste con molte immagini; Impeller aiuta, ma la frugalità resta la regola.
Build, test e pubblicazione
Per lo sviluppo puoi usare l'emulatore Wear OS incluso in Android Studio (Device Manager → categoria Wear OS, scegli una forma rotonda per testare davvero il layout) oppure un orologio fisico con debug ADB via Wi-Fi.
flutter devices
flutter run -d <id-emulatore-wear>
flutter build appbundle --release
Sull'emulatore puoi simulare l'ambient mode con:
adb shell am broadcast -a com.google.android.wearable.action.ENTER_AMBIENT
adb shell am broadcast -a com.google.android.wearable.action.EXIT_AMBIENT
Per la pubblicazione hai due strade: un'app standalone con un proprio applicationId e una scheda Play dedicata al form factor Wear OS, oppure un'app distribuita insieme a quella mobile (stesso package, APK multipli). Nella Play Console dovrai dichiarare il supporto a Wear OS e superare la review sulle linee guida di qualità specifiche: avvio rapido, nessun testo tagliato sullo schermo rotondo, gestione corretta dell'ambient e assenza di riferimenti a interazioni "da telefono".
Conclusioni
Portare Flutter su Wear OS è tecnicamente semplice — il grosso del lavoro è di design. Configura il manifest per il form factor watch, usa wear_plus per forma e ambient mode, supporta la corona rotante, riduci la UI all'essenziale e decidi fin dall'inizio se l'app è standalone o companion. Con queste basi puoi riutilizzare la logica di dominio dell'app mobile e aggiungere una superficie al polso con uno sforzo contenuto.