OrientationBuilder & Multi-Platform Layouts
Detecting Device Orientation
Many apps need to adapt their layout when the user rotates their device. Flutter provides the OrientationBuilder widget that rebuilds its child whenever the device orientation changes between portrait and landscape modes.
OrientationBuilder Widget
The OrientationBuilder provides an Orientation value (either Orientation.portrait or Orientation.landscape) to its builder function. This lets you return entirely different widgets based on orientation.
Basic OrientationBuilder Usage
class OrientationAwarePage extends StatelessWidget {
const OrientationAwarePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Orientation Demo')),
body: OrientationBuilder(
builder: (context, orientation) {
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: orientation == Orientation.portrait
? 2
: 4,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
),
itemCount: 20,
itemBuilder: (context, index) {
return Card(
color: Colors.primaries[index % Colors.primaries.length],
child: Center(
child: Text(
'Item \${index + 1}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
);
},
);
},
),
);
}
}
OrientationBuilder determines orientation based on the available width and height of the parent widget, not the actual device rotation. If the parent’s width exceeds its height, the orientation is landscape. This means it also works inside constrained containers.
Using MediaQuery for Orientation
You can also check orientation directly from MediaQuery, which is useful when you need the information outside a builder widget.
MediaQuery Orientation Check
class MediaQueryOrientationPage extends StatelessWidget {
const MediaQueryOrientationPage({super.key});
@override
Widget build(BuildContext context) {
final orientation = MediaQuery.orientationOf(context);
final size = MediaQuery.sizeOf(context);
final isLandscape = orientation == Orientation.landscape;
return Scaffold(
appBar: AppBar(
title: Text(
isLandscape ? 'Landscape Mode' : 'Portrait Mode',
),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Screen: \${size.width.toInt()} x \${size.height.toInt()}'),
Text('Orientation: \${orientation.name}'),
const SizedBox(height: 16),
Expanded(
child: isLandscape
? _buildLandscapeLayout()
: _buildPortraitLayout(),
),
],
),
),
);
}
Widget _buildPortraitLayout() {
return ListView(
children: List.generate(
10,
(i) => Card(
child: ListTile(
leading: CircleAvatar(child: Text('\${i + 1}')),
title: Text('Item \${i + 1}'),
),
),
),
);
}
Widget _buildLandscapeLayout() {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 2.5,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: 10,
itemBuilder: (context, i) => Card(
child: Center(
child: Text(
'Item \${i + 1}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
),
);
}
}
Platform Detection
When building apps that run on multiple platforms (iOS, Android, web, desktop), you often need to detect the current platform and adapt your UI accordingly.
The Platform Class
The dart:io library provides the Platform class with boolean properties for each operating system. However, this class is not available on the web.
Platform Detection (Non-Web)
import 'dart:io' show Platform;
class PlatformInfo {
static String get currentPlatform {
if (Platform.isAndroid) return 'Android';
if (Platform.isIOS) return 'iOS';
if (Platform.isMacOS) return 'macOS';
if (Platform.isWindows) return 'Windows';
if (Platform.isLinux) return 'Linux';
if (Platform.isFuchsia) return 'Fuchsia';
return 'Unknown';
}
}
kIsWeb and defaultTargetPlatform
For web-safe platform detection, Flutter provides kIsWeb from foundation.dart and defaultTargetPlatform from the framework.
Web-Safe Platform Detection
import 'package:flutter/foundation.dart'
show kIsWeb, defaultTargetPlatform, TargetPlatform;
class AdaptivePlatform {
static bool get isWeb => kIsWeb;
static bool get isMobile =>
!kIsWeb &&
(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS);
static bool get isDesktop =>
!kIsWeb &&
(defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.windows ||
defaultTargetPlatform == TargetPlatform.linux);
static bool get isTablet {
// Tablets are mobile devices with larger screens
// We combine platform check with screen size
return isMobile;
// Caller should also check screen width > 600
}
}
dart:io in code that runs on the web. It will cause compilation errors. Always use kIsWeb to check for web first, or use conditional imports.
Adaptive Layout System
A robust approach is to define breakpoints and create an adaptive layout builder that automatically selects the right layout for phone, tablet, and desktop.
Breakpoint-Based Adaptive Layout
enum DeviceType { phone, tablet, desktop }
class ResponsiveBreakpoints {
static const double tabletBreakpoint = 600;
static const double desktopBreakpoint = 1024;
static DeviceType getDeviceType(double width) {
if (width >= desktopBreakpoint) return DeviceType.desktop;
if (width >= tabletBreakpoint) return DeviceType.tablet;
return DeviceType.phone;
}
}
class AdaptiveLayout extends StatelessWidget {
final Widget Function(BuildContext context) phoneLayout;
final Widget Function(BuildContext context)? tabletLayout;
final Widget Function(BuildContext context)? desktopLayout;
const AdaptiveLayout({
super.key,
required this.phoneLayout,
this.tabletLayout,
this.desktopLayout,
});
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
final deviceType = ResponsiveBreakpoints.getDeviceType(width);
return switch (deviceType) {
DeviceType.desktop =>
(desktopLayout ?? tabletLayout ?? phoneLayout)(context),
DeviceType.tablet =>
(tabletLayout ?? phoneLayout)(context),
DeviceType.phone =>
phoneLayout(context),
};
}
}
Portrait vs Landscape Grid
Here’s a practical example that combines orientation detection with the breakpoint system to create a photo gallery that adapts to both orientation and screen size.
Adaptive Photo Gallery
class AdaptiveGallery extends StatelessWidget {
const AdaptiveGallery({super.key});
int _getColumnCount(BuildContext context, Orientation orientation) {
final width = MediaQuery.sizeOf(context).width;
final deviceType = ResponsiveBreakpoints.getDeviceType(width);
return switch ((deviceType, orientation)) {
(DeviceType.phone, Orientation.portrait) => 2,
(DeviceType.phone, Orientation.landscape) => 4,
(DeviceType.tablet, Orientation.portrait) => 3,
(DeviceType.tablet, Orientation.landscape) => 5,
(DeviceType.desktop, _) => 6,
};
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Photo Gallery')),
body: OrientationBuilder(
builder: (context, orientation) {
final columns = _getColumnCount(context, orientation);
return GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: 30,
itemBuilder: (context, index) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Colors.primaries[index % Colors.primaries.length],
child: Center(
child: Icon(
Icons.photo,
color: Colors.white.withValues(alpha: 0.7),
size: 32,
),
),
),
);
},
);
},
),
);
}
}
Tablet Split View
Tablets have enough screen space to show a master-detail layout. On phones, you navigate between screens; on tablets, both panels appear side by side.
Master-Detail Split View
class SplitViewPage extends StatefulWidget {
const SplitViewPage({super.key});
@override
State<SplitViewPage> createState() => _SplitViewPageState();
}
class _SplitViewPageState extends State<SplitViewPage> {
int? _selectedIndex;
final List<Map<String, String>> _items = List.generate(
20,
(i) => {
'title': 'Article \${i + 1}',
'body': 'This is the full content of article \${i + 1}. '
'It contains detailed information that is shown '
'in the detail panel on larger screens.',
},
);
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
final isWide = width >= 600;
return Scaffold(
appBar: AppBar(title: const Text('Articles')),
body: isWide
? Row(
children: [
SizedBox(
width: 320,
child: _buildList(),
),
const VerticalDivider(width: 1),
Expanded(
child: _selectedIndex != null
? _buildDetail(_selectedIndex!)
: const Center(
child: Text('Select an article'),
),
),
],
)
: _buildList(),
);
}
Widget _buildList() {
return ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) {
final isSelected = _selectedIndex == index;
return ListTile(
selected: isSelected,
selectedTileColor: Colors.blue.withValues(alpha: 0.1),
title: Text(_items[index]['title']!),
subtitle: Text(
_items[index]['body']!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: () {
setState(() => _selectedIndex = index);
final width = MediaQuery.sizeOf(context).width;
if (width < 600) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => Scaffold(
appBar: AppBar(
title: Text(_items[index]['title']!),
),
body: _buildDetail(index),
),
),
);
}
},
);
},
);
}
Widget _buildDetail(int index) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_items[index]['title']!,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
_items[index]['body']!,
style: const TextStyle(fontSize: 16),
),
],
),
);
}
}
Platform-Adaptive Navigation
Different platforms have different navigation conventions. Mobile apps use bottom navigation bars, while desktop apps typically use side navigation rails or full drawers.
Adaptive Navigation Shell
class AdaptiveNavigationShell extends StatefulWidget {
const AdaptiveNavigationShell({super.key});
@override
State<AdaptiveNavigationShell> createState() =>
_AdaptiveNavigationShellState();
}
class _AdaptiveNavigationShellState
extends State<AdaptiveNavigationShell> {
int _selectedIndex = 0;
static const List<NavigationDestination> _destinations = [
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.search), label: 'Search'),
NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
];
static const List<NavigationRailDestination> _railDestinations = [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.search),
label: Text('Search'),
),
NavigationRailDestination(
icon: Icon(Icons.person),
label: Text('Profile'),
),
];
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
final deviceType = ResponsiveBreakpoints.getDeviceType(width);
return Scaffold(
body: Row(
children: [
if (deviceType == DeviceType.desktop)
NavigationRail(
extended: true,
selectedIndex: _selectedIndex,
destinations: _railDestinations,
onDestinationSelected: (i) =>
setState(() => _selectedIndex = i),
)
else if (deviceType == DeviceType.tablet)
NavigationRail(
extended: false,
selectedIndex: _selectedIndex,
destinations: _railDestinations,
onDestinationSelected: (i) =>
setState(() => _selectedIndex = i),
),
Expanded(
child: _buildPage(_selectedIndex),
),
],
),
bottomNavigationBar: deviceType == DeviceType.phone
? NavigationBar(
selectedIndex: _selectedIndex,
destinations: _destinations,
onDestinationSelected: (i) =>
setState(() => _selectedIndex = i),
)
: null,
);
}
Widget _buildPage(int index) {
return Center(
child: Text(
'Page \${index + 1}',
style: const TextStyle(fontSize: 24),
),
);
}
}
OrientationBuilder lets you react to portrait/landscape changes. Platform detection with kIsWeb and defaultTargetPlatform enables safe cross-platform checks. Combining breakpoints with orientation creates truly adaptive layouts. Use split views on tablets, adaptive navigation for different screen sizes, and always test on multiple form factors.