Skip to content

[Android] viewInsets.bottom is restored to the keyboard height after returning from the background while no TextField is focused #191156

Open

Description

@boiOne

Steps to reproduce

Tested on a Pixel 7 running Android 17 (API 37), 3 button navigation, debug build, Impeller (Vulkan).
The code sample below is a flutter create bug project with only main.dart replaced.

  1. Run the app. The banner at the top reads viewInsets.bottom = 0.00.
  2. Tap the TextField. The keyboard opens and the banner follows the animation up to 398.10.
  3. Switch to another app (recents or home), then return to this app.
  4. Tap the unfocus button.
  5. Tap second page (no text field), a route that contains no text input at all.

This is timing-dependent and does not fire on every attempt, varying the moment of backgrounding after step 2 changes whether it happens. It took a handful of attempts here.

A hit is distinguishable from a miss in the log: when the inset stays wrong, the resume contains

ImeTracker: system_server:…: onCancelled at PHASE_CLIENT_ANIMATION_CANCEL

followed by further Sending viewport metrics to the engine lines. When the inset ends up correct, that line is absent. See the Logs section.

Expected results

  • After step 3 the keyboard is gone, so MediaQuery.viewInsets.bottom should be 0.0 and stay there.
  • After step 5 the second page's Scaffold body should fill the available height.

Actual results

After returning from the background, viewInsets.bottom is briefly reported as 0.0 and is then set back to the keyboard height, where it stays. No keyboard is visible and no TextField is focused: the banner reads viewInsets.bottom = 398.10, text field focused: false.

Any Scaffold with the default resizeToAvoidBottomInset: true then lays out its body short by that amount. In the sample, the second page's body is 516.19 instead of 914.29.

The value survives a hot restart and is cleared only by relaunching the app.

Related:

Code sample

main.dart
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
    home: const FirstPage(),
    builder: (context, child) => Stack(
      children: [
        child!,
        const Positioned(top: 0, left: 0, right: 0, child: _InsetBanner()),
      ],
    ),
  );
}

class _InsetBanner extends StatefulWidget {
  const _InsetBanner();

  @override
  State<_InsetBanner> createState() => _InsetBannerState();
}

class _InsetBannerState extends State<_InsetBanner> {
  @override
  void initState() {
    super.initState();
    FocusManager.instance.addListener(_onFocusChanged);
  }

  @override
  void dispose() {
    FocusManager.instance.removeListener(_onFocusChanged);
    super.dispose();
  }

  void _onFocusChanged() {
    if (mounted) setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    final inset = MediaQuery.viewInsetsOf(context).bottom;
    final focus = FocusManager.instance.primaryFocus;
    final focused =
        focus?.hasPrimaryFocus == true &&
        focus?.context?.findAncestorStateOfType<EditableTextState>() != null;
    final bad = inset > 0 && !focused;
    return IgnorePointer(
      child: Material(
        color: bad ? Colors.red : Colors.black87,
        child: SafeArea(
          bottom: false,
          child: Padding(
            padding: const EdgeInsets.all(8),
            child: Text(
              'viewInsets.bottom = ${inset.toStringAsFixed(2)}\n'
              'text field focused: $focused'
              '${bad ? '\nINCONSISTENT: inset without focus' : ''}',
              style: const TextStyle(color: Colors.white, fontSize: 16),
            ),
          ),
        ),
      ),
    );
  }
}

class FirstPage extends StatelessWidget {
  const FirstPage({super.key});

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('1 - has a text field')),
    body: Padding(
      padding: const EdgeInsets.fromLTRB(16, 96, 16, 16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const TextField(
            decoration: InputDecoration(
              border: OutlineInputBorder(),
              labelText: 'tap me, then background the app',
            ),
          ),
          const SizedBox(height: 24),
          OutlinedButton(
            onPressed: () => FocusManager.instance.primaryFocus?.unfocus(),
            child: const Text('unfocus'),
          ),
          const SizedBox(height: 12),
          FilledButton(
            onPressed: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const SecondPage()),
            ),
            child: const Text('second page (no text field)'),
          ),
        ],
      ),
    ),
  );
}

class SecondPage extends StatelessWidget {
  const SecondPage({super.key});

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('2 - no text field')),
    body: Container(
      color: Colors.amber,
      child: Center(
        child: Text(
          'This amber area is the Scaffold body.\n'
          'It should reach the bottom of the screen.\n\n'
          'body height: '
          '${MediaQuery.sizeOf(context).height - MediaQuery.viewInsetsOf(context).bottom}',
          textAlign: TextAlign.center,
        ),
      ),
    ),
  );
}

Screenshots or Video

