There is a distinct sinking feeling known only to mobile developers: your Flutter app works perfectly on the emulator, runs smoothly on a Pixel, and performs flawlessly on a Samsung Galaxy. Then, a user reports a bug from a Xiaomi Redmi Note or a Poco device.
"When I tap the password field, the keyboard covers it. I can't see what I'm typing."
On many Xiaomi devices running MIUI or the newer HyperOS, standard Flutter layout behaviors fail. The viewport doesn't resize, the Scaffold ignores the keyboard, and your input fields remain buried.
This is not a simple layout error; it is a conflict between Flutter’s rendering engine and Xiaomi’s non-standard implementation of Android’s Window Insets. This guide provides the technical root cause and a production-grade solution to fix keyboard occlusion on Xiaomi devices.
The Root Cause: Why MIUI Breaks Flutter Layouts
To fix the problem, we must understand the disconnect between the Operating System and the Flutter Framework.
1. The windowSoftInputMode Conflict
Android activities define how the window handles the On-Screen Keyboard (OSK) via the windowSoftInputMode attribute. Most Flutter apps rely on adjustResize. Theoretically, this tells the OS to resize the window containing the UI to fit the available space above the keyboard.
However, Xiaomi’s MIUI heavily modifies the Android view system to support their proprietary "Full Screen Gesture" navigation. When these gestures are enabled, the OS often misreports the available screen real estate during the keyboard animation.
2. Delayed Inset Reporting
Flutter relies on MediaQueryData.viewInsets.bottom to determine how much space the keyboard occupies. When the keyboard slides up:
- Native Android broadcasts an inset change.
- Flutter’s engine captures this.
- The
Scaffoldwidget reads the inset and applies padding (ifresizeToAvoidBottomInsetis true).
On affected Xiaomi devices, the inset change event is often delayed until after the animation completes, or it reports a value of 0.0 because the OS treats the keyboard as a floating overlay rather than a structural window element. Consequently, Flutter thinks the screen size hasn't changed, and the UI remains static.
The Solution: A Two-Layer Defense
We cannot rely solely on the default behaviors of Scaffold. We must enforce correct resizing at the native level and implement a layout structure that forces a recalculation of the rendering tree when metrics change.
Step 1: Enforcing Native Resize
First, ensure your Android manifest is explicitly requesting a resize. While this is the default in new Flutter projects, legacy migrations or accidental edits can remove it.
Open android/app/src/main/AndroidManifest.xml and locate the <activity> tag. Ensure the windowSoftInputMode is set strictly to adjustResize.
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application ...>
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
<!-- CRITICAL LINE BELOW -->
android:windowSoftInputMode="adjustResize">
<!-- ... meta-data and intent-filters ... -->
</activity>
</application>
</manifest>
Note: Do not use stateVisible or adjustPan. These settings will almost guarantee layout breaks on Xiaomi devices.
Step 2: The Manual Inset Pattern (The Flutter Fix)
If the native configuration fails (common on MIUI 13+), we must bypass the Scaffold's automatic resizing and manually inject the padding based on the specific media query metrics.
The most robust solution involves disabling the Scaffold's automatic behavior and wrapping your content in a scroll view that is explicitly padded by the viewInsets.
Here is the complete, drop-in solution using modern Dart features and MediaQuery.viewInsetsOf.
import 'package:flutter/material.dart';
class XiaomiSafeFormScreen extends StatelessWidget {
const XiaomiSafeFormScreen({super.key});
@override
Widget build(BuildContext context) {
// We capture the bottom inset (keyboard height) explicitly.
// Using viewInsetsOf is more performant than of(context) in Flutter 3.10+
final bottomPadding = MediaQuery.viewInsetsOf(context).bottom;
return Scaffold(
// 1. Disable the default resize behavior.
// We are taking manual control to ensure Xiaomi devices respond correctly.
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: const Text('Xiaomi Keyboard Fix'),
backgroundColor: Colors.deepPurple,
foregroundColor: Colors.white,
),
// 2. Wrap the body in a GestureDetector to dismiss keyboard on tap outside
body: GestureDetector(
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
child: SizedBox.expand(
child: SingleChildScrollView(
// 3. Apply the padding manually to the scroll view
padding: EdgeInsets.only(bottom: bottomPadding),
physics: const ClampingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Login',
style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
// Input Fields
_buildTextField(label: 'Email Address', icon: Icons.email),
const SizedBox(height: 16),
_buildTextField(label: 'Password', icon: Icons.lock, isObscure: true),
const SizedBox(height: 32),
// Action Button
FilledButton(
onPressed: () {
// Submit logic
},
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: const Text('Sign In'),
),
// 4. Extra space to ensure the UI can scroll above the keyboard
// nicely on smaller screens.
const SizedBox(height: 20),
],
),
),
),
),
),
);
}
Widget _buildTextField({
required String label,
required IconData icon,
bool isObscure = false
}) {
return TextField(
obscureText: isObscure,
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
fillColor: Colors.grey.shade100,
),
);
}
}
Deep Dive: Why This Code Works
You might wonder why setting resizeToAvoidBottomInset: false fixes a resizing bug. It seems counter-intuitive.
Decoupling from Scaffold Logic
When resizeToAvoidBottomInset is true, the Scaffold attempts to change the height of the body widget to match the remaining screen space. On Xiaomi devices, the calculation of "remaining space" is the point of failure.
By setting it to false, we tell the Scaffold: "Do not change the body size." The body remains full screen, extending behind the keyboard.
Injecting Manual Padding
We then use SingleChildScrollView with padding: EdgeInsets.only(bottom: bottomPadding).
The variable bottomPadding is derived from MediaQuery.viewInsetsOf(context).bottom. Even if the OS delays the window resize event, the Flutter framework still receives the window metric updates via the engine. By binding the padding directly to this metric in the Widget tree, we force a rebuild of the SingleChildScrollView's constraints the moment Flutter detects the metric change.
This bypasses the potentially buggy layout logic inside the Scaffold implementation and gives you direct control over the whitespace.
Handling Edge Cases
While the solution above covers 95% of cases, real-world apps have complex navigation structures. Here is how to handle specific edge cases.
1. Bottom Navigation Bars
If your screen has a BottomNavigationBar, the manual padding approach needs a slight adjustment. The standard Scaffold handles the bottom bar height automatically, but since we are handling padding manually, we need to account for safe areas.
Modify the padding logic to include the bottom safe area:
final bottomPadding = MediaQuery.viewInsetsOf(context).bottom;
final safeAreaBottom = MediaQuery.paddingOf(context).bottom;
// Inside SingleChildScrollView
padding: EdgeInsets.only(
bottom: bottomPadding > 0 ? bottomPadding : safeAreaBottom + 16
),
2. Full-Screen Modal Bottom Sheets
showModalBottomSheet creates its own overlay and context. If you face this issue inside a modal, ensure you set isScrollControlled: true and wrap the modal content in a Padding widget that listens to viewInsets.
showModalBottomSheet(
context: context,
isScrollControlled: true, // Critical for full-screen behavior
builder: (context) {
return Padding(
// Push content up by keyboard height
padding: EdgeInsets.only(
bottom: MediaQuery.viewInsetsOf(context).bottom
),
child: const YourBottomSheetContent(),
);
},
);
Conclusion
The fragmentation of the Android ecosystem remains one of the primary challenges for mobile engineering. Manufacturer-specific OS modifications, like those found in Xiaomi's MIUI, can silently break standard Flutter implementations.
By explicitly configuring adjustResize in the Android Manifest and taking manual control of scroll view padding within your Flutter widgets, you ensure a consistent, accessible experience for users across all devices. Don't rely on defaults; rely on explicit metric management.