<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bibek Panda</title>
    <description>The latest articles on DEV Community by Bibek Panda (@bibekpanda55).</description>
    <link>https://dev.to/bibekpanda55</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F505680%2F674aaa9b-3cc7-4643-9069-bbfffb37d9bf.jpg</url>
      <title>DEV Community: Bibek Panda</title>
      <link>https://dev.to/bibekpanda55</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bibekpanda55"/>
    <language>en</language>
    <item>
      <title>Flutter: A not so simple login form - Keyboard adaptive layout with RenderShiftedBox</title>
      <dc:creator>Bibek Panda</dc:creator>
      <pubDate>Tue, 21 Jul 2026 04:55:17 +0000</pubDate>
      <link>https://dev.to/bibekpanda55/flutter-a-not-so-simple-login-form-keyboard-adaptive-layout-with-rendershiftedbox-1g9o</link>
      <guid>https://dev.to/bibekpanda55/flutter-a-not-so-simple-login-form-keyboard-adaptive-layout-with-rendershiftedbox-1g9o</guid>
      <description>&lt;p&gt;&lt;em&gt;Why every obvious fix fails, what the keyboard really is under the hood, and the 20-line render box that makes the screen move with it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The short version:&lt;/strong&gt; Set &lt;code&gt;resizeToAvoidBottomInset: false&lt;/code&gt;, read the keyboard&lt;br&gt;
height from &lt;code&gt;View.of(context).viewInsets.bottom / devicePixelRatio&lt;/code&gt; inside&lt;br&gt;
&lt;code&gt;didChangeMetrics&lt;/code&gt;, pad your body's bottom by that height, and shrink the hero&lt;br&gt;
with a small &lt;code&gt;RenderShiftedBox&lt;/code&gt;. Full code below; runnable repo at the end.&lt;/p&gt;



&lt;p&gt;Every screen I build starts the same way: the design file open on one side, an empty Dart file on the other.&lt;/p&gt;

&lt;p&gt;The login screen was supposed to be the easy one. A large hero illustration at the top, a phone number field in the middle, a "Get OTP" button anchored at the bottom. I spent the better part of a day getting it pixel-perfect — the illustration centered just right, the pill-shaped input, the button stretched edge to edge with exactly the spacing the design called for. I ran it on the simulator, leaned back, and admired it. This was a good-looking screen.&lt;/p&gt;

&lt;p&gt;Then I did the one thing I hadn't done yet: I tapped the phone number field.&lt;/p&gt;

&lt;p&gt;The keyboard slid up — and took my button with it. The primary CTA, the single control this entire screen exists to serve, was buried beneath the number pad. My beautiful layout hadn't broken, exactly; it just hadn't been designed for the moment a keyboard claims a third of the screen.&lt;/p&gt;

&lt;p&gt;One tap. Two weeks. This article covers everything I learned working through the problem — and how a seemingly ordinary login form turned into one of the most interesting layout challenges I've encountered in Flutter.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[IMAGE PLACEHOLDER 1 — hero GIF, side by side: "before" (button buried under keyboard) vs "after" (image shrinks, button rides the keyboard up). This is the social preview card — put it here, above the fold.]&lt;/code&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The screen
&lt;/h2&gt;

&lt;p&gt;Nothing exotic. Two screens, actually — login and OTP verification — with the same skeleton (that "two screens" detail pays off later):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────┐
│   [illustration] │   ← big SVG hero, ~400px tall
│                  │
│   Login with     │
│   Phone number   │
│   [+91 ______ ]  │
│   ☐ I agree...   │
│                  │
│   [  Get OTP  ]  │   ← pinned to the bottom
└──────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The requirement was equally simple to state: &lt;strong&gt;when the keyboard opens, the illustration should slide up out of the way — by exactly the keyboard's height — so the button ends up sitting right on top of the keyboard.&lt;/strong&gt; When the keyboard closes, everything slides back. Like the screen is breathing with the keyboard.&lt;/p&gt;