Screenshots / Video demonstration Image
After returning from the background and tapping `unfocus`: no keyboard on screen, no focused text field, but `viewInsets.bottom = 398.10`.
Image
The second page, which contains no text input: the amber `Scaffold` body stops ~40% above the bottom edge. `body height: 516.19` instead of `914.29`.

Logs

Logs

Android reports that the IME is gone, and the inset that reaches MediaQuery disagrees.

Run where the inset stays wrong, the same resume, from ime:null onwards:

D/VRI[MainActivity]: WindowInsets changed: ime:null
D/FlutterJNI: Sending viewport metrics to the engine.
W/ImeBackCallbackProxy: Ime callback not found. Ignoring unregisterReceivedCallback.
D/InsetsController: hide(ime())
D/InsetsController: Setting requestedVisibleTypes to 503 (was 511)
D/WindowOnBackDispatcher: setTopOnBackInvokedCallback (unwrapped): android.view.ImeBackAnimationController@b9ec638
D/WindowOnBackDispatcher: setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@5eaf65
D/WindowOnBackDispatcher: setTopOnBackInvokedCallback (unwrapped): android.view.ImeBackAnimationController@b9ec638
I/ImeTracker: system_server:884a5fae: onCancelled at PHASE_CLIENT_ANIMATION_CANCEL
D/FlutterJNI: Sending viewport metrics to the engine.
D/WindowOnBackDispatcher: setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@5eaf65
D/FlutterJNI: Sending viewport metrics to the engine.

Run where the inset ends up correct, same point:

D/VRI[MainActivity]: WindowInsets changed: ime:null
D/FlutterJNI: Sending viewport metrics to the engine.
W/ImeBackCallbackProxy: Ime callback not found. Ignoring unregisterReceivedCallback.
D/InsetsController: hide(ime())
D/InsetsController: Setting requestedVisibleTypes to 503 (was 511)
I/ImeTracker: 80c8e479: onRequestHide at ORIGIN_CLIENT reason HIDE_SOFT_INPUT fromUser false userId 0 displayId 0
D/InsetsController: hide(ime())
I/ImeTracker: 80c8e479: onCancelled at PHASE_CLIENT_ALREADY_HIDDEN

The difference is PHASE_CLIENT_ANIMATION_CANCEL and the two additional Sending viewport metrics to the engine lines that follow it. Those are the frames in which the value goes back to the keyboard height.

Earlier in both runs, opening the keyboard produced WindowInsets changed: ime:[0,0,0,1045]. 1045 physical pixels / 2.625 device pixel ratio = 398.0952 logical pixels, i.e. exactly the value the framework then reports. For scale, this device's navigation bar is navigationBars:[0,0,0,126], i.e. 48 logical pixels.

Flutter Doctor output

flutter doctor -v
[√] Flutter (Channel stable, 3.41.7, on Microsoft Windows [Version 10.0.26200.9168], locale de-DE) [461ms]
    • Flutter version 3.41.7 on channel stable at D:\dev\flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision cc0734ac71 (4 months ago), 2026-04-15 21:21:08 -0700
    • Engine revision 59aa584fdf
    • Dart version 3.11.5
    • DevTools version 2.54.2
    • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration

[√] Windows Version (11 Home 64-bit, 25H2, 2009) [1.744ms]

[√] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [9,7s]
    • Android SDK at D:\Users\rawtp\AppData\Local\Android\Sdk
    • Emulator version 36.5.10.0 (build_id 15081367) (CL:N/A)
    • Platform android-36.1, build-tools 36.1.0
    • ANDROID_HOME = D:\Users\rawtp\AppData\Local\Android\Sdk
    • Java binary at: D:\Program Files\Android\Android Studio\jbr\bin\java
      This is the JDK bundled with the latest Android Studio installation on this machine.
      To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
    • Java version OpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108)
    • All Android licenses accepted.

[√] Chrome - develop for the web [128ms]
    • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe

[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.2.0) [126ms]
    • Visual Studio at C:\Program Files\Microsoft Visual Studio\2022\Community
    • Visual Studio Community 2022 version 17.2.32505.173
    • Windows 10 SDK version 10.0.19041.0

[√] Connected device (4 available) [675ms]
    • Pixel 7 (mobile)  • 2A231********* • android-arm64  • Android 17 (API 37)
    • Windows (desktop) • windows        • windows-x64    • Microsoft Windows [Version 10.0.26200.9168]
    • Chrome (web)      • chrome         • web-javascript • Google Chrome 151.0.7922.138
    • Edge (web)        • edge           • web-javascript • Microsoft Edge 149.0.4022.69

[√] Network resources [511ms]
    • All expected network resources are available.

• No issues found!

