--- name: mobile-flutter-design description: Apply Apple Human Interface Guidelines to Flutter apps on iOS. Use when designing or refining Flutter screens that should feel native on iOS — layout, navigation, typography, dark mode, spacing, and accessibility — expressed in Flutter (Cupertino + Material) rather than SwiftUI. --- # Flutter iOS Design Adapts Apple's Human Interface Guidelines (HIG) to Flutter. The HIG principles are framework-agnostic; this skill re-expresses them in idiomatic Flutter so a Flutter app can feel native on iOS without leaving Dart. Converted from the SwiftUI-based `mobile-ios-design` skill. ## When to Use This Skill - Designing/refining Flutter screens that must feel native on iOS - Choosing Cupertino vs. Material widgets for an iOS-leaning app - Getting spacing, typography, and hierarchy to HIG standards in Flutter - Implementing iOS-style navigation (stacks, tabs, bottom sheets) in Flutter - Supporting Dynamic Type (text scaling), Dark Mode, and accessibility - Making glanceable, uncluttered dashboards and lists ## Cupertino vs. Material — the first decision Flutter ships two design languages. Pick deliberately: - **Material (`MaterialApp`, `Scaffold`, `NavigationBar`)** — the pragmatic default for cross-platform apps. Looks clean on iOS; not pixel-native but users accept it. **If the app already commits to Material, stay in Material** and apply HIG *principles* (spacing, hierarchy, restraint) rather than swapping widgets. Don't mix half-and-half — inconsistency reads as broken. - **Cupertino (`CupertinoApp`, `CupertinoPageScaffold`, `CupertinoNavigationBar`)** — pixel-native iOS look (system fonts, iOS transitions, sliding back gesture). Use when "feels exactly like an Apple app" is a hard requirement, or for a specific surface (a settings screen, an action sheet). - **Hybrid** — Material shell + Cupertino for specific interactions (`showCupertinoModalPopup` for action sheets, `CupertinoContextMenu`). Fine when intentional and consistent. > This project (FieldFlowSales mobile) is a **Material** app (`NavigationBar`, > `showModalBottomSheet`, custom cards). Apply HIG at the principle level here; > reach for Cupertino widgets only for genuinely iOS-idiomatic moments. ## Core Concepts ### 1. HIG Principles (framework-agnostic) - **Clarity** — legible text, precise icons, subtle adornment. Prefer content over chrome. In Flutter: lean on `Theme.of(context).textTheme`, keep borders and shadows light, avoid decorative gradients competing with content. - **Deference** — the UI helps users understand content without competing with it. Translate to restraint: one accent color per surface, generous whitespace, no card-in-card-in-card nesting. - **Depth** — layers and motion convey hierarchy. In Flutter: elevation/shadow for cards, `showModalBottomSheet` sliding up for transient tasks, hero/route transitions for drill-down. ### 2. Layout — SwiftUI → Flutter | SwiftUI | Flutter | | --- | --- | | `VStack(alignment:spacing:)` | `Column(crossAxisAlignment:, children:[…, SizedBox(height:)…])` | | `HStack` | `Row` | | `ZStack` | `Stack` | | `Spacer()` | `Spacer()` or `Expanded` | | `.padding()` | `Padding(padding: EdgeInsets…)` / `Container(padding:)` | | `LazyVStack` / `List` | `ListView` / `ListView.builder` | | `LazyVGrid(adaptive)` | `GridView.builder(gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 200))` | | `LazyVGrid(fixed 3)` | `GridView.count(crossAxisCount: 3)` | ```dart // VStack(alignment: .leading, spacing: 12) equivalent Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Title', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 12), Text('Subtitle', style: Theme.of(context).textTheme.bodyMedium ?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)), ], ) // HStack with Spacer + trailing accent Row( children: [ Icon(Icons.star, color: Colors.amber), const SizedBox(width: 8), const Text('Featured'), const Spacer(), Text('View All', style: TextStyle(color: Theme.of(context).colorScheme.primary)), ], ) ``` Spacing scale: HIG leans on multiples of 4/8. Use a consistent scale (4, 8, 12, 16, 24) rather than arbitrary values. Standard screen edge padding is 16. ### 3. Navigation | SwiftUI | Flutter | | --- | --- | | `NavigationStack` + `NavigationLink` | `Navigator` + `MaterialPageRoute` (or `go_router`) | | `.navigationTitle` | `AppBar(title:)` | | `TabView` | `Scaffold(bottomNavigationBar: NavigationBar)` + `IndexedStack` | | `.sheet` | `showModalBottomSheet` | | `.confirmationDialog` | `showModalBottomSheet` of actions, or `showCupertinoModalPopup` | | `.fullScreenCover` | `Navigator.push` with `fullscreenDialog: true` | Full patterns and code in `references/flutter-navigation.md`. Key HIG rule: push for drill-down (a back stack), sheet for a transient/self-contained task, tabs for peer sections. Keep tab pages alive with `IndexedStack` so switching doesn't rebuild/refetch (this app learned that the hard way — it fixed a tab flicker). ### 4. System Integration - **Icons**: SF Symbols → Flutter `Icons` (Material) or `CupertinoIcons`. For an iOS feel use `CupertinoIcons`; for a Material app, `Icons` is consistent. Pick one set per app. - **Dynamic Type**: SwiftUI scales automatically; Flutter needs care. Never hardcode font sizes for body copy where you can use `textTheme`. Respect the user's setting via `MediaQuery.textScalerOf(context)` — don't clamp it to 1.0 (that breaks accessibility). Test at 200% text size; ensure rows grow, not clip. - **Haptics**: `HapticFeedback.selectionClick()` / `.lightImpact()` on meaningful actions (toggle, pull-to-refresh complete) — iOS users expect it. ### 5. Visual Design - **Semantic colors (dark-mode safe)**: SwiftUI's `.primary`/`.secondary` → Flutter's `ColorScheme`. Use `Theme.of(context).colorScheme.onSurface`, `onSurfaceVariant`, `surface`, `primary` instead of literal `Colors.black`/ `Colors.grey.shadeXXX`. Literal colors are the #1 cause of broken dark mode. If the app is light-only today, still prefer `colorScheme` so dark mode is a theme change, not a rewrite. - **Materials/blur**: SwiftUI `.ultraThinMaterial` → `BackdropFilter` with `ImageFilter.blur` behind a translucent color (use sparingly; it's costly). - **Elevation**: card shadow via `BoxShadow(color: Colors.black.withValues(alpha: 0.06), blurRadius: 8, offset: Offset(0, 4))` or `Material(elevation:)`. HIG shadows are *subtle* — low opacity, soft blur. Avoid heavy drop shadows. - **Corner radius**: 12 for cards, 20+ for pills/sheets, matching iOS softness. ## Quick Start Component (SwiftUI FeatureCard → Flutter) ```dart class FeatureCard extends StatelessWidget { final String title; final String description; final IconData icon; final VoidCallback? onTap; const FeatureCard({ super.key, required this.title, required this.description, required this.icon, this.onTap, }); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: scheme.surface, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Row( children: [ Container( width: 44, height: 44, alignment: Alignment.center, decoration: BoxDecoration( color: scheme.primary.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Icon(icon, color: scheme.primary), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 4), Text( description, maxLines: 2, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium ?.copyWith(color: scheme.onSurfaceVariant), ), ], ), ), Icon(Icons.chevron_right, color: scheme.outline), ], ), ), ); } } ``` ## Best Practices (Flutter-adapted) 1. **Prefer `ColorScheme`/`textTheme` over literals** — automatic dark mode and consistent typography. Reserve literal colors for brand accents only. 2. **One icon set per app** (`Icons` or `CupertinoIcons`) — don't mix. 3. **Respect text scaling** — use `textTheme`, don't hardcode body sizes, don't clamp `textScaler`. Let rows grow. 4. **Add semantics** — wrap icon-only buttons with `Semantics(label:)` or use `IconButton(tooltip:)`; label meaningful images. Test with TalkBack/VoiceOver. 5. **Respect safe areas** — wrap edge content in `SafeArea`; don't hardcode status-bar/home-indicator padding. 6. **Keep tab pages alive** — `IndexedStack` (or `AutomaticKeepAliveClientMixin`) so switching tabs doesn't refetch and flicker. 7. **Lazy lists** — `ListView.builder`/`GridView.builder` for long content, never a `Column` of hundreds of children. 8. **Glanceable first** — put what a user needs at a glance at the top; collapse detail behind taps (e.g. a summary row that expands). Cards should be slim. 9. **Test on device** — haptics, scroll physics, and true colors don't show in a simulator screenshot. ## Common Issues (Flutter) - **Broken dark mode** — literal `Colors.black`/`Colors.grey.shadeXXX`. Use `colorScheme`. - **Clipped text at large font sizes** — fixed heights on rows containing text; let them size to content, use `Flexible`/`Expanded`. - **Jank on long lists** — non-lazy `Column`/`ListView(children:[…])`; switch to `.builder`. - **Overflowing dialogs/sheets** — unbounded content in a `Dialog`; give the body `overflow-y` via a scroll view and cap tall inner lists (this app hit exactly this in a modal — `DialogContent` was `display:grid`, so a flex child didn't scroll; the fix was `overflow` on the content itself + a height-capped list). - **Tab flicker / lost scroll** — recreating tab pages on switch; use `IndexedStack`. - **Nested-scroll gesture conflict** — a scrollable inside a scrollable fighting for the drag. Avoid unless the inner region genuinely needs its own scroll; prefer letting the page scroll. ## References - `references/flutter-navigation.md` — stacks, tabs, bottom sheets, dialogs, deep links, transitions (SwiftUI navigation → Flutter). - `references/flutter-components.md` — lists, forms/inputs, buttons/menus, sheets, loading/skeletons, async images, animations, gestures (SwiftUI components → Flutter).