&lt;p&gt;If you've built forms in Flutter, you're probably already typing "just use &lt;code&gt;resizeToAvoidBottomInset&lt;/code&gt;" into the comments. Hold that thought — that's failed attempt number one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Everything that didn't work (and &lt;em&gt;why&lt;/em&gt; — that's the good part)
&lt;/h2&gt;

&lt;p&gt;I'm going to walk through the failures because each one taught me something real about how Flutter lays out a screen. If you just want the solution, skip ahead to &lt;strong&gt;"The reframe: the keyboard is an animation you can subscribe to."&lt;/strong&gt; But the failures are where the understanding lives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attempt 1: &lt;code&gt;resizeToAvoidBottomInset&lt;/code&gt; (let the Scaffold handle it)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Scaffold&lt;/code&gt; has &lt;code&gt;resizeToAvoidBottomInset: true&lt;/code&gt; by default. When the keyboard opens, the Scaffold shrinks its body so nothing sits underneath the keyboard. Free fix, right?&lt;/p&gt;

&lt;p&gt;Except our column was &lt;em&gt;taller than the shrunken body&lt;/em&gt;. Hero + title + form + button doesn't fit in half a screen. The layout overflowed especially on smaller devices, and the button — the last child — was the thing that got pushed off the edge. The Scaffold resized; our content didn't care.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson: &lt;code&gt;resizeToAvoidBottomInset&lt;/code&gt; resizes the viewport. It doesn't make your content any shorter.&lt;/strong&gt; Something still has to give, and by default the thing that gives is whatever's at the bottom. Which is exactly where your CTA lives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attempt 2: Collapse the image — &lt;code&gt;Align(heightFactor)&lt;/code&gt; + &lt;code&gt;AnimatedSize&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Okay — so let's &lt;em&gt;make&lt;/em&gt; the content shorter. Detect the keyboard, fold the hero away, and let &lt;code&gt;AnimatedSize&lt;/code&gt; smooth the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;keyboardOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MediaQuery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;viewInsetsOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bottom&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;AnimatedSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;duration:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;milliseconds:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nl"&gt;curve:&lt;/span&gt; &lt;span class="n"&gt;Curves&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;easeOut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;ClipRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Align&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;alignment:&lt;/span&gt; &lt;span class="n"&gt;Alignment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;topCenter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;heightFactor:&lt;/span&gt; &lt;span class="n"&gt;keyboardOpen&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;HeroImage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And… it works! Tap the field, the keyboard comes up, the logo slides away, the button rises into view. Open a pull request, ship it, go home.&lt;/p&gt;

&lt;p&gt;Then you use it for a minute, and something feels off. Watch the &lt;em&gt;dismiss&lt;/em&gt; closely: the keyboard slides down on its own curve and duration — the OS's — while &lt;code&gt;AnimatedSize&lt;/code&gt; runs its &lt;em&gt;own&lt;/em&gt; tween with its own curve and duration. Two animations, two clocks, no coordination. The logo lands early or late, the keyboard is still moving after the image has settled (or the other way around), and the screen feels disconnected — like two dancers who can't hear the same song. It's not a &lt;em&gt;connected&lt;/em&gt; flow; the logo and the keyboard each do their own thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson: you cannot tween your way into sync with the keyboard. The only way to move &lt;em&gt;with&lt;/em&gt; it is to be driven &lt;em&gt;by&lt;/em&gt; it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That lesson is the whole article, really. Let me show you what it means.&lt;/p&gt;




&lt;h2&gt;
  
  
  The reframe: the keyboard is an animation you can subscribe to
&lt;/h2&gt;

&lt;p&gt;Here's the mental shift that unlocked everything.&lt;/p&gt;

&lt;p&gt;When the OS animates the keyboard up, it reports — frame by frame — how many pixels of your window it currently covers. Flutter surfaces this as &lt;code&gt;viewInsets.bottom&lt;/code&gt;. During the ~250ms slide, that value doesn't jump from 0 to 300. It sweeps: 0, 14, 43, 96, 170, 240, 287, 300.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The keyboard isn't an event. It's an animation, and the OS is broadcasting every frame of it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;(One honest caveat: the per-frame sweep is what you get on iOS and on Android 11+, where &lt;code&gt;WindowInsetsAnimation&lt;/code&gt; drives the inset. On Android 10 and below the inset arrives as a single jump — the layout still ends up in exactly the right place, you just don't get the glide.)&lt;/p&gt;

&lt;p&gt;So instead of listening for "keyboard opened" and starting our &lt;em&gt;own&lt;/em&gt; animation (attempt 2's mistake), we bind our layout directly to that per-frame value. The keyboard becomes the single source of truth — the one clock everyone dances to. Perfect sync isn't something you tune; it falls out of the architecture.&lt;/p&gt;

&lt;p&gt;Three pieces make it work.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full, assembled source for all three pieces:&lt;/strong&gt; &lt;code&gt;[GIST/REPO LINK — one file, keyboard_aware_header.dart]&lt;/code&gt;. Code targets Flutter 3.10+ (it uses &lt;code&gt;View.of&lt;/code&gt;); written against 3.27.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Piece 1: A widget that always knows the keyboard's height
&lt;/h2&gt;

&lt;p&gt;We need the true inset, updated every frame. The trigger is &lt;code&gt;WidgetsBindingObserver.didChangeMetrics&lt;/code&gt;, which fires on every window-metrics change — including each frame of the keyboard's slide — and the source is the raw &lt;code&gt;FlutterView&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KeyboardHeightBuilder&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;KeyboardHeightBuilder&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="kt"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;keyboardHeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;KeyboardHeightBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_KeyboardHeightBuilderState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_KeyboardHeightBuilderState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;KeyboardHeightBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;WidgetsBindingObserver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;ValueNotifier&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_keyboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ValueNotifier&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;WidgetsBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;WidgetsBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;removeObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;_keyboard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;didChangeMetrics&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;View&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;_keyboard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;viewInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;devicePixelRatio&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ValueListenableBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;valueListenable:&lt;/span&gt; &lt;span class="n"&gt;_keyboard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;widget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details worth pausing on:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The division.&lt;/strong&gt; The raw &lt;code&gt;FlutterView&lt;/code&gt; reports insets in &lt;em&gt;physical&lt;/em&gt; pixels; your widget code speaks &lt;em&gt;logical&lt;/em&gt; pixels. On a 3× display, a 900-physical-pixel keyboard is 300 logical pixels. Forget the &lt;code&gt;/ devicePixelRatio&lt;/code&gt; and everything you drive with this number will be two-to-three times too big. (The ambient &lt;code&gt;MediaQuery&lt;/code&gt; does this conversion for you — the raw view does not.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the raw view and not &lt;code&gt;MediaQuery&lt;/code&gt;?&lt;/strong&gt; Fair question. There's a trap worth knowing: when a &lt;code&gt;Scaffold&lt;/code&gt; resizes for the keyboard (&lt;code&gt;resizeToAvoidBottomInset: true&lt;/code&gt;, the default), it has &lt;em&gt;already accounted for&lt;/em&gt; the inset — so it hands its body a &lt;code&gt;MediaQuery&lt;/code&gt; with the bottom inset removed (you can see the &lt;code&gt;removeViewInsets&lt;/code&gt; call in &lt;code&gt;scaffold.dart&lt;/code&gt;). Ask "how big is the keyboard?" from inside that body and you're told "what keyboard?" Our final architecture turns the Scaffold's resizing off (Piece 3), so the ambient &lt;code&gt;MediaQuery.viewInsetsOf&lt;/code&gt; would actually tell the truth here — but I read the raw view anyway, for two reasons: it makes this widget correct &lt;em&gt;regardless&lt;/em&gt; of how any ancestor Scaffold is configured, and it lets me scope rebuilds myself instead of inheriting &lt;code&gt;MediaQuery&lt;/code&gt;'s. Treat it as a robustness-plus-control choice, not a hard requirement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;ValueNotifier&lt;/code&gt; instead of &lt;code&gt;setState&lt;/code&gt;.&lt;/strong&gt; Calling &lt;code&gt;setState&lt;/code&gt; here would re-run this &lt;code&gt;State&lt;/code&gt;'s build every frame. Routing the value through a &lt;code&gt;ValueNotifier&lt;/code&gt; + &lt;code&gt;ValueListenableBuilder&lt;/code&gt; is a micro-optimization on its own — the &lt;em&gt;load-bearing&lt;/em&gt; optimization is in Piece 3, where the expensive subtree gets captured once and skipped entirely. Hold that thought.&lt;/p&gt;




&lt;h2&gt;
  
  
  Piece 2: Shrinking the image by exact pixels (the fun part)
&lt;/h2&gt;

&lt;p&gt;Now the hard problem. I want to say: &lt;em&gt;"render this image at its natural size, but make it occupy &lt;code&gt;keyboard&lt;/code&gt; fewer pixels of height, clipping the excess off the top."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Go ahead and try to build that from stock widgets. I'll wait.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SizedBox(height: ...)&lt;/code&gt;? You'd need to know the image's rendered height up front — ours scales with screen width.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Align(heightFactor: ...)&lt;/code&gt;? Fractions only. There's no "shrink by 240 pixels."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Transform.translate&lt;/code&gt;? Moves the &lt;em&gt;painting&lt;/em&gt;, not the &lt;em&gt;layout&lt;/em&gt;. The image slides visually but keeps its full slot — nothing below it moves up. (Classic trap: transforms are paint-time, layout is done by then.)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CustomSingleChildLayout&lt;/code&gt;? So close. But its delegate decides the parent's size from the incoming &lt;em&gt;constraints&lt;/em&gt; — it never gets to see the child's measured size. "Child's height minus N" is structurally impossible to express.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one stings, because "size me relative to my child" is exactly the job. When no widget can do a layout job, you drop one level down — to a render object. It sounds scarier than it is. Here's the whole thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_ShrinkTop&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;SingleChildRenderObjectWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;_ShrinkTop&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;by&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;child&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;_RenderShrinkTop&lt;/span&gt; &lt;span class="n"&gt;createRenderObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;_RenderShrinkTop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;updateRenderObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_RenderShrinkTop&lt;/span&gt; &lt;span class="n"&gt;renderObject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;renderObject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;shrinkBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_RenderShrinkTop&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;RenderShiftedBox&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;_RenderShrinkTop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_shrinkBy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;_shrinkBy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="n"&gt;shrinkBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_shrinkBy&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;_shrinkBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;markNeedsLayout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;performLayout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;child&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;constraints&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;smallest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;constraints&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loosen&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nl"&gt;parentUsesSize:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;visible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;height&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;_shrinkBy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;constraints&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;constrain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visible&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parentData&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;BoxParentData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visible&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twenty lines of actual logic. Three jobs inside &lt;code&gt;performLayout&lt;/code&gt; — plus one accomplice outside the function:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;child.layout(..., parentUsesSize: true)&lt;/code&gt;&lt;/strong&gt; — lay the image out at its natural size, and ask permission to read that size back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;size = ...&lt;/code&gt; with &lt;code&gt;child.size.height - _shrinkBy&lt;/code&gt;&lt;/strong&gt; — and here's the magic: &lt;code&gt;size&lt;/code&gt; is what &lt;em&gt;we&lt;/em&gt; report to our parent. We're allowed to lie. The image is 430px tall, but we tell the enclosing &lt;code&gt;Column&lt;/code&gt; "I'm 190px." The Column believes us and lays out the title 190px down. &lt;strong&gt;This one line is what pulls the entire rest of the screen upward.&lt;/strong&gt; The &lt;code&gt;clamp&lt;/code&gt; means a keyboard taller than the image just collapses us to zero rather than going negative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;offset&lt;/code&gt;&lt;/strong&gt; — the child still &lt;em&gt;paints&lt;/em&gt; at 430px, so we shift it up by the trimmed amount. The visible part is the image's bottom slice; the top slice hangs above our reported box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The accomplice: a &lt;code&gt;ClipRect&lt;/code&gt;&lt;/strong&gt; around the whole thing (in the wrapper widget below) — because layout and painting are separate passes. Reporting a small &lt;code&gt;size&lt;/code&gt; doesn't stop the child from painting outside it; without the clip, the image happily draws over the title below. &lt;code&gt;ClipRect&lt;/code&gt; cuts painting to our reported bounds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Layout says how much room you take. Offset says which slice shows. Clip says nothing leaks. Three concerns, three mechanisms.&lt;/p&gt;

&lt;p&gt;Why &lt;code&gt;RenderShiftedBox&lt;/code&gt; as the base class? It's Flutter's "box with one child painted at an offset" primitive — it already handles child management, painting at &lt;code&gt;parentData.offset&lt;/code&gt;, and hit-testing. We override exactly one method. (Its cousin &lt;code&gt;RenderProxyBox&lt;/code&gt; won't do: it always adopts the child's size, and changing the size is our entire point.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One trap to know about:&lt;/strong&gt; this box reports its child's &lt;em&gt;full&lt;/em&gt; intrinsic height — the shrink only happens in real layout. Park it under an &lt;code&gt;IntrinsicHeight&lt;/code&gt; and the intrinsic measurement channel will grant it the full slot, silently pinning your collapse open. (Historical footnote: &lt;code&gt;Align&lt;/code&gt;'s &lt;code&gt;heightFactor&lt;/code&gt; had a similar intrinsics blind spot on older Flutter SDKs; current SDKs factor it in. Custom render boxes like ours have the blind spot unless you override the &lt;code&gt;computeIntrinsic*&lt;/code&gt; methods yourself.) Our final layout simply doesn't use &lt;code&gt;IntrinsicHeight&lt;/code&gt; — the cleanest fix of all.&lt;/p&gt;

&lt;p&gt;And the pretty public face:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KeyboardAwareHeader&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;KeyboardAwareHeader&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;KeyboardHeightBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ClipRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;_ShrinkTop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;by:&lt;/span&gt; &lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what's &lt;em&gt;absent&lt;/em&gt;: no &lt;code&gt;AnimatedSize&lt;/code&gt;, no duration, no curve. The keyboard's own per-frame value drives &lt;code&gt;markNeedsLayout&lt;/code&gt; directly. When the OS says "I'm at 187px," the image is 187px shorter &lt;em&gt;that frame&lt;/em&gt;. Open, dismiss, interrupted mid-slide — the image tracks perfectly, because it isn't running an animation. It's obeying one.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[IMAGE PLACEHOLDER 2 — short GIF: keyboard opening, dismissing, and being interrupted mid-slide, image tracking it exactly. A before/after against the Attempt-2 desynced version is even stronger.]&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Piece 3: Parking the button on top of the keyboard
&lt;/h2&gt;

&lt;p&gt;The image shrinking makes room, but something still has to &lt;em&gt;position&lt;/em&gt; the button. I'd been burned enough by flexible-space tricks (a &lt;code&gt;Spacer&lt;/code&gt; happily re-expands to eat whatever room you free) that I wanted something that couldn't be absorbed, couldn't be negotiated with. It turned out to be the most boring widget in Flutter — &lt;code&gt;Padding&lt;/code&gt; (the snippet below is simplified from our production page; &lt;code&gt;FlutterLogo&lt;/code&gt; and &lt;code&gt;FilledButton&lt;/code&gt; are stand-ins for your hero image and CTA):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;resizeToAvoidBottomInset:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// we're taking over&lt;/span&gt;
  &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;SafeArea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;KeyboardAwarePadding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;     &lt;span class="c1"&gt;// Padding(bottom: keyboard) — see below&lt;/span&gt;
      &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;symmetric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;horizontal:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="n"&gt;Expanded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;SingleChildScrollView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;KeyboardAwareHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;FlutterLogo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;size:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// your hero image&lt;/span&gt;
                  &lt;span class="p"&gt;),&lt;/span&gt;
                  &lt;span class="c1"&gt;// title, phone field, terms checkbox…&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
              &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;FilledButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;                &lt;span class="c1"&gt;// your CTA — bottom of the padded area&lt;/span&gt;
            &lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
            &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Get OTP'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick is &lt;code&gt;Padding(bottom: keyboardHeight)&lt;/code&gt; around the &lt;em&gt;entire body&lt;/em&gt;, with the Scaffold's own resizing turned off. The padding simply deletes the keyboard's strip from the layout. The body now &lt;em&gt;ends&lt;/em&gt; where the keyboard &lt;em&gt;begins&lt;/em&gt; — so the button cluster at the bottom of the column sits right on top of the keyboard. Nothing to cancel, nothing to absorb, no gap. When the keyboard closes, the padding is zero and the layout is byte-for-byte the original design.&lt;/p&gt;