Device: Pixel 7, device pixel ratio 2.625, 3 button navigation, debug build, Impeller (Vulkan).

Activity

  1. boiOne commented on Aug 15, 2026

    @boiOne
    Author

    Correction to the reproduction notes above: PHASE_CLIENT_ANIMATION_CANCEL is not a reliable marker. I have since seen a run where the inset ends up wrong without that line appearing at all, so it should not be used to tell a hit from a miss.

    What has held across every failing run so far is narrower: after WindowInsets changed: ime:null, the framework briefly reports viewInsets.bottom == 0.0, and further Sending viewport metrics to the engine lines then restore it to the keyboard height. That transient 0.0 followed by the restore is the signature to look for.

  2. suryaKommana2662 commented on Aug 17, 2026

    @suryaKommana2662
    Contributor

    @boiOne Thanks for the detailed reproduction steps, sample code, logs, and clarification. I noticed that the issue was verified on Flutter 3.41.7. Could you please verify whether the issue is still reproducible on the latest stable Flutter release?

  3. added
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    and removed
    in triagePresently being triaged by the triage team
    on Aug 17, 2026
  4. boiOne commented on Aug 17, 2026

    @boiOne
    Author

    @suryaKommana2662

    Still reproducible on the latest stable, Flutter 3.47.0.

    Flutter 3.47.0
    Framework revision 4cf2416426 (2026-08-11)
    Engine revision 5f77625673
    Dart 3.13.0
    Device: Pixel 7, Android 17 (API 37), Impeller
    

    I rebuilt the minimal reproduction from the original report against 3.47.0 and ran through the steps repeatedly.
    It took about six attempts before it triggered, which matches the timing-dependent nature described earlier:
    the app has to go to the background while the keyboard animation is still running. If the animation completes first, the bug does not occur.

    When it triggers, the reproduction's banner turns red and reports INCONSISTENT: inset without focus, with viewInsets.bottom still holding the keyboard height while no EditableText has focus.

  5. removed
    waiting for responseThe Flutter team cannot make further progress on this issue until the original reporter responds
    on Aug 17, 2026
  6. added
    in triagePresently being triaged by the triage team
    and removed
    in triagePresently being triaged by the triage team
    on Aug 17, 2026
  7. suryaKommana2662 commented on Aug 18, 2026

    @suryaKommana2662
    Contributor

    @boiOne Thanks for confirming that the issue is still reproducible on the latest stable Flutter 3.47.0 and for providing the updated reproduction details. The issue is sufficiently reproducible with the provided minimal example and appears to be specific to Android IME/window-inset handling after returning from the background while the IME animation is in progress.

    This appears to be related to #190974, which also involves incorrect viewInsets.bottom values during IME transitions on Android. However, the reproduction conditions and symptoms differ #190974 is specific to a navigation-bar-height jump at the end of the IME opening animation on API < 35 with edge-to-edge, whereas this issue involves the keyboard height being restored after returning from the background even though the keyboard is no longer visible and no text field is focused. The issue will remain open and is being routing to the team-android for further investigation.

  8. added
    platform-androidAndroid applications specifically
    engineflutter/engine related. See also e: labels.
    team-androidOwned by Android platform team
    and removed
    in triagePresently being triaged by the triage team
    on Aug 18, 2026
  9. 7 remaining items

  10. added
    P1High-priority issues at the top of the work list
    triaged-androidTriaged by Android platform team
    on Aug 18, 2026
  11. flutter-triage-bot commented on Aug 18, 2026

    @flutter-triage-bot

    The triaged-android label is irrelevant if there is no team-android label or fyi-android label.

  12. justinmc commented on Aug 18, 2026

    @justinmc
    Contributor

    Seems likely not Android specific, though I couldn't reproduce it on an iOS simulator with a quick couple of tries.

  13. elvrusu commented on Oct 19, 2026

    @elvrusu

    Are there any workarounds to this issue? Since fixing it on Flutter side will take some time.

  14. Henry-Hiles commented on Oct 20, 2026

    @Henry-Hiles

    13. From Android triage to framework triage: Please feel free to re-route this back to us but the iOS issue linked #189051 appears to have the exact same issue, so not sure this is Android-specific.

    That linked issue has been fixed by an iOS-specific PR, so this is indeed Android-specific. Personally, I cannot reproduce it on Android 15, but users of my app can reproduce it on Android 17. Can this be re-routed back to the Android team?

  15. Henry-Hiles commented on Oct 20, 2026

    @Henry-Hiles
  16. added a commit that references this issue on Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P1High-priority issues at the top of the work listengineflutter/engine related. See also e: labels.platform-androidAndroid applications specificallyteam-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions