Dart Tutorial

Flutter Lesson 60 of 83 4 min read

SafeArea, Orientation and the Keyboard in Flutter

Handle notches, rounded corners, landscape mode and the on-screen keyboard in Flutter with SafeArea, OrientationBuilder and view insets.

On this page

Screens are not plain rectangles. They have notches, camera holes, rounded corners, a status bar at the top and a gesture bar at the bottom. They rotate. A keyboard slides up over a third of them. This lesson covers all three.

SafeArea #

SafeArea adds exactly enough padding to keep its child out of the parts the system or the hardware covers.

import 'package:flutter/material.dart';

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        backgroundColor: Colors.teal,
        body: SafeArea(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Padding(
                padding: EdgeInsets.all(16),
                child: Text('Clear of the status bar', style: TextStyle(color: Colors.white)),
              ),
              Spacer(),
              Padding(
                padding: EdgeInsets.all(16),
                child: Text('Clear of the gesture bar', style: TextStyle(color: Colors.white)),
              ),
            ],
          ),
        ),
      ),
    ),
  );
}

Things to know:

  • An AppBar already protects the top. A NavigationBar already protects the bottom.
  • Use SafeArea on screens with no app bar, and for content pinned to the bottom, such as a button.
  • You can protect only some sides: SafeArea(top: false, child: ...).
  • Put it inside the Scaffold, so the background colour still reaches the screen edges.

In landscape on a phone with a notch, the unsafe area is on the left or right. SafeArea handles that too, which hand-written top padding does not.

Content behind the bars #

For a full-bleed photo or map, let the background extend under the bars and keep only the controls inside a SafeArea.

Stack(
  fit: StackFit.expand,
  children: [
    Image.network(url, fit: BoxFit.cover),   // edge to edge
    SafeArea(
      child: Align(
        alignment: Alignment.topLeft,
        child: IconButton(
          onPressed: () => Navigator.pop(context),
          icon: const Icon(Icons.arrow_back, color: Colors.white),
        ),
      ),
    ),
  ],
)

Orientation #

OrientationBuilder(
  builder: (context, orientation) {
    return GridView.count(
      crossAxisCount: orientation == Orientation.portrait ? 2 : 4,
      children: [for (var i = 0; i < 12; i++) Card(child: Center(child: Text('$i')))],
    );
  },
)

OrientationBuilder reports the orientation of the space it is given: portrait if taller than wide. Often it is better to decide by width with LayoutBuilder, since a “portrait” tablet is still wider than a “landscape” phone.

Landscape on a phone #

A phone in landscape is wide but very short, typically 360 to 400 logical pixels high. A layout with a tall header and a form will not fit. Two rules:

  • Make every screen scrollable, so nothing is cut off.
  • Consider a two-column arrangement: image on the left, form on the right.

Locking orientation #

If a screen truly only works one way, such as a game or a video player, lock it.

import 'package:flutter/services.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
  ]);
  runApp(const MyApp());
}

Locking the whole app to portrait is common for phone apps, but it makes the app poor on tablets and is ignored on some large-screen devices. Supporting both is the better default.

The keyboard #

When the keyboard opens, Scaffold shrinks its body so the content stays visible. Problems appear when the body cannot shrink.

“Bottom overflowed by N pixels” when the keyboard opens: the body is a Column that no longer fits. Make it scrollable.

Scaffold(
  body: SafeArea(
    child: SingleChildScrollView(
      padding: const EdgeInsets.all(24),
      child: Column(
        children: const [
          TextField(decoration: InputDecoration(labelText: 'Email')),
          SizedBox(height: 16),
          TextField(decoration: InputDecoration(labelText: 'Password')),
        ],
      ),
    ),
  ),
)

When a text field gains focus, Flutter scrolls it into view automatically, as long as it is inside a scrollable.

A button that stays above the keyboard:

Scaffold(
  body: const MessageList(),
  bottomNavigationBar: Padding(
    padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom),
    child: const SafeArea(child: MessageInputBar()),
  ),
)

viewInsetsOf(context).bottom is the height of the keyboard, or zero when it is hidden.

Dismissing the keyboard:

// When the user taps outside a field
GestureDetector(
  onTap: () => FocusScope.of(context).unfocus(),
  child: ...,
)

// When the user scrolls a list
ListView(
  keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
  children: const [],
)

Status bar colour #

Light icons on a light background are invisible. An AppBar sets the right icon colour for its background. On a screen without one, set it yourself:

AnnotatedRegion<SystemUiOverlayStyle>(
  value: SystemUiOverlayStyle.light, // light icons, for a dark background
  child: const Scaffold(backgroundColor: Colors.black, body: Placeholder()),
)

Foldables and split screen #

Foldable phones and tablets in split-screen mode change the window size while the app is running. If your layout is driven by MediaQuery.sizeOf and LayoutBuilder, it adapts with no extra work. Keep state out of the layout, so nothing is lost when the layout switches.

Try it yourself #

Build a chat screen: a list of messages and an input bar at the bottom. Make sure the input bar stays above the keyboard, the list scrolls to the end when a message is sent, dragging the list hides the keyboard, and everything stays clear of the notch in both orientations.

Practise in the playground Updated by Santosh Adhikari