&lt;p&gt;(Why turn &lt;code&gt;resizeToAvoidBottomInset&lt;/code&gt; off? Because if the Scaffold &lt;em&gt;also&lt;/em&gt; resized, we'd be lifting the content twice — once by the resize, once by our padding — and the button would float a full keyboard-height above the keyboard.)&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Expanded(SingleChildScrollView(...))&lt;/code&gt; handles the other axis of the problem: on a small phone with the keyboard open, hero + form may not fit even with the image fully collapsed. The scroll view absorbs the overflow. The button never scrolls — it's outside the scroll area, bolted above the keyboard.&lt;/p&gt;

&lt;p&gt;And that "two screens" detail from the beginning? The OTP screen reuses &lt;code&gt;KeyboardAwareHeader&lt;/code&gt; and &lt;code&gt;KeyboardAwarePadding&lt;/code&gt; unchanged — which is the whole point of extracting them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wait — aren't &lt;code&gt;Expanded&lt;/code&gt; and &lt;code&gt;SingleChildScrollView&lt;/code&gt; contradictory?
&lt;/h3&gt;

&lt;p&gt;This trips up almost everyone (it tripped me). &lt;code&gt;Expanded&lt;/code&gt; says "take &lt;em&gt;all&lt;/em&gt; the remaining space"; a scroll view exists for content &lt;em&gt;bigger&lt;/em&gt; than its space. Opposites?&lt;/p&gt;

&lt;p&gt;No — they operate at different levels of the constraint system. Constraints flow down; sizes flow up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;Column&lt;/code&gt; has a bounded height (screen minus padding).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Expanded&lt;/code&gt; measures its siblings (the button), subtracts, and hands its child a &lt;strong&gt;tight, bounded&lt;/strong&gt; height: "you are exactly 540px."&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;SingleChildScrollView&lt;/code&gt; takes those 540px as its &lt;strong&gt;viewport&lt;/strong&gt; — and hands &lt;em&gt;its&lt;/em&gt; child &lt;strong&gt;unbounded&lt;/strong&gt; height: "be as tall as you like."&lt;/li&gt;
&lt;li&gt;The inner &lt;code&gt;Column&lt;/code&gt; becomes its natural 700px. Taller than the viewport → it scrolls. Shorter → it just sits there.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;Expanded&lt;/code&gt; &lt;em&gt;bounds&lt;/em&gt; the scroll view; the scroll view &lt;em&gt;unbounds&lt;/em&gt; its child. Complementary, not contradictory. In fact this pairing is the standard cure for the dreaded &lt;em&gt;"Vertical viewport was given unbounded height"&lt;/em&gt; crash — which is precisely what you get if you drop a scroll view straight into a &lt;code&gt;Column&lt;/code&gt;, because a &lt;code&gt;Column&lt;/code&gt; offers its children infinite height and the viewport can't work with that.&lt;/p&gt;




&lt;h2&gt;
  
  
  The performance pass: rebuild a Padding, not a page
&lt;/h2&gt;

&lt;p&gt;One more thing bugged me. &lt;code&gt;KeyboardAwarePadding&lt;/code&gt; naïvely written would rebuild the &lt;em&gt;whole body&lt;/em&gt; — column, scroll view, form, button — on every frame of the keyboard animation. Sixty rebuilds a second of widgets whose configuration hasn't changed.&lt;/p&gt;

&lt;p&gt;The fix is a pattern you already know from &lt;code&gt;AnimatedBuilder&lt;/code&gt;'s &lt;code&gt;child&lt;/code&gt; parameter, and it hinges on the fast path in &lt;code&gt;Element.updateChild&lt;/code&gt; (from Flutter's &lt;code&gt;framework.dart&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasSameSuperclass&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;widget&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;newWidget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="n"&gt;newChild&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// reuse the element — the subtree is skipped wholesale&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Widget.operator==&lt;/code&gt; is &lt;code&gt;@nonVirtual&lt;/code&gt; and falls back to object identity — so if the child widget is the &lt;em&gt;same instance&lt;/em&gt; as last frame, Flutter doesn't rebuild it, doesn't recurse into it, doesn't touch it. Create the expensive subtree &lt;strong&gt;once&lt;/strong&gt;, outside the per-frame builder, and merely &lt;em&gt;reference&lt;/em&gt; it inside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KeyboardAwarePadding&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;KeyboardAwarePadding&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;padding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zero&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;EdgeInsets&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// ← created once by the page, captured here&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;KeyboardHeightBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;padding:&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;bottom:&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// ← same instance every frame → subtree skipped&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page builds its &lt;code&gt;Column&lt;/code&gt; exactly once — pages don't rebuild during a keyboard animation &lt;em&gt;as long as they don't depend on &lt;code&gt;MediaQuery.of(context)&lt;/code&gt;&lt;/em&gt; (with the Scaffold's resizing off, the ambient &lt;code&gt;MediaQueryData&lt;/code&gt; changes every frame of the slide; use aspect-scoped accessors like &lt;code&gt;MediaQuery.sizeOf&lt;/code&gt; if you need screen dimensions, or you'll silently defeat this whole optimization). Every frame, the builder produces a fresh cheap &lt;code&gt;Padding&lt;/code&gt; wrapping the &lt;em&gt;identical&lt;/em&gt; &lt;code&gt;Column&lt;/code&gt; instance, and the reconciler skips the subtree.&lt;/p&gt;

&lt;p&gt;So the honest per-frame bill for the whole screen: two &lt;code&gt;ValueNotifier&lt;/code&gt;s fire (padding + header), two featherweight builder subtrees rebuild (a &lt;code&gt;Padding&lt;/code&gt;; a &lt;code&gt;ClipRect&lt;/code&gt;/&lt;code&gt;_ShrinkTop&lt;/code&gt; pair), and the image's size change relays out the column above it. No form rebuild, no button rebuild, no page rebuild. This is also the same reason &lt;code&gt;const&lt;/code&gt; constructors matter — &lt;code&gt;const&lt;/code&gt; widgets are canonicalized, so they're &lt;em&gt;always&lt;/em&gt; identical across builds and always skipped. The &lt;code&gt;child&lt;/code&gt;-capture pattern gets you that win when your subtree can't be &lt;code&gt;const&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd tattoo on my arm (a.k.a. takeaways)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The keyboard is an animation, not an event&lt;/strong&gt; — bind layout to &lt;code&gt;viewInsets.bottom&lt;/code&gt; per frame; never run your own tween. (Per-frame on iOS and Android 11+.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MediaQuery.viewInsets&lt;/code&gt; can read zero inside a resizing Scaffold&lt;/strong&gt; — the Scaffold consumed it. Read the raw &lt;code&gt;FlutterView&lt;/code&gt;, and divide by &lt;code&gt;devicePixelRatio&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intrinsic measurement is a separate channel&lt;/strong&gt; — a custom render box that shrinks in layout still reports full intrinsic height, so keep it away from &lt;code&gt;IntrinsicHeight&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Transform&lt;/code&gt; moves paint, not layout&lt;/strong&gt; — translated pixels don't free space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can't out-spacer a &lt;code&gt;Spacer&lt;/code&gt;&lt;/strong&gt; — position bottom-anchored UI with padding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Expanded&lt;/code&gt; + &lt;code&gt;SingleChildScrollView&lt;/code&gt; compose&lt;/strong&gt; — one bounds the viewport, the other unbounds its child.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A small render object is a tool, not overkill&lt;/strong&gt; — &lt;code&gt;RenderShiftedBox&lt;/code&gt; + one &lt;code&gt;performLayout&lt;/code&gt; override; &lt;code&gt;size&lt;/code&gt; is yours to (honestly) lie about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hoist expensive subtrees above per-frame builders&lt;/strong&gt; — identical child instances are skipped wholesale by the reconciler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The iPhone phone pad has no Done key&lt;/strong&gt; — for fixed-length input, the last digit is your Done button.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A login screen. The most basic UI in any app. It just took one custom render object, one raw-view listener, one constraint-system epiphany, and one small conspiracy against the &lt;code&gt;Spacer&lt;/code&gt; widget to make it feel effortless.&lt;/p&gt;

&lt;p&gt;That's the thing about "basic" UI — the polish that makes users feel nothing at all is where all the interesting engineering hides.&lt;/p&gt;




</description>
      <category>flutter</category>
      <category>dart</category>
      <category>ios</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
