Platform-specific behaviors and adaptations

Adaptation philosophy

There are generally two cases of platform adaptiveness:

  1. Things that are behaviors of the OS environment (such as text editing and scrolling) and that would be ‘wrong’ if a different behavior took place.
  2. Things that are conventionally implemented in apps using the OEM’s SDKs (such as using parallel tabs on iOS or showing an android.app.AlertDialog on Android).

This article mainly covers the automatic adaptations provided by Flutter in case 1 on Android and iOS.

For case 2, Flutter bundles the means to produce the appropriate effects of the platform conventions but doesn’t adapt automatically when app design choices are needed. For a discussion, see issue #8410 and the Material/Cupertino adaptive widget problem definition.

For an example of an app using different information architecture structures on Android and iOS but sharing the same content code, see the platform_design code samples.

Flutter provides the navigation patterns seen on Android and iOS and also automatically adapts the navigation animation to the current platform.

On Android, the default Navigator.push() transition is modeled after startActivity(), which generally has one bottom-up animation variant.

On iOS:

  • The default Navigator.push() API produces an iOS Show/Push style transition that animates from end-to-start depending on the locale’s RTL setting. The page behind the new route also parallax-slides in the same direction as in iOS.
  • A separate bottom-up transition style exists when pushing a page route where PageRoute.fullscreenDialog is true. This represents iOS’s Present/Modal style transition and is typically used on fullscreen modal pages.
An animation of the bottom-up page transition on Android
Android page transition
An animation of the end-start style push page transition on iOS
iOS push transition
An animation of the bottom-up style present page transition on iOS
iOS present transition

Platform-specific transition details

On Android, two page transition animation styles exist depending on your OS version:

On iOS when the push style transition is used, Flutter’s bundled CupertinoNavigationBar and CupertinoSliverNavigationBar nav bars automatically animate each subcomponent to its corresponding subcomponent on the next or previous page’s CupertinoNavigationBar or CupertinoSliverNavigationBar.

An animation of the page transition on Android pre-Android P
Android Pre-P
An animation of the page transition on Android on Android P
Android Post-P
An animation of the nav bar transitions during a page transition on iOS
iOS Nav Bar

Back navigation

On Android, the OS back button, by default, is sent to Flutter and pops the top route of the WidgetsApp’s Navigator.

On iOS, an edge swipe gesture can be used to pop the top route.

A page transition triggered by the Android back button
Android back button
A page transition triggered by an iOS back swipe gesture
iOS back swipe gesture

Scrolling

Scrolling is an important part of the platform’s look and feel, and Flutter automatically adjusts the scrolling behavior to match the current platform.

Physics simulation

Android and iOS both have complex scrolling physics simulations that are difficult to describe verbally. Generally, iOS’s scrollable has more weight and dynamic friction but Android has more static friction. Therefore iOS gains high speed more gradually but stops less abruptly and is more slippery at slow speeds.

A soft fling where the iOS scrollable slid longer at lower speed than Android
Soft fling comparison
A medium force fling where the Android scrollable reached speed faster and stopped more abruptly after reaching a longer distance
Medium fling comparison
A strong fling where the Android scrollable reach speed faster and reached significantly more distance
Strong fling comparison

Overscroll behavior

On Android, scrolling past the edge of a scrollable shows an overscroll glow indicator (based on the color of the current Material theme).

On iOS, scrolling past the edge of a scrollable overscrolls with increasing resistance and snaps back.

Android and iOS scrollables being flung past their edge and exhibiting platform specific overscroll behavior
Dynamic overscroll comparison
Android and iOS scrollables being overscrolled from a resting position and exhibiting platform specific overscroll behavior
Static overscroll comparison

Momentum

On iOS, repeated flings in the same direction stacks momentum and builds more speed with each successive fling. There is no equivalent behavior on Android.

Repeated scroll flings building momentum on iOS
iOS scroll momentum

Return to top

On iOS, tapping the OS status bar scrolls the primary scroll controller to the top position. There is no equivalent behavior on Android.

Tapping the status bar scrolls the primary scrollable back to the top
iOS status bar tap to top

Typography

When using the Material package, the typography automatically defaults to the font family appropriate for the platform. On Android, the Roboto font is used. On iOS, the OS’s San Francisco font family is used.

When using the Cupertino package, the default theme always uses the San Francisco font.

The San Francisco font license limits its usage to software running on iOS, macOS, or tvOS only. Therefore a fallback font is used when running on Android if the platform is debug-overridden to iOS or the default Cupertino theme is used.

You might choose to adapt the text styling of Material widgets to match the default text styling on iOS. You can see widget-specific examples in the UI Component section.

Roboto font on Android
Roboto on Android
San Francisco font on iOS
San Francisco on iOS

Iconography

When using the Material package, certain icons automatically show different graphics depending on the platform. For instance, the overflow button’s three dots are horizontal on iOS and vertical on Android. The back button is a simple chevron on iOS and has a stem/shaft on Android.

Android appropriate icons
Icons on Android
iOS appropriate icons
Icons on iOS

The material library also provides a set of platform-adaptive icons through Icons.adaptive.

Haptic feedback

The Material and Cupertino packages automatically trigger the platform appropriate haptic feedback in certain scenarios.

For instance, a word selection via text field long-press triggers a ‘buzz’ vibrate on Android and not on iOS.

Scrolling through picker items on iOS triggers a ‘light impact’ knock and no feedback on Android.

Text editing

Flutter also makes the below adaptations while editing the content of text fields to match the current platform.

Keyboard gesture navigation

On Android, horizontal swipes can be made on the soft keyboard’s spacebar to move the cursor in Material and Cupertino text fields.

On iOS devices with 3D Touch capabilities, a force-press-drag gesture could be made on the soft keyboard to move the cursor in 2D via a floating cursor. This works on both Material and Cupertino text fields.

Moving the cursor via the space key on Android
Android space key cursor move
Moving the cursor via 3D Touch drag on the keyboard on iOS
iOS 3D Touch drag cursor move

Text selection toolbar

With Material on Android, the Android style selection toolbar is shown when a text selection is made in a text field.

With Material on iOS or when using Cupertino, the iOS style selection toolbar is shown when a text selection is made in a text field.

Android appropriate text toolbar
Android text selection toolbar
iOS appropriate text toolbar
iOS text selection toolbar

Single tap gesture

With Material on Android, a single tap in a text field puts the cursor at the location of the tap.

A collapsed text selection also shows a draggable handle to subsequently move the cursor.

With Material on iOS or when using Cupertino, a single tap in a text field puts the cursor at the nearest edge of the word tapped.

Collapsed text selections don’t have draggable handles on iOS.

Moving the cursor to the tapped position on Android
Android tap
Moving the cursor to the nearest edge of the tapped word on iOS
iOS tap

Long-press gesture

With Material on Android, a long press selects the word under the long press. The selection toolbar is shown upon release.

With Material on iOS or when using Cupertino, a long press places the cursor at the location of the long press. The selection toolbar is shown upon release.

Selecting a word via long press on Android
Android long press
Selecting a position via long press on iOS
iOS long press

Long-press drag gesture

With Material on Android, dragging while holding the long press expands the words selected.

With Material on iOS or when using Cupertino, dragging while holding the long press moves the cursor.

Expanding word selection via long press drag on Android
Android long press drag
Moving the cursor via long press drag on iOS
iOS long press drag

Double tap gesture

On both Android and iOS, a double tap selects the word receiving the double tap and shows the selection toolbar.

Selecting a word via double tap on Android
Android double tap
Selecting a word via double tap on iOS
iOS double tap

UI components

This section includes preliminary recommendations on how to adapt Material widgets to deliver a natural and compelling experience on iOS. Your feedback is welcomed on issue #8427.

Widgets with .adaptive() constructors

Several widgets support .adaptive() constructors. The following table lists these widgets. Adaptive constructors substitute the corresponding Cupertino components when the app is run on an iOS device.

Widgets in the following table are used primarily for input, selection, and to display system information. Because these controls are tightly integrated with the operating system, users have been trained to recognize and respond to them. Therefore, we recommend that you follow platform conventions.

Material Widget Cupertino Widget Adaptive Constructor
Switch in Material 3
Switch
Switch in HIG
CupertinoSwitch
Switch.adaptive()
Slider in Material 3
Slider
Slider in HIG
CupertinoSlider
Slider.adaptive()
Circular progress indicator in Material 3
CircularProgressIndicator
Activity indicator in HIG
CupertinoActivityIndicator
CircularProgressIndicator.adaptive()
 Checkbox in Material 3
Checkbox
Checkbox in HIG
CupertinoCheckbox
Checkbox.adaptive()
Radio in Material 3
Radio
Radio in HIG
CupertinoRadio
Radio.adaptive()

Top app bar and navigation bar

Since Android 12, the default UI for top app bars follows the design guidelines defined in Material 3. On iOS, an equivalent component called “Navigation Bars” is defined in Apple’s Human Interface Guidelines (HIG).

 Top App Bar in Material 3
Top App Bar in Material 3
Navigation Bar in Human Interface Guidelines
Navigation Bar in Human Interface Guidelines

Certain properties of app bars in Flutter apps should be adapted, like system icons and page transitions. These are already automatically adapted when using the Material AppBar and SliverAppBar widgets. You can also further customize the properties of these widgets to better match iOS platform styles, as shown below.

// Map the text theme to iOS styles
TextTheme cupertinoTextTheme = TextTheme(
    headlineMedium: CupertinoThemeData()
        .textTheme
        .navLargeTitleTextStyle
         // fixes a small bug with spacing
        .copyWith(letterSpacing: -1.5),
    titleLarge: CupertinoThemeData().textTheme.navTitleTextStyle)
...

// Use iOS text theme on iOS devices
ThemeData(
      textTheme: Platform.isIOS ? cupertinoTextTheme : null,
      ...
)
...

// Modify AppBar properties
AppBar(
        surfaceTintColor: Platform.isIOS ? Colors.transparent : null,
        shadowColor: Platform.isIOS ? CupertinoColors.darkBackgroundGray : null,
        scrolledUnderElevation: Platform.isIOS ? .1 : null,
        toolbarHeight: Platform.isIOS ? 44 : null,
        ...
      ),

But, because app bars are displayed alongside other content in your page, it’s only recommended to adapt the styling so long as its cohesive with the rest of your application. You can see additional code samples and a further explanation in the GitHub discussion on app bar adaptations.

Bottom navigation bars

Since Android 12, the default UI for bottom navigation bars follow the design guidelines defined in Material 3. On iOS, an equivalent component called “Tab Bars” is defined in Apple’s Human Interface Guidelines (HIG).

Bottom Navigation Bar in Material 3
Bottom Navigation Bar in Material 3
Tab Bar in Human Interface Guidelines
Tab Bar in Human Interface Guidelines

Since tab bars are persistent across your app, they should match your own branding. However, if you choose to use Material’s default styling on Android, you might consider adapting to the default iOS tab bars.

To implement platform-specific bottom navigation bars, you can use Flutter’s NavigationBar widget on Android and the CupertinoTabBar widget on iOS. Below is a code snippet you can adapt to show a platform-specific navigation bars.

final Map<String, Icon> _navigationItems = {
    'Menu': Platform.isIOS ? Icon(CupertinoIcons.house_fill) : Icon(Icons.home),
    'Order': Icon(Icons.adaptive.share),
  };

...

Scaffold(
  body: _currentWidget,
  bottomNavigationBar: Platform.isIOS
          ? CupertinoTabBar(
              currentIndex: _currentIndex,
              onTap: (index) {
                setState(() => _currentIndex = index);
                _loadScreen();
              },
              items: _navigationItems.entries
                  .map<BottomNavigationBarItem>(
                      (entry) => BottomNavigationBarItem(
                            icon: entry.value,
                            label: entry.key,
                          ))
                  .toList(),
            )
          : NavigationBar(
              selectedIndex: _currentIndex,
              onDestinationSelected: (index) {
                setState(() => _currentIndex = index);
                _loadScreen();
              },
              destinations: _navigationItems.entries
                  .map<Widget>((entry) => NavigationDestination(
                        icon: entry.value,
                        label: entry.key,
                      ))
                  .toList(),
            ));

Alert dialog

Since Android 12, the default UI of alert dialogs (also known as a “basic dialog”) follows the design guidelines defined in Material 3 (M3). On iOS, an equivalent component called “alert” is defined in Apple’s Human Interface Guidelines (HIG).

Basic Dialog in Material 3
Basic Dialog in M3
Alert in Human Interface Guidelines
Alert in HIG

Since alert dialogs are often tightly integrated with the operating system, their design generally needs to follow the platform conventions. This is especially important when a dialog is used to request user input about security, privacy, and destructive operations (e.g., deleting files permanently). As an exception, a branded alert dialog design can be used on non-critical user flows to highlight specific information or messages.

To implement platform-specific alert dialogs, you can use Flutter’s AlertDialog widget on Android and the CupertinoAlertDialog widget on iOS. Below is a code snippet you can adapt to show a platform-specific alert dialog.

void _showAdaptiveDialog(
  context, {
  required Text title,
  required Text content,
  required List<Widget> actions,
}) {
  Platform.isIOS || Platform.isMacOS
      ? showCupertinoDialog<String>(
          context: context,
          builder: (BuildContext context) => CupertinoAlertDialog(
            title: title,
            content: content,
            actions: actions,
          ),
        )
      : showDialog(
          context: context,
          builder: (BuildContext context) => AlertDialog(
            title: title,
            content: content,
            actions: actions,
          ),
        );
}

To learn more about adapting alert dialogs, check out the GitHub discussion on dialog adaptations. You can leave feedback or ask questions in the discussion.