# settings_ui > Flutter package for settings screens that look native on each platform from one API: iOS 26-style grouped cards on iOS, macOS 26-style System Settings forms on macOS, Windows 11-style cards on Windows, Android 16-style cards on Android and Fuchsia, GNOME (libadwaita) boxed lists on Linux, and Chrome-style cards on the web. The style is picked at runtime. Import `package:settings_ui/settings_ui.dart`. The 3.x API is unchanged in 4.x: 4.0.0 removes and renames nothing. What differs is the Flutter UI library the app must use (see below) and the look. 4.0.0 adds: - macOS (System Settings), Windows 11 and GNOME (libadwaita) styles. In 3.x, macOS and Windows used the iOS style and Linux the Android one. - The switches of the iOS, macOS, Windows and GNOME styles, public so you can use them on their own: `CupertinoSettingsSwitch` (the iOS 26 switch, replacing `CupertinoSwitch`), `MacosSettingsSwitch` (+ `MacosSettingsSwitchSize`), `FluentSettingsSwitch` and `AdwaitaSettingsSwitch`. Also `AdwaitaPanDownIcon`, the arrow of GNOME combo rows. - `SettingsDestination` and `SettingsTile.navigation(destination:)`: tiles that open pages with the platform's page header. - `SettingsSplitView`, with `SettingsSplitController` and `SettingsSplitLayout`: list and page side by side on tablets, foldables, desktop and the web. - The `SettingsThemeData` fields `selectedTileColor`, `selectedTileTextColor`, `selectedTileIconColor` and `listPaneBackground`, for the split view. - Bug fixes, among them: `SettingsList.brightness` is no longer ignored, iOS simple tiles show `value`, `crossAxisAlignment: CrossAxisAlignment.start` works on wide screens, the side padding follows the list's own width, and an empty section renders nothing. The CHANGELOG lists them all. ## Install and requirements | App imports | Add | Requires | |---|---|---| | `package:material_ui/material_ui.dart`, `package:cupertino_ui/cupertino_ui.dart` | `settings_ui: ^4.0.0` | Flutter >=3.44, Dart >=3.12 | | `package:flutter/material.dart`, `package:flutter/cupertino.dart` | `settings_ui: ^3.0.1` | Flutter >=3.16 | - 4.x reads colors, brightness and platform from the `Theme` and `CupertinoTheme` of material_ui and cupertino_ui. In an app still on `package:flutter/material.dart` it compiles, but uses default colors, stays light in dark mode, and debug builds print once: `settings_ui: SettingsList found no Theme from package:material_ui ...`. - settings_ui depends on material_ui and cupertino_ui, but an app that imports them must list them in its own `pubspec.yaml` too (`flutter pub add material_ui cupertino_ui`). Otherwise the `depend_on_referenced_packages` lint fails. - Moving an app to material_ui: `dart fix --apply --code=migrate_design_widgets`, see . ## Minimal example (4.x) ```dart import 'package:material_ui/material_ui.dart'; import 'package:settings_ui/settings_ui.dart'; void main() => runApp(const MaterialApp(home: SettingsScreen())); class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @override State createState() => _SettingsScreenState(); } class _SettingsScreenState extends State { bool _notifications = true; // load from and save to your store @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Settings')), body: SettingsList( sections: [ SettingsSection( title: const Text('General'), tiles: [ SettingsTile.navigation( leading: const Icon(Icons.language), title: const Text('Language'), value: const Text('English'), onPressed: (context) {/* push a picker screen */}, ), SettingsTile.switchTile( leading: const Icon(Icons.notifications), title: const Text('Notifications'), initialValue: _notifications, onToggle: (value) => setState(() => _notifications = value), ), ], ), SettingsSection( title: const Text('About'), tiles: [ SettingsTile( title: const Text('Version'), value: const Text('1.4.0 (57)'), ), SettingsTile.navigation( title: const Text('Open-source licenses'), onPressed: (context) => showLicensePage(context: context), ), ], ), ], ), ); } } ``` ## Widgets and parameters ### SettingsList - `sections` (required): `List`, so `SettingsSection`, `CustomSettingsSection` or your own subclass. - `platform`: `DevicePlatform?`. `null` or `DevicePlatform.device` means auto-detect (default). - `applicationType`: `ApplicationType`, default `.material`. Where light or dark comes from: `.material` reads `Theme`, `.cupertino` reads `CupertinoTheme` (then the platform brightness), for a `CupertinoApp`. `.both` reads `CupertinoTheme` when the app runs on iOS or macOS and `Theme` elsewhere; it goes by the detected platform, not by `platform:`. - `brightness`: `Brightness?`. Forces light or dark, overriding the detected brightness, in every style (the Android and web styles then use a color scheme of that brightness made from the app's primary color). Leave it null to follow the app theme. - `lightTheme`, `darkTheme`: `SettingsThemeData?`, merged over the platform defaults. - `contentPadding`: `EdgeInsetsGeometry?`, padding of the inner `ListView`. Default: none; 20 vertical / 16 horizontal on web; 12 on top (0 when the first section has a title) and 10 at the bottom on macOS; 24 horizontal (16 below 641 wide) and 33 at the bottom on Windows (36 with the last card's gap); 24 on top in the GNOME style. When the list is wider than 810 (680 on web, 640 on macOS, 1000 on Windows), side padding keeps the tiles in a column of that width. The GNOME style narrows it like `AdwClamp`: the full width up to 400, then easing to at most 600 (in sp: they grow as much as body text does, also with Android 14+ non-linear font scaling). The width is the list's own, so it works in split views and side panels. - `crossAxisAlignment`: `CrossAxisAlignment`, default `center`. `start` puts that column at the start edge (left in LTR, right in RTL) on wide lists instead of centering it. No effect when `contentPadding` is set. - `scrollController`, `physics`, `shrinkWrap` (default `false`): passed to the inner `ListView`. ### SettingsSection - `tiles` (required): `List`. With an empty list the section renders nothing, title included. - `title`: `Widget?`. Use sentence case ("Notifications"), like iOS 26. Apps written for 3.x often pass ALL-CAPS titles for the old iOS look; change them. - `margin`: `EdgeInsetsDirectional?` (not `EdgeInsets`). - `titlePadding`: `EdgeInsetsGeometry?`, padding around the title. ### CustomSettingsSection, CustomSettingsTile `CustomSettingsSection(child: ...)` and `CustomSettingsTile(child: ...)` render `child` as it is. Inside a `SettingsList`, `SettingsTheme.of(context).themeData` and `SettingsTheme.of(context).platform` give the resolved colors and style. ### SettingsTile None of the constructors is `const`. | Constructor | Use for | End of row | |---|---|---| | `SettingsTile(...)` | read-only values and actions (version, sign out) | never a chevron | | `SettingsTile.navigation(...)` | rows that open another screen or picker | a chevron in the iOS, macOS, Windows and web styles, GNOME's `go-next` arrow in the GNOME style, none in the Android style | | `SettingsTile.switchTile(...)` | on/off settings | `CupertinoSettingsSwitch` in the iOS style, `MacosSettingsSwitch` in the macOS style, `FluentSettingsSwitch` in the Windows style, `AdwaitaSettingsSwitch` in the GNOME style, Material `Switch` elsewhere | All constructors: - `title` (required): `Widget`. - `leading`, `trailing`: `Widget?`. - `description`: `Widget?`. Android and web: second line under the title, hidden when `value` is set. Windows: grey second line inside the card. GNOME style: dimmed second line under the title, also when `value` is set. iOS and macOS styles: footer text under the row, outside the card; the tiles after it start a new card. - `titleDescription`: `Widget?`. Second line inside the row, iOS, macOS, Windows and GNOME styles only (ignored on Android and web). In the Windows and GNOME styles it comes before `description`. - `onPressed`: `Function(BuildContext context)?`, written `onPressed: (context) { ... }`. - `enabled`: `bool`, default `true`. `false` greys the row and ignores taps. - `compact`: `bool`, default `false`. Halves the vertical padding. - Padding, all `EdgeInsetsGeometry?`, each replacing the default padding around that part: `leadingPadding`, `titlePadding`, `trailingPadding` (in the Android and web styles not on switch tiles), `descriptionPadding`, and `titleDescriptionPadding` (iOS, macOS, Windows and GNOME styles, the only ones that show `titleDescription`). `SettingsTile` and `SettingsTile.navigation` only: - `value`: `Widget?`. iOS, macOS, Windows and GNOME styles: grey (GNOME: dimmed) text at the end of the row, one line, at most half the row. Android and web: second line under the title, shown instead of `description`. `SettingsTile.navigation` only: - `destination`: `SettingsDestination?`. The page the tile opens, after `onPressed` (see below). `SettingsTile.switchTile` only: - `initialValue` (required): `bool?`. The current value. The tile is controlled and keeps no state of its own. - `onToggle` (required, nullable): `Function(bool value)?`. `null` disables the switch. - `activeSwitchColor`: `Color?`. On-state color of the switch. - It has no `value` parameter. Taps on switch tiles: in the Android, GNOME and web styles, tapping anywhere on the row toggles the switch and `onPressed` is never called. In the iOS, macOS and Windows styles, as in their Settings apps, only the switch itself toggles; tapping the rest of the row calls `onPressed` (if set). Screen readers read each tile as its own item, a tile with `onPressed` as a button, and section titles as headings. They read a switch tile as one item, "Title, switch, on". When the row also has `onPressed` in the iOS, macOS and Windows styles, the row and its switch are two items, both named by the title. A disabled tile ignores taps and the keyboard. Windows Settings writes "On"/"Off" before each switch. The package does not add it (it would be English only). For that look, pass a localized label as `trailing` on switch tiles, only on Windows since `trailing` shows in every style: `trailing: Theme.of(context).platform == TargetPlatform.windows ? Text(isOn ? l10n.on : l10n.off) : null`. ### CupertinoSettingsSwitch The iOS 26 switch, drawn in Flutter: a 63x28 track with a pill thumb that turns into a Liquid Glass-style lens while pressed or dragged. Switch tiles in the iOS style use it, and you can use it on its own anywhere. - `value` (required): `bool`. - `onChanged` (required, nullable): `ValueChanged?`. `null` disables it (half opacity). - `activeTrackColor`: `Color?`, default system green. - `inactiveTrackColor`: `Color?`, default `#C5C5C7` light, `#5A5A5E` dark. It is controlled (rebuild it with the new value), plays a light haptic on iOS and macOS, and its lens paints up to about 12.5pt past the track ends and 6pt above and below, so don't clip it tightly. ### MacosSettingsSwitch The macOS 26 System Settings switch, drawn in Flutter: a capsule track with a wider-than-tall capsule knob. Switch tiles in the macOS style use it, and you can use it on its own anywhere. - `value` (required): `bool`. - `onChanged` (required, nullable): `ValueChanged?`. `null` disables it (paler track). - `activeTrackColor`: `Color?`, default the macOS accent blue. - `inactiveTrackColor`: `Color?`, default black 10% (light) or white 10% (dark). - `size`: `MacosSettingsSwitchSize`, `.regular` (default, 36x16, the row switch) or `.large` (44x20, the main switch of a pane). It is controlled, the knob follows a drag, and Space toggles it when focused. ### FluentSettingsSwitch The Windows 11 (WinUI 3) toggle, drawn in Flutter: a 40x20 outlined track with a 12px knob that grows to 14px on hover and stretches to 17x14 while pressed; ON fills the track with the accent. Switch tiles in the Windows style use it, and you can use it on its own anywhere. - `value` (required): `bool`. - `onChanged` (required, nullable): `ValueChanged?`. `null` disables it (Windows disabled colors). - `activeTrackColor`: `Color?`, default the Windows accent `#0067C0` light, `#4CC2FF` dark; the knob turns white or black to contrast. - `inactiveTrackColor`: `Color?`, the outline and knob color when off. It is controlled, can be dragged, toggles with Space or Enter when focused, and draws the Windows focus rectangle up to 8px outside itself. ### AdwaitaSettingsSwitch The GNOME switch (GtkSwitch with libadwaita 1.10), drawn in Flutter: a 46x26 track with a round 20px knob. Switch tiles in the GNOME style use it, and you can use it on its own anywhere. - `value` (required): `bool`. - `onChanged` (required, nullable): `ValueChanged?`. `null` disables it (half opacity). - `activeTrackColor`: `Color?`, default GNOME blue `#3584E4`. - `inactiveTrackColor`: `Color?`, default black 12% light, white 15% dark. - `brightness`: `Brightness?`, default from the `CupertinoTheme`, then the platform. - `focusNode`, `autofocus`: keyboard focus. Space or Enter toggles it. It is controlled (rebuild it with the new value). Tap to toggle, or drag the knob past the middle. ### AdwaitaPanDownIcon GNOME's `pan-down-symbolic` arrow (16px box, 10x6 chevron, `IconTheme` color by default; `color`, `size`). Build a GNOME combo row with `trailing: Row(mainAxisSize: MainAxisSize.min, children: [Text('5 minutes'), SizedBox(width: 9), AdwaitaPanDownIcon()])`; in the GNOME style, text in `trailing` gets the row's font and color. Plain `value` shows dimmed instead, without the arrow. ### SettingsDestination `SettingsDestination(id: 'network', title: ..., builder: (context) => SettingsList(...), actions: [...])`. A navigation tile with a `destination` opens the page itself: - In a `SettingsList`: pushes a `CupertinoPageRoute` (iOS, macOS and GNOME styles) or `MaterialPageRoute` (other styles) named `id`, with the package's header: iOS 26 inline title and round back button; Android's 36sp title that collapses into a 64dp bar on scroll; the macOS 52pt toolbar (back/forward capsule, 15pt semibold title); the Windows 28px semibold page title, with a back button, or a breadcrumb "Parent › Page" (parent tappable, goes back) on pages opened from a page, on one line: when it doesn't fit, the first crumbs collapse into a "…" crumb (goes back to the last hidden page) and a long title ends in an ellipsis; GNOME's flat 46px header bar (centered bold title, flat back button); Chrome's page title over the 680px column. `builder` returns only the body. - `title` (`Widget?`) defaults to the tile's title. `actions` (`List?`) go at the end of the header. - A `SettingsList` in the body takes the `platform`, `brightness`, `lightTheme`, `darkTheme` and `applicationType` it leaves unset from the list that opened the page. Nested lists elsewhere still detect their own style. - For the Android header to collapse, the body's scroll view must use the `PrimaryScrollController` (a `SettingsList` without `scrollController` does). ### SettingsSplitView A whole settings screen: list pane plus detail pane when wide, the list with pages pushed over it when narrow. Don't put it under an `AppBar`; it draws both panes' headers (and a back button in the list pane when its route can pop). Same `sections`, `platform`, `applicationType`, `brightness`, `lightTheme`, `darkTheme` as `SettingsList`, plus: - `title`: `Widget?`, the list pane title. - `initialDestinationId`: `String?`. Page two panes show until the user picks one. Default: the first destination in `sections`, unless `emptyDetailBuilder` is set. - `emptyDetailBuilder`: `WidgetBuilder?`, the detail pane with no page. - `controller`: `SettingsSplitController?` with `select(String id)`, `clearSelection()`, `selectedId`, `isSplit`; a `ChangeNotifier`. `select` before the view is built picks the first page, also in one pane (deep links). `SettingsSplitView.of(context)` / `maybeOf(context)` return it from inside the view. - `onDestinationChanged`: `ValueChanged?`, after the shown page changes, including when a layout change shows or hides the default page. For URL sync; the package has no router. - `layout`: `SettingsSplitLayout.auto` (default), `.single`, `.split`. `breakpoint`: `double?`, a width that replaces the style's rule. `listPaneWidth`: `double?`, at most half the width. Windows: sets the open pane only (the 48px rail stays; its menu button opens a pane this wide over the page, default 320). - `restorationId`: `String?`, restores the shown page (pages pushed inside a page are not restored). Defaults per style: | Style | Two panes when | List pane | Selected tile | |---|---|---|---| | iOS | width >= 600 and shortest side >= 600 (desktop: width only) | 320pt sidebar, `listPaneBackground` #E2E6F0 / #181D20, rows without cards | capsule #0080F5 / #13A4FF, white text | | macOS | width >= 560 | 232pt full-height sidebar on #EDEDED / #282828, 0.5pt edge; 32pt rows, 20pt icons, 11pt bold headers | #0070F5 / #007AFF fill (radius 8, inset 10), white semibold text; #D7D7D7 / #464646 while the app is inactive (`AppLifecycleState` not resumed) | | Windows | width >= 641 (641-1007: 48px icon rail with tooltips and a menu button that opens a 320px pane over the page) | 300px `NavigationView` pane from 1008, page color #F3F3F3 / #202020; items >= 36px, radius 4, 16px icons | #EAEAEA / #2D2D2D plus a 3x16 accent pill at the start edge that slides between items | | Android, Fuchsia | width >= 720 and shortest side >= 600 (desktop: width only) | 36.36% of the width on `surfaceDim`, phone cards; no leading icons under 380dp | card filled with `surfaceContainer` (the page color) | | Linux (GNOME) | width > 550sp | 25% of the width, clamped 180-280sp, on #EBEBED / #2E2E32, 1px border; 43px rows, radius 9, 16px icons; 46px header bar with centered bold title | #D8D8DB (dark: white 10%), text unchanged; hover 7%, pressed 16%. One pane: the sidebar fills the window, no row selected | | web | width > 980 | Chrome menu, 266px, 40px items, no descriptions | tint of `primary` / `primary` in dark, end-rounded pill | The macOS, Windows and GNOME list panes are sidebars (System Settings, Windows Settings `NavigationView`, GNOME Settings `AdwNavigationSplitView`), not cards. The detail pane gets the style's page header (see SettingsDestination). Their pages keep their own look, and their own column and margins, in the detail pane; iOS and Android pages fill it. In these three styles: Up and Down move between sidebar rows and stop at the first and last, Enter/Space opens one (macOS opens on focus), Tab moves between panes. A click focuses the row, so the keys go on from there; its focus ring shows only after a key press. GNOME's sp sizes grow as much as body text does, also with Android 14+ non-linear font scaling. A plain `Icon` as a macOS sidebar `leading` is tinted with the accent; pass your own 20pt widget for System Settings' colored squircles. Behavior: only tiles with a `destination` whose id is shown are highlighted, and only with two panes. Destinations of tiles inside a page push inside the detail pane. Two panes to one: a page the user opened stays on top of the list (with its pushed pages and state), the default page is dropped. Back (system back, Android predictive back, the iOS back swipe) closes, in order: a route a list tile pushed itself in one pane (`Navigator.push`, a menu, a sheet), pages pushed inside the detail pane, the page over the list, then the screen; a `PopScope` in a page can veto, the back swipe included. While a route pushed from the list is open in one pane, the view stays in one pane even if the window widens. When the tile of the picked page goes away (a conditional page), the page closes (two panes show the default page, one pane the list) and doesn't come back with its tile; for a tile inside a `CustomSettingsSection`, call `clearSelection()` when you remove it. A separating hinge (hinge, or fold half opened) gets one pane on each side. RTL puts the list on the right. ### Other exports `DevicePlatform` (`android`, `fuchsia`, `iOS`, `linux`, `macOS`, `windows`, `web`, `device`), `PlatformUtils.detectPlatform(context)`, `ApplicationType`, `SettingsTileType`, `MacosSettingsSwitchSize`, `SettingsTheme`, `SettingsThemeData`, `AbstractSettingsSection`, `AbstractSettingsTile`, `SettingsSplitController`, `SettingsSplitLayout`. ## Platform styles | `DevicePlatform` | Style | Look | |---|---|---| | `iOS` | iOS 26+ | 26pt rounded cards with 20pt side margins, 52pt rows, 17pt text, 17pt semibold section headers, `description` as grey footers, `CupertinoSettingsSwitch`, chevron on navigation tiles. Fixed iOS system colors. | | `macOS` | macOS 26+ System Settings | 12pt rounded cards (#F7F7F7 on white, #252525 on #1E1E1E) with 20pt side margins, 36pt rows (48pt with a leading icon), 13pt text, 13pt semibold headers above the cards, `description` as 11pt grey footers, `MacosSettingsSwitch`, chevron on navigation tiles, 640pt column. No hover highlight; pressed rows tint. Fixed macOS system colors. | | `windows` | Windows 11 | one card per tile (4px corners, hairline border, 3px apart, 70px min height), 14px titles, 12px grey descriptions and values at the end of the row, 14px semibold section headers, `FluentSettingsSwitch`, chevron on navigation tiles, hover/pressed/focus states on clickable cards, 1000px column with 24px margins. Fixed Windows 11 colors. | | `linux` | GNOME (libadwaita 1.10) | one card per section (12px corners, soft shadow), 54px rows with full-width separators, bold 14.67px group titles, 14.67px titles, `description` as a dimmed subtitle, `value` dimmed at the end, `AdwaitaSettingsSwitch`, `go-next` arrow on navigation tiles, hover and focus highlights, 600px column. Fixed GNOME colors. | | `android`, `fuchsia` | Android 16+ | each tile on its own card, 2dp apart, 20dp corners at the ends of a group; 16sp titles; Material `Switch` with a check or cross on the thumb; no chevron. Colors from `ColorScheme`. | | `web` | Chrome settings | cards with 8px corners and a light shadow, 14px titles, 13px descriptions, chevron on navigation tiles, 680px column. Colors from `ColorScheme`. | | platforms added by Flutter forks (e.g. OpenHarmony) | iOS 26+ | | Auto-detection: `kIsWeb` gives `web` in every browser, phones included. Otherwise it uses `Theme.of(context).platform`, so `ThemeData(platform: ...)` and `TargetPlatformVariant` in tests change the style. ## Theming with SettingsThemeData Defaults: the Android and web styles derive colors from `Theme.of(context).colorScheme` (Material 3). The iOS style uses fixed iOS system colors (grouped grey page, white or #1C1C1E cards), the macOS style fixed macOS ones (white or #1E1E1E page, #F7F7F7 or #252525 cards), the Windows style fixed Windows 11 colors (#F3F3F3 page, #FBFBFB cards; #202020 and #2B2B2B in dark mode) and the GNOME style fixed libadwaita colors (#FAFAFB page and white cards, #222226 and white 8% in dark mode); these four ignore `ColorScheme`. Light or dark follows the app theme (see `applicationType`, `brightness`). Override per brightness with `SettingsList(lightTheme: SettingsThemeData(...), darkTheme: SettingsThemeData(...))`. `SettingsThemeData` has a `const` constructor; fields left null keep the default. All fields are optional: - `settingsListBackground` (`Color?`): page background. - `settingsSectionBackground` (`Color?`): card background. - `dividerColor` (`Color?`): line between tiles in the iOS, macOS, GNOME and web styles, card border in the Windows style (Android cards have none). - `tileHighlightColor` (`Color?`): pressed row. The GNOME style hovers with 3/8 of its opacity. - `titleTextColor` (`Color?`), `titleTextStyle` (`TextStyle?`): section titles. In the iOS style the color also applies to `titleDescription` and `description`. - `settingsTileTextColor` (`Color?`), `tileTextStyle` (`TextStyle?`): tile titles. - `tileDescriptionTextColor` (`Color?`): `description` and `value` on Android and web; `description` and `titleDescription` in the macOS, Windows and GNOME styles. - `tileDescriptionTextStyle` (`TextStyle?`): `description` everywhere, `value` on Android, GNOME and web, and `titleDescription` in the macOS, Windows and GNOME styles. - `trailingTextColor` (`Color?`): `value` in the iOS, macOS, Windows and GNOME styles. - `leadingIconsColor` (`Color?`): leading and trailing icons, and the chevron (lighter in the macOS style). - `inactiveTitleColor` (`Color?`): title and icons of a disabled tile. - `inactiveSubtitleColor` (`Color?`): `description` and `value` of a disabled tile on Android, Windows, GNOME and web, `titleDescription` in the macOS style. - `inactiveSwitchColor` (`Color?`): switch of a disabled tile. Without it, the macOS style draws a paler accent (System Settings) instead of `inactiveTitleColor`. - `selectedTileColor`, `selectedTileTextColor`, `selectedTileIconColor` (`Color?`): the selected tile in a `SettingsSplitView`'s list pane. - `listPaneBackground` (`Color?`): a `SettingsSplitView`'s list pane with two panes (web: `settingsListBackground`). ## Testing - 4.x: pump the screen inside `MaterialApp` or `CupertinoApp` from material_ui or cupertino_ui, and import `Switch` and `Icons` from those packages in tests too. - `flutter test` renders the Android style by default. Cover the iOS style with `variant: TargetPlatformVariant.only(TargetPlatform.iOS)` (and the macOS style with `TargetPlatform.macOS`) instead of setting `platform:` in app code. - Find switches with `find.byType(Switch)` (Android, web), `find.byType(CupertinoSettingsSwitch)` (iOS; it is not a `CupertinoSwitch`), `find.byType(MacosSettingsSwitch)` (macOS), `find.byType(FluentSettingsSwitch)` (Windows) or `find.byType(AdwaitaSettingsSwitch)` (Linux). Tap the switch itself: tapping the title only toggles on Android, Linux and web. ## Rules for agents Do: - Choose the version from the app's imports (table above). Don't add 4.x to an app still on `package:flutter/material.dart`, and don't migrate the app unless the user asks. - With 4.x, add `material_ui` and `cupertino_ui` to the app's `pubspec.yaml` when its code imports them. - Leave `platform` unset so each OS gets its own style. Set it only when the user wants one look everywhere. - Use `SettingsTile` to show a value or run an action without a chevron (version, sign out, delete account). Use `SettingsTile.navigation` only when the row opens another screen or picker. Use `SettingsTile.switchTile` for on/off. - Treat `initialValue` as the current value: in `onToggle`, update and persist the state, then rebuild. Give every switch an `onToggle`, because `null` disables it. - Write section titles in sentence case. - Build tiles with collection `if`. A section left with no tiles renders nothing, so it needs no extra check. - Wrap other widgets in `CustomSettingsTile` or `CustomSettingsSection`. Plain widgets aren't accepted in `tiles` or `sections`. - Give `SettingsList` a bounded height (`Scaffold.body`, `Expanded`). Inside another scroll view, set `shrinkWrap: true` and `physics: const NeverScrollableScrollPhysics()`. - Control dark mode through the app theme (`MaterialApp.themeMode`, `ThemeData`). In a `CupertinoApp`, pass `applicationType: ApplicationType.cupertino`. Use `brightness:` only to force one mode. - Put explanatory text in `description`, best on the last tile of a section, since the iOS and macOS styles show it as a footer that ends the card. - Change colors through `SettingsThemeData`, per brightness. - For sub-pages, prefer `SettingsTile.navigation(destination: SettingsDestination(...))` over `Navigator.push` with a hand-built `Scaffold` and app bar, and return only the body from `builder`. - For tablets, foldables, desktop and the web, use `SettingsSplitView` as the whole screen, with a unique `id` on every destination. Test both layouts by setting `tester.view.physicalSize` (for example 402x874 and 1210x834, `devicePixelRatio = 1`). Don't: - Don't write `const SettingsTile(...)` or put tiles in a `const` list. - Don't pass strings. `title`, `value`, `description` and `leading` are widgets such as `Text` and `Icon`. - Don't write `onPressed: () {}`. It takes a context: `onPressed: (context) {}`. - Don't put toggle logic in a switch tile's `onPressed`. It never runs on Android, Linux and web, and on iOS, macOS and Windows it runs for taps beside the switch, not on it. - Don't set `value` and `description` on one tile expecting both on Android and web. Only `value` shows. - Don't expect `titleDescription` to show on Android or web. - Don't pass `EdgeInsets` to `SettingsSection.margin`. It takes `EdgeInsetsDirectional`. - Don't expect `ColorScheme` or `CupertinoThemeData.primaryColor` to recolor the iOS, macOS, Windows or GNOME styles. Use `SettingsThemeData` (and `activeSwitchColor`). - Don't hardcode colors on tiles; they break dark mode. - Don't look for `CupertinoSwitch` in 4.x tests. The iOS style uses `CupertinoSettingsSwitch`, the macOS style `MacosSettingsSwitch`, the Windows style `FluentSettingsSwitch` and the GNOME (Linux) style `AdwaitaSettingsSwitch`, not a Material `Switch`. - Don't wrap `SettingsSplitView` in a `Scaffold` with an `AppBar`, and don't give a destination's body its own app bar: the package draws the headers. - Don't call `SettingsSplitController.select` with the id of a page that only exists inside another page. It takes top-level destination ids. - 3.x only: the iOS style of a plain `SettingsTile` doesn't show `value`. Use `SettingsTile.navigation` or `trailing` there. Fixed in 4.0.0. ## Recipe: build a settings screen for an app Follow these steps when the user asks you to add or build a settings screen with settings_ui. The goal is a store-ready screen in which every row works. 1. Inspect the app before changing anything, and tell the user what you found: - Flutter version and UI imports: `package:material_ui` / `package:cupertino_ui` -> `settings_ui: ^4.0.0`; `package:flutter/material.dart` -> `settings_ui: ^3.0.1` (see Install and requirements). Don't migrate the app unless the user asks. - The platforms it ships (`ios/`, `android/`, `macos/`, `windows/`, `linux/`, `web/`), and whether it runs on iPad, tablets or foldables. - State management, persistence (shared_preferences, Hive, secure storage...), localization, routing, and whether the app supports light, dark and system theme modes. - Existing preferences and feature flags, sign-up and sign-in, in-app purchases, permissions in `ios/Runner/Info.plist` and `android/app/src/main/AndroidManifest.xml`, and any existing settings UI. 2. Propose grouped sections that follow Apple's Human Interface Guidelines and Material conventions (for example: account, appearance, notifications, privacy, support, about; destructive actions last). For each row give the tile type, the state behind it and where it is stored. If the app runs on tablets, desktop or the web, propose a `SettingsSplitView` (list and page side by side). Wait for the user's OK before adding dependencies, adding screens or moving existing code. 3. Build it with settings_ui, following the app's existing patterns and the Rules for agents above: - `SettingsTile.switchTile` for on/off, `SettingsTile.navigation` for rows that open a page or picker, plain `SettingsTile` for values and actions (no chevron). Section titles in sentence case. - Settings sub-pages: `destination: SettingsDestination(id: ..., builder: ...)` on the navigation tile. The builder returns only the body; the package draws the header and back button. - On tablets, desktop and the web: `SettingsSplitView` as the whole screen, with no app bar above it and a unique `id` on every destination. - Wire every row to real state that survives a restart. No placeholder rows, empty handlers or TODOs. Permission rows show the real OS status and open system settings when denied. - Use the app's localization for strings and its router for other screens. Leave `platform` unset, so iOS, Android, macOS, Windows, Linux and the web each get their own style. 4. Add these rows when they apply. Ask the user for URLs and email addresses; never invent them. - Version and build number (package_info_plus). Open-source licenses (`showLicensePage`). - Privacy policy, terms of use and contact support (url_launcher). - Restore purchases, if the app sells subscriptions or non-consumable purchases. - Sign out and Delete account, if users can create accounts. Deletion starts in the app, asks for confirmation and calls the real backend (App Store Review Guideline 5.1.1(v); Google Play has a similar rule). 5. Add widget tests (see Testing): the screen renders in the style of every platform the app ships (`TargetPlatformVariant`), every toggle persists, links and actions fire, and a split view shows one pane at 402x874 and two at 1210x834 (`tester.view.physicalSize`). 6. Run `flutter analyze` and `flutter test`. Then run the app on an iOS simulator and an Android emulator, plus iPad, desktop or the web if the app ships there, in light and dark mode (`xcrun simctl ui booted appearance dark`, `adb shell cmd uimode night yes`), and screenshot the settings screen each time (`xcrun simctl io booted screenshot`, `adb exec-out screencap -p`). Fix anything clipped, misaligned or hard to read. If you can't launch a device, tell the user what to run. 7. Summarize: files changed, dependencies added, each row with its state and storage key, tests added, screenshot paths, and anything the user still needs to provide. ## Known issues - iOS with VoiceOver: after a `SettingsSplitView` changes layout (for example on rotation, or when an iPad window is resized), VoiceOver can place the frames of the rows in a pane wrongly until the app restarts. This is a Flutter engine bug: it also happens without settings_ui when a subtree with its own semantics node moves to a new parent. Touch input without VoiceOver is not affected. Don't try to work around it in app code. ## Optional - [README](https://github.com/yako-dev/flutter-settings-ui/blob/master/README.md): screenshots, theming examples, API tables, prompts for coding agents - [CHANGELOG](https://github.com/yako-dev/flutter-settings-ui/blob/master/CHANGELOG.md): breaking changes in 4.0.0 - [Example app](https://github.com/yako-dev/flutter-settings-ui/tree/master/example/lib): iOS, Android, macOS, Windows, GNOME, web and Material 3 screens, a split view demo (`cd example && flutter run --route '/split-view?platform=android'`) and a showcase, a fictional app's settings in every style (`--route '/showcase?platform=macOS'`) - [material_ui](https://pub.dev/packages/material_ui): moving an app off `package:flutter/material.dart`