Designing for Both Worlds: Adaptive UIs with Material and Cupertino in Flutter
Creating intuitive, beautiful, and adaptive user interfaces is at the heart of every great mobile app. In Flutter, you can seamlessly design UIs that look native across both Android and iOS using Material Design and Cupertino widgets. These two design systems ensure that your app not only looks good but also feels right on the platform it’s running on.
In this article, we’ll explore the core principles behind building user-friendly interfaces in Flutter, diving into both Material and Cupertino design systems. We’ll guide you through practical examples, showing how to create adaptive layouts that adjust to each platform’s unique look and feel.
In our previous article, Testing Flutter Apps: A Beginner’s Guide to Unit Tests, we focused on ensuring the reliability of your code. Now, let’s shift our attention to crafting polished user experiences.
As always, all examples can be run in DartPad, so you can start coding without needing to set up an IDE.
General Theory and Platform Selection Code
Why Do We Need Adaptive UI?
Every mobile platform (Android and iOS) has its own interface standards. Users expect familiar design elements on their devices. For instance, buttons and dialog boxes look different on Android than on iOS. Flutter provides two key UI styles: Material Design for Android and Cupertino for iOS. It’s important to adapt your app’s appearance and behavior to look and feel native on each platform.
Let’s start by creating a platform selection screen to demonstrate how a UI can adapt based on the selected platform.
Platform Selection Code
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
void main() {
runApp(PlatformSelectionApp());
}
class PlatformSelectionApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Adaptive UI Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: PlatformSelectionScreen(),
);
}
}
class PlatformSelectionScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Select Platform')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => AndroidScreen()),
);
},
child: Text('Material Design (Android)'),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return CupertinoApp(
theme: CupertinoThemeData(brightness: Brightness.light),
home: CupertinoScreen(),
);
},
),
);
},
child: Text('Cupertino (iOS)'),
),
],
),
),
);
}
}
What is Material Design?
Material Design, developed by Google, is a design language aimed at creating a unified and engaging user experience across platforms and devices. It is based on the principles of “material metaphor,” which suggests that UIs should behave like real-world objects with clear, consistent surfaces and edges. Material Design emphasizes bold colors, meaningful motion, and responsive layouts that adapt to any screen size.
Key characteristics of Material Design
- Depth and Elevation: Components use shadows and layers to indicate hierarchy, action, and importance. For instance, an ElevatedButton literally elevates above the surface, indicating it’s meant for user interaction.
- Motion: Transitions and animations are integral to Material Design. They provide feedback to users and ensure smooth interactions.
- Grid-based layouts: Material Design emphasizes a grid-based structure to provide balance and alignment for layouts.
- Typography: Material Design gives importance to clear and legible typography, with defined text styles like
headline
,title
, andbody
.
Material Design in Flutter is implemented using a range of widgets like Scaffold, AppBar, TextField, ElevatedButton, and more. These widgets not only adhere to Android standards but also provide a consistent user experience across various screen sizes.
Key Material UI Components
- Scaffold: The base structure for laying out UI components, including
AppBar
,Drawer
, andBottomNavigationBar
. - TextField: A material input field for text.
- Switch and Checkbox: Widgets for toggling true/false values.
- Slider: A widget to select a value from a range.
- Chip: A small, interactive UI element to display information.
- ElevatedButton and AlertDialog: For user interaction and displaying alerts.
Android UI Code
class AndroidScreen extends StatefulWidget {
const AndroidScreen({
super.key,
});
@override
State<AndroidScreen> createState() => _AndroidScreenState();
}
class _AndroidScreenState extends State<AndroidScreen> {
bool _switchValue = false;
bool? _checkboxValue = false;
double _sliderValue = 50;
int? _seletedChip = 1;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Material Design Example')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
decoration: InputDecoration(labelText: 'Enter text'),
),
SwitchListTile(
title: Text('Enable feature'),
value: _switchValue,
onChanged: (bool value) {
setState(() {
_switchValue = value;
});
},
),
CheckboxListTile(
title: Text('Check this option'),
value: _checkboxValue,
onChanged: (bool? value) {
setState(() {
_checkboxValue = value;
});
},
),
Slider(
value: _sliderValue,
min: 0,
max: 100,
onChanged: (value) {
setState(() {
_sliderValue = value;
});
},
),
Wrap(
spacing: 5.0,
children: List<Widget>.generate(
3,
(int index) {
return ChoiceChip(
label: Text('Item $index'),
selected: _seletedChip == index,
onSelected: (bool selected) {
setState(() {
_seletedChip = selected ? index : null;
});
},
);
},
).toList(),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Material Alert'),
content: Text('This is a Material AlertDialog.'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: Text('CANCEL'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('OK'),
),
],
);
},
);
},
child: Text('Show AlertDialog'),
),
],
),
),
);
}
}
What is Cupertino?
Cupertino is Apple’s design language for iOS, offering a minimalist, clean, and fluid design. Unlike Material Design, which emphasizes boldness and depth, Cupertino favors simplicity and flat design, ensuring that elements feel natural within the iOS environment.
Key characteristics of Cupertino Design
- Minimalistic Design: Cupertino apps are clean and clutter-free. The UI components are sleek with minimal decoration, designed to give the content prominence over design elements.
- Consistent Look and Feel: Components like the CupertinoNavigationBar and CupertinoSegmentedControl ensure iOS apps feel native.
- Fluid Animations: Animations in Cupertino are smooth, subtle, and fluid, providing feedback through gestures like swipe, pull, or tap.
- System Colors and Typography: Cupertino widgets automatically adapt to iOS system themes (light/dark mode) and use system fonts to maintain consistency with other native iOS apps.
Flutter provides Cupertino widgets to help developers create iOS-like apps, such as CupertinoPageScaffold, CupertinoButton, CupertinoSwitch, and CupertinoAlertDialog. These widgets ensure that your app feels like a native iOS app while still being developed in Flutter.
Key Cupertino UI Components
- CupertinoPageScaffold: The main structure for iOS-like apps.
- CupertinoTextField: A text input field styled like iOS.
- CupertinoSwitch: A toggle switch for on/off states.
- CupertinoSlider: A slider for selecting a value from a range.
- CupertinoSegmentedControl: A segmented control for selecting between options.
- CupertinoButton and CupertinoAlertDialog: For user interactions and displaying alerts.
iOS UI Code
class CupertinoScreen extends StatefulWidget {
const CupertinoScreen({
super.key,
});
@override
State<CupertinoScreen> createState() => _CupertinoScreenState();
}
class _CupertinoScreenState extends State<CupertinoScreen> {
bool _switchValue = false;
bool? _checkboxValue = false;
double _sliderValue = 50;
int? _seletedSegment = 0;
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Cupertino Example'),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
CupertinoTextField(
placeholder: 'Enter text',
),
CupertinoSwitch(
value: _switchValue,
onChanged: (bool value) {
setState(() {
_switchValue = value;
});
},
),
CupertinoSlider(
value: _sliderValue,
min: 0,
max: 100,
onChanged: (value) {
setState(() {
_sliderValue = value;
});
},
),
CupertinoCheckbox(
value: _checkboxValue,
onChanged: (bool? value) {
setState(() {
_checkboxValue = value;
});
},
),
CupertinoSegmentedControl(
groupValue: _seletedSegment,
children: {
0: Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Text('Option 1'),
),
1: Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Text('Option 2'),
),
2: Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Text('Option 3'),
),
},
onValueChanged: (value) {
setState(() {
_seletedSegment = value;
});
},
),
CupertinoButton(
child: Text('Show AlertDialog'),
onPressed: () {
showCupertinoDialog(
context: context,
builder: (context) {
return CupertinoAlertDialog(
title: Text('Cupertino Alert'),
content: Text('This is a CupertinoAlertDialog.'),
actions: [
CupertinoDialogAction(
isDestructiveAction: true,
child: Text('CANCEL'),
onPressed: () => Navigator.of(context).pop(),
),
CupertinoDialogAction(
isDefaultAction: true,
child: Text('OK'),
onPressed: () => Navigator.of(context).pop(),
),
],
);
},
);
},
),
],
),
),
),
);
}
}
Comparing Material and Cupertino
When comparing Material Design and Cupertino styles, it’s clear that each reflects the unique design philosophies of its respective platform. Material Design is more bold and colorful, while Cupertino emphasizes simplicity and minimalism. Here are a few key differences:
Visual Design:
- Material Design uses shadows and elevations to indicate the hierarchy of elements.
- Cupertino Design opts for a flatter, less decorative approach.
Controls:
- Material includes components like the FloatingActionButton and Chips, which don’t have direct equivalents in Cupertino.
- Cupertino uses more familiar iOS elements like SegmentedControl and NavigationBars.
Interactions:
- Both styles offer similar widgets for basic interactions like sliders and switches, but the look and feel are different.
- Cupertino components often use native iOS animations, while Material widgets offer more structured animations.
Flutter’s ability to create adaptive UIs for both iOS and Android is powerful, allowing you to create apps that feel native on either platform without needing separate codebases.
Conclusion
In this article, we’ve explored how to create adaptive user interfaces in Flutter using Material Design and Cupertino widgets. By understanding the design principles behind each style, you can create apps that look and feel native to both Android and iOS users. Remember, a well-designed UI is crucial to delivering a great user experience.
Feel free to share your thoughts, progress, or suggestions in the comments. Don’t forget to subscribe for more tutorials, and continue exploring Flutter’s capabilities to build visually stunning and user-friendly apps for any platform!