Irbisa · Cheatsheet

Responsive & Adaptive UI

Breakpoints, LayoutBuilder, NavigationRail, foldables, orientation, SafeArea, text scaling

Middle Developer · 20 questions

  1. 01

    What is the difference between responsive and adaptive UI in Flutter?

    Responsive UI rearranges the same content to fit the space it has; adaptive UI changes how the app behaves for the device, input and platform.

  2. 02

    What is the difference between MediaQuery and LayoutBuilder, and which one should decide a widget's layout?

    MediaQuery describes the whole window (size, insets, text scale, user settings), while LayoutBuilder gives you the constraints the parent actually passed to this widget.

    class ProductCard extends StatelessWidget {  const ProductCard({super.key, required this.product});   final Product product;   @override…
  3. 03

    What breakpoints do you use to switch a screen between phone, tablet and desktop layouts, and where does that check go?

    Use Material 3's window size classes: compact under 600 dp, medium 600–839, expanded 840–1199, large 1200–1599, extra-large from 1600.

    enum WindowSize { compact, medium, expanded, large, extraLarge } WindowSize windowSizeOf(double width) => switch (width) {      < 600 => WindowSize.compact,      < 840 => WindowSize.medium,      < 1200 => WindowSize.expanded,…
  4. 04

    How do you handle orientation in Flutter: MediaQuery.orientationOf, OrientationBuilder, and locking with SystemChrome?

    Rotation is just a new window size: nothing is recreated, and widgets that depend on the size rebuild.

    Future<void> main() async {  WidgetsFlutterBinding.ensureInitialized();  // a hint: large Android screens and iPad windows may ignore it  await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);  runApp(const App());}…
  5. 05

    A teammate scales every size by screen width with flutter_screenutil. Why do many teams avoid that, and what do they use instead?

    Flutter sizes are already logical pixels, so 16 dp is about the same physical size on every phone; scaling by width just makes a bigger screen show the same layout, larger.

    // wrong: every size proportional to a 375 x 812 design frame// SizedBox(//   width: 343.w, height: 48.h, // 763 x 71 dp on an 834 dp wide iPad//   child: FilledButton(onPressed: pay, child: Text('Pay', style: TextStyle(fontSize: 16.sp))),// )…
  6. 06

    Opening the keyboard rebuilds widgets that have no text field anywhere near them. What does MediaQuery.of(context) have to do with it?

    MediaQuery.of(context) subscribes a widget to every field of MediaQueryData, and the keyboard changes viewInsets on each frame of its slide-in.

  7. 07

    On an iPhone the 34 px gap under this SendBar vanishes as soon as the keyboard opens. Which inset is SafeArea reading, and is this a bug?

    Scaffold(  body: Column(    children: [      const Expanded(child: MessageList()),      SafeArea(        top: false,        child: SendBar(controller: _controller),      ),    ],  ),);

    SafeArea reads MediaQuery.padding, which is viewPadding minus viewInsets, so once the keyboard covers the home indicator the bottom padding is zero.

  8. 08

    Find the bug: this sign-in screen looks fine until the keyboard opens, then a yellow-and-black overflow stripe appears at the bottom.

    class SignInScreen extends StatelessWidget {  const SignInScreen({super.key});   @override  Widget build(BuildContext context) {    final height = MediaQuery.sizeOf(context).height;    return Scaffold(      appBar: AppBar(title: const Text('Sign in')),      body: Column(children: [        SizedBox(height: height * 0.4, child: const Center(child: FlutterLogo(size: 96))),        const SignInForm(), // two fields and a button, about 200 px      ]),    );  }}

    The header is 40% of the window height, but MediaQuery.sizeOf does not change when the keyboard opens, while the Scaffold body shrinks by the keyboard height.

  9. 09

    Your custom bottom bar shows three dots for labels at 200% system font size. What does MediaQuery.textScalerOf give you, and where is clamping defensible?

    MediaQuery.textScalerOf(context) returns a TextScaler, and scaler.scale(14) is the size a 14 px font is really drawn at.

    final scaler = MediaQuery.textScalerOf(context);final large = scaler.scale(14) > 20; // the real painted size, not 14 * factor // right: reflow instead of truncating at 200%final userRow = large    ? Column(…
  10. 10

    This grid is 1000 px wide. How many columns does it lay out, how wide is each card, and what changes inside a 400 px side pane?

    GridView.builder(  gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(    maxCrossAxisExtent: 240,    crossAxisSpacing: 16,    mainAxisSpacing: 16,    childAspectRatio: 0.75,  ),  itemCount: products.length,  itemBuilder: (context, i) => ProductCard(product: products[i]),);

    Four columns of 238 px: the count is (1000 / (240 + 16)).ceil() = 4, and the width is (1000 − 3 × 16) / 4.

  11. 11

    A row of cards is wrapped in IntrinsicHeight so they match the tallest one, and the list starts to jank. What is that widget costing you?

    IntrinsicHeight asks its whole subtree how tall it would like to be before laying it out for real.

  12. 12

    Find the bug: StatCard lays out correctly in a grid, but the dashboard's horizontal strip of the same cards throws a layout error.

    class StatCard extends StatelessWidget {  const StatCard({super.key, required this.stat});  final Stat stat;  @override  Widget build(BuildContext context) => LayoutBuilder(        builder: (context, c) => c.maxWidth >= 240            ? Row(children: [Expanded(child: Text(stat.label)), Text(stat.value)])            : Column(children: [Text(stat.label), Text(stat.value)]),      );}// dashboard: a strip of cards that scrolls sidewaysWidget statStrip(List<Stat> stats) => ListView(      scrollDirection: Axis.horizontal,      children: [for (final s in stats) StatCard(stat: s)],    );

    A horizontal ListView gives its children unbounded width, so c.maxWidth is double.infinity, the >= 240 check always picks the Row, and an Expanded inside a Row with unbounded width throws "RenderFlex children have non-zero flex but incoming width constraints are unbounded".

  13. 13

    Resizing a desktop window from phone width to full screen resets the tab you were on. How do you wire a bottom bar and a navigation rail to one router?

    Keep the selected tab in the router, not in the chrome, and swap only the chrome around one shell: NavigationBar when compact, NavigationRail from 600 dp.

    const destinations = [  (icon: Icons.inbox, label: 'Inbox'),  (icon: Icons.send, label: 'Sent'),]; // builder of StatefulShellRoute.indexedStack…
  14. 14

    The iOS build has to feel native. How much do Flutter's .adaptive constructors give you, and what is left to branch by hand?

    The .adaptive constructors cover a short list of controls that draw their Cupertino twin on iOS and macOS.

    // Controls: the adaptive constructor is the whole changeSwitch.adaptive(value: enabled, onChanged: onToggle); // CupertinoSwitch on Apple showAdaptiveDialog<void>(  context: context,  builder: (ctx) => AlertDialog.adaptive(…
  15. 15

    The same Flutter screen now ships to desktop and web. What has to change for a mouse and keyboard beyond a wider layout?

    Everything a finger never needed: hover feedback and a pointer cursor, a right-click menu, a visible focus ring with a sensible tab order, and scroll views a mouse can drag where users expect it.

    class ContactRow extends StatelessWidget {  const ContactRow({super.key, required this.contact});  final Contact contact;   @override  Widget build(BuildContext context) {…
  16. 16

    Your inbox is a list on a phone and list-plus-message on a tablet. After rotating into two panes, the back gesture pops an invisible route. How do you model this?

    Make the selected message state, not a pushed route.

    class _InboxScreenState extends State<InboxScreen> {  String? _selectedId; // lives ABOVE the breakpoint  final _navKey = GlobalKey<NavigatorState>();   void _select(String? id) => setState(() => _selectedId = id);…
  17. 17

    A desktop user drags this window narrower: the list jumps back to the top and the selection is gone. What happens to the state when the width crosses 840?

    class OrdersScreen extends StatelessWidget {  const OrdersScreen({super.key});   @override  Widget build(BuildContext context) {    return LayoutBuilder(      builder: (context, c) => c.maxWidth >= 840          ? const WideOrders()    // StatefulWidget: list + detail, owns _selectedId          : const NarrowOrders(), // StatefulWidget: list only, owns _selectedId    );  }}

    Crossing 840 swaps WideOrders for NarrowOrders.

  18. 18

    The app is dragged into Android split screen, and on a foldable half the form ends up under the hinge. What does Flutter tell you about that hardware?

    MediaQuery.displayFeaturesOf(context) lists folds, hinges and cutouts in window coordinates.

    class HingeAwarePanes extends StatelessWidget {  const HingeAwarePanes({super.key, required this.start, required this.end});  final Widget start;  final Widget end;   @override…
  19. 19

    The design has to hold at four widths and at 200% text scale. How do you test that without maintaining forty golden files?

    Split the job.

    const sizes = [Size(390, 844), Size(673, 841), Size(1024, 768), Size(1440, 900)]; void main() {  for (final size in sizes) {    for (final scale in const [1.0, 2.0]) {      testWidgets('orders at ${size.width.toInt()} dp, ${scale}x text', (tester) async {…
  20. 20

    Phone, tablet and desktop from one Flutter codebase: do you write three widget trees or one tree full of width conditionals?

    Neither: split by layer.