Flutter Lesson 61 of 83 5 min read
Adaptive Apps in Flutter: Android, iOS, Web and Desktop
Make a Flutter app feel native on each platform: detect the platform, use adaptive widgets, support mouse and keyboard, and handle web.
On this page
Responsive means fitting the screen size. Adaptive means fitting the platform: its conventions, its input devices and its abilities. An app can run everywhere and still feel foreign if it ignores them.
Detecting the platform #
import 'package:flutter/foundation.dart';
bool get isWeb => kIsWeb;
bool get isApple =>
defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS;
bool get isDesktop => switch (defaultTargetPlatform) {
TargetPlatform.windows || TargetPlatform.macOS || TargetPlatform.linux => true,
_ => false,
};
bool get isMobile =>
defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS;
defaultTargetPlatform and kIsWeb work on every platform, including web. Avoid Platform.isAndroid from dart:io, which makes a web build fail.
Note that on the web defaultTargetPlatform reports the device’s operating system, so “is this a desktop?” may need !kIsWeb as well, depending on what you are deciding.
Inside widgets, Theme.of(context).platform gives the same value and can be overridden in tests.
Adaptive widgets #
Several Material widgets have an .adaptive constructor that draws the iOS style on Apple platforms.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: AdaptiveDemo()));
class AdaptiveDemo extends StatefulWidget {
const AdaptiveDemo({super.key});
@override
State<AdaptiveDemo> createState() => _AdaptiveDemoState();
}
class _AdaptiveDemoState extends State<AdaptiveDemo> {
bool _on = true;
double _value = 0.5;
Future<void> _confirm() async {
await showAdaptiveDialog<void>(
context: context,
builder: (context) => AlertDialog.adaptive(
title: const Text('Delete this item?'),
content: const Text('This cannot be undone.'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Delete')),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Adaptive widgets')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
SwitchListTile.adaptive(
title: const Text('Notifications'),
value: _on,
onChanged: (v) => setState(() => _on = v),
),
Slider.adaptive(value: _value, onChanged: (v) => setState(() => _value = v)),
const SizedBox(height: 16),
const Center(child: CircularProgressIndicator.adaptive()),
const SizedBox(height: 16),
FilledButton(onPressed: _confirm, child: const Text('Show dialog')),
],
),
);
}
}
| Adaptive constructor | On iOS it draws |
|---|---|
Switch.adaptive, SwitchListTile.adaptive | An iOS switch |
Slider.adaptive | An iOS slider |
CircularProgressIndicator.adaptive | The iOS activity spinner |
AlertDialog.adaptive, with showAdaptiveDialog | An iOS alert |
Icon(Icons.adaptive.share) | The platform’s share, back and more icons |
Platform conventions worth following #
| Android | iOS | |
|---|---|---|
| Back | System back button or gesture | Swipe from the left edge, and a back button at the top-left |
| App bar title | Left-aligned | Centred |
| Scrolling | Stops with a glow or stretch | Bounces |
| Page transition | Fade and slide up | Slide in from the right |
| Date picker | Calendar | Wheels |
Flutter handles the first four for you when you use MaterialApp and MaterialPageRoute. Do not override them without a reason.
Fully Cupertino screens #
For an app that should look entirely like iOS, the cupertino library has a complete widget set: CupertinoApp, CupertinoPageScaffold, CupertinoNavigationBar, CupertinoButton, CupertinoTextField, CupertinoPicker. Most cross-platform apps use Material with a custom theme and the adaptive constructors, which is far less work than maintaining two interfaces.
Mouse and keyboard #
Desktop and web users have a pointer and a keyboard. Material widgets support hover, focus and keyboard activation already. For your own widgets:
class HoverCard extends StatefulWidget {
const HoverCard({super.key, required this.child, required this.onTap});
final Widget child;
final VoidCallback onTap;
@override
State<HoverCard> createState() => _HoverCardState();
}
class _HoverCardState extends State<HoverCard> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: GestureDetector(
onTap: widget.onTap,
child: AnimatedScale(
scale: _hovered ? 1.03 : 1,
duration: const Duration(milliseconds: 120),
child: widget.child,
),
),
);
}
}
InkWell gives hover and focus highlights for free, so prefer it where it fits.
Keyboard shortcuts:
CallbackShortcuts(
bindings: {
const SingleActivator(LogicalKeyboardKey.keyS, control: true): _save,
const SingleActivator(LogicalKeyboardKey.escape): _close,
},
child: Focus(autofocus: true, child: const EditorView()),
)
LogicalKeyboardKey comes from package:flutter/services.dart. Also make sure Tab moves through your form fields in a sensible order.
Other desktop and web expectations #
- Tooltips on icon buttons, since there is hover.
- Scrollbars, which Flutter shows automatically on desktop.
- Right-click menus where they make sense (
MenuAnchor). - Denser layouts. A mouse is more precise than a finger, so set
visualDensity: VisualDensity.compactfor desktop. - Resizable windows, which means truly responsive layouts.
- Selectable text. Wrap content in
SelectionArea.
Features that do not exist everywhere #
Not every plugin supports every platform. The camera plugin has no Linux support, and dart:io files do not exist on the web. Check each package’s platform list on pub.dev, and guard the feature:
if (!kIsWeb && isMobile)
IconButton(icon: const Icon(Icons.camera_alt), onPressed: _takePhoto),
For larger differences, hide the choice behind an interface with one implementation per platform, so the rest of the app does not care which is in use.
Web specifics #
- Use go_router so the address bar, back button and bookmarks work.
- The first load is larger than a typical web page. Show something quickly, and keep the app’s assets small.
- Flutter web is best for applications. For pages that must rank in search engines, plain HTML is the better tool.
- Browsers block requests to servers that do not allow them (CORS). This is a server setting, not a Flutter bug.
Try it yourself #
Take a settings screen and make it adaptive: use adaptive switches and dialogs, show a hover effect and tooltips on desktop, add a keyboard shortcut for “save”, and hide a “take photo” option on platforms where the camera is not available.