Avoiding the On-Screen Keyboard in Flutter

Martin Rybak
Flutter NYC
Published in
5 min readFeb 14, 2019
Every colored area is wrapped in its own KeyboardAvoider.

Newcomers to both native iOS and Android development often struggle with what should be an easy problem: making room for the on-screen software keyboard. There are many different solutions, some of which are built-in and some of which use third-party packages. How does Flutter handle this scenario?

Flutter comes with a built-in Scaffold widget that automatically adjusts the bottom padding of its body widget to accomodate the on-screen keyboard. However, it comes with 2 major caveats:

  1. It pushes all content up, which you may not want.
  2. It assumes that it fills the whole screen, which it may not.

In the first case, consider the common scenario of a search field above a results view with a floating action button (FAB) on bottom. While the text field is active, I want to avoid the keyboard so that users can scroll to the last row in the list. I therefore embed my widget in a Scaffold. However, by doing so it also pushes up my FAB, which is not what I want.

Scaffold pushing up the FAB with the keyboard.

In the second case, I have a tablet app and want to make sure that the left and right panels are not obscured by the keyboard. Notice that they do not extend all the way to the bottom of the screen. But if I wrap them both in aScaffold, their bottoms will both shift up by the entire height of the keyboard, which is just flat-out incorrect. Not to mention that it feels extremely inappopriate to use a Scaffold in this way, since each comes with its own FAB, drawer, bottom sheet, etc. We can conclude that a Scaffold is not the right tool for the job.

Scaffolds always adjust their bottom padding to the full height of the keyboard.

So what can we do?

We can get the height of the on-screen keyboard by doing a MediaQuery and accessing its viewInsets.bottom property. Then we can wrap our widget in a simple Padding widget that consumes this value. Because MediaQuery is an InheritedWidget, our widget will automatically rebuild whenever the app’s viewInsets change:

class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bottom = MediaQuery.of(context).viewInsets.bottom;
return Padding(
padding: EdgeInets.only(bottom: bottom),
child: Placeholder(),
);
}
}

However we still have the same problem as the Scaffold: our widget assumes that it fills the entire screen. We need the bottom padding to equal the amount the widget is actually overlapped by the keyboard. How can we do that?

The short answer is that we cannot; at least not using widgets alone. The reason is that in Flutter, widgets are only short-lived immutable blueprints of our content. The Flutter engine uses those blueprints to layout and paint our actual content on the screen, which lives in a separate place called the render tree.

This sounds scary but it’s really not — if you’ve ever done any native mobile or web development you are already intimately familar with the concept of a view tree (DOM on web). Whenever you update a view by changing its text or color, you are updating the equivalent of the render tree in Flutter. In fact, the whole philosophy behind React and Flutter is to abstract away this “imperative” style of UI programming with a “declarative” one. Instead of telling the UI how to update, we tell it what we want it to look like, and let the framework do the hard work of actually figuring out what views to update. In Flutter, widgets are this mechanism. Behind the scenes, they are blueprints for updating the render tree.

So how do we find out how much our widget overlaps with the on-screen keyboard? First, we need to find the RenderObject for our widget in the render tree. We do that by calling findRenderObject() on our widget’s context. A BuildContext just represents a widget’s unique location in the render tree.

final renderObject = context.findRenderObject();

Now that we have it, we need to get its coordinates on the screen:

final renderObject = context.findRenderObject();
final renderBox = renderObject as RenderBox;
final offset = renderBox.localToGlobal(Offset.zero);
final widgetRect = Rect.fromLTWH(
offset.dx,
offset.dy,
renderBox.size.width,
renderBox.size.height,
);

Next we need to find the coordinates of the on-screen keyboard. We can get this from the global window class provided by the dart:ui package. The only wrinkle here is that we need to convert from pixels to points.

final keyboardTopPixels = window.physicalSize.height — window.viewInsets.bottom;
final keyboardTopPoints = keyboardTopPixels / window.devicePixelRatio;

Now we can implement our logic. First, let’s figure out how much the keyboard overlaps our widget:

final overlap = widgetRect.bottom — keyboardTopPoints;

We can now use this value to update our bottom padding. Let’s do this by making our widget stateful and introducing a new state variable called _overlap:

class _MyWidgetState extends State<MyWidget> {
double _overlap = 0;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: _overlap),
child: Placeholder(),
);
}
}

Now we can update our _overlap variable by calling setState(), which will cause our widget to rebuild. Let’s also make sure that the overlap is not negative.

if (overlap >= 0) {
setState(() {
_overlap = overlap;
});
}

Now where do we put all this code? Flutter has a nifty mixin calledWidgetsBindingObserver that provides adidChangeMetrics() method which gets called whenever the window’s metrics change:

@override
void didChangeMetrics() {

}

And that’s it! Putting it all together into a working example you can paste right into your main.dart file:

import 'package:flutter/material.dart';
import 'dart:ui';
void main() => runApp(MyWidget());class MyWidget extends StatefulWidget {
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with WidgetsBindingObserver {
double _overlap = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Column(
children: <Widget>[
Material(
child: TextFormField(
initialValue: "Edit me!",
),
),
Expanded(
child: Padding(
padding: EdgeInsets.only(bottom: _overlap),
child: Placeholder(),
),
)
],
),
);
}
@override
void didChangeMetrics() {
final renderObject = context.findRenderObject();
final renderBox = renderObject as RenderBox;
final offset = renderBox.localToGlobal(Offset.zero);
final widgetRect = Rect.fromLTWH(
offset.dx,
offset.dy,
renderBox.size.width,
renderBox.size.height,
);
final keyboardTopPixels = window.physicalSize.height - window.viewInsets.bottom;
final keyboardTopPoints = keyboardTopPixels / window.devicePixelRatio;
final overlap = widgetRect.bottom - keyboardTopPoints;
if (overlap >= 0) {
setState(() {
_overlap = overlap;
});
}
}
}

Hopefully this helps make the render tree a slightly less mysterious topic for you. Don’t be afraid to drop down to this layer when you need some more advanced functionality in Flutter. Just remember that the widget’s render object is only updated after the build() method returns.

There are some more advanced features you could still add, such as animating the padding change, automatically wrapping content in a ScrollView, and automatically scrolling to a focused text field. For these cases, feel free to use the KeyboardAvoider widget we have published on pub.dartlang.org. Your implementation might be different. Pull requests are always welcome. Enjoy!

--

--