Ultimate guide for Flutter Keyboard Issues: All 3 platforms iOS, Android & Web

Fix Flutter keyboard issues using Mixin and Scaffold properties. Ensure a seamless user experience on all platforms with these proven solutions.

Flutter is a powerful framework for cross-platform development, allowing developers to “Write once, use everywhere.” However, when it comes to Flutter Web, there are certain keyboard issues that make it difficult for production use.

Top 3 Flutter Keyboard Issues

  1. Keyboard covers up the text fields
  2. Extra spacing above the keyboard
  3. Keyboard insets work fine on mobile apps but break on the web

At the time I am writing this blog, Flutter 3.27.0 is on the stable channel. Flutter keyboard issues are one of those which you can not ignore in terms of production deployment. Keyboard is a major player in context of taking input from the end user for all 3 platforms(iOS, Android and Web) and you just can’t afford to not fix it before going live.

Let’s see how this very simple set of widgets have different output across all 3 platforms.

Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Sign Up'),
        centerTitle: true,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const SizedBox(height: 50),
            Text('Change is good. Let\'s signup and be a part of this change',
            style: TextStyle(fontSize: 40)),
            const SizedBox(height: 80),
            TextFormField(
              controller: _firstNameController,
              decoration: const InputDecoration(
                labelText: 'First Name',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.person),
              )
            ),
            const SizedBox(height: 30),
            TextFormField(
              controller: _lastNameController,
              decoration: const InputDecoration(
                labelText: 'Last Name',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.person),
              )
            ),
            const SizedBox(height: 30),
            TextFormField(
              controller: _lastNameController,
              decoration: const InputDecoration(
                labelText: 'Address',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.person),
              )
            ),
            const SizedBox(height: 60),
        
            ElevatedButton(
              onPressed: () {
                if (_formKey.currentState!.validate()) {
                  // Add your signup logic here
                  print('Form is valid');
                }
              },
              style: ElevatedButton.styleFrom(
                padding: const EdgeInsets.symmetric(vertical: 16),
              ),
              child: const Text(
                'Sign Up',
                style: TextStyle(fontSize: 16),),),],),),);
  }

Flutter Keyboard Behavior on iOS & Android (Apps)

iOS AppAndroid App
iOSAndroid

Easy ! No changes required and works out of the box on both iOS and Android mobile apps. Just by using Scaffold and SingleChildScrollView.

Flutter Keyboard Behavior on iOS & Android (Web)

iOS WebAndroid Web
Flutter keyboard issues iosFlutter keyboard issues android

⚠️ Flutter Keyboard Issues Detected:

  • On iOS Web, extra spacing appears between the keyboard and the text field.
  • On Android Web, behavior is inconsistent—sometimes it works fine, sometimes it doesn’t.

Let’s explore how to fix these.

Fix iOS Web Flutter Keyboard Issues

Understanding the Problem 🧩

  • The iOS keyboard has a Done button in the toolbar.
  • There’s another Done button at the bottom-right of the keyboard.
  • The top Done button hides the keyboard but doesn’t unfocus the text field.
  • Now when you reopen the keyboard while the textfield is still in focus(cursor blinking), extra spacing is added.

The Solution ✅

We can detect when the keyboard is hidden and unfocus the entire widget using FocusScope. For a modular and reusable approach, we use a Mixin.

///A Mixin to be applied to a type T that EXTENDS a stateful widget ON State of type T AND WidgetsBindingObserver. Use it to solve Flutter keyboard issues in iOS Web.
mixin iOSKeyboardInsetUtil<T extends StatefulWidget>
    on State<T>, WidgetsBindingObserver {

  bool _isKeyboardVisible = false;

  //1. Only apply if the app is running on (web AND iOS)
  bool get isIOSWeb {
    return kIsWeb && TargetPlatform.iOS == defaultTargetPlatform;
  }

  @override
  void initState() {
    super.initState();
    //2. Listen to WidgetsBinding for didChangeMetrics below
    if (isIOSWeb) {
      WidgetsBinding.instance.addObserver(this);
    }
  }

  @override
  void dispose() {
    if (isIOSWeb) {
      WidgetsBinding.instance.removeObserver(this);
    }
    super.dispose();
  }

  @override
  void didChangeMetrics() {
    //3. Listen to didChangeMetrics and detect the keyboard hide/show state via viewInsets.bottom.
    super.didChangeMetrics();
    if (isIOSWeb) {
      final bottomInset = View.of(context).viewInsets.bottom;

      //4. Store keyboard "opened" state based on the condition that viewInsets.bottom >0
      final newKeyboardState = bottomInset > 0;

      //5. Update the _isKeyboardVisible and use it in your parent widget if needed.
      if (_isKeyboardVisible != newKeyboardState) {
        setState(() {
          _isKeyboardVisible = newKeyboardState;
        });

        if (!newKeyboardState) {
          //6. If newKeyboardState is false(keyboard is hidden), unfocus from the widget.
          FocusScope.of(context).requestFocus(FocusNode());
        }
      }
    }
  }

  bool get isKeyboardVisible => _isKeyboardVisible;
}

There you go! Use this mixin with your stateful widget like below:

class _SignupPageState extends State<SignupPage> with WidgetsBindingObserver, iOSKeyboardInsetUtil {...}

Don’t worry if you need the full main.dart sample, it is at the end of this post ⬇️ ⬇️ ⬇️ ⬇️ ⬇️

Fix Android Web Flutter Keyboard Issues

Understanding the Problem 🧩

Well android’s behaviour is unpredictable so I am not pretty sure what causes it. Honestly I had to do a lot of hit and try to reach this fix so remember me in your blessings 🤑

The Solution ✅

A scaffold has a default property of resizeToAvoidBottomInset which defaults to true.

  • This works fine on iOS App, Android App, and iOS Web.
  • However, it breaks on Android Web, leading to incorrect keyboard insets.

You guessed it right, we will set this to false in android web.

bool get isAndroidWeb {
    return (kIsWeb && defaultTargetPlatform == TargetPlatform.android);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: isAndroidWeb ? false : true,

Nice! Now you have successfully fixed the Flutter keyboard issues in all the platforms.

Wait what about desktop web flutter keyboard issues? Well you can keep your hardware keyboard anywhere, in your lap, on your belly, etc, it won’t cause any issue because it’s a ⭐️ HARDWARE ⭐️ keyboard not inside your screen. I know, it’s a bad joke but I am good at writing code not humour.

Here is github gist for the mixin alone.

Full Main.dart file

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

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const SignupPage(),
    );
  }
}

class SignupPage extends StatefulWidget {
  const SignupPage({super.key});

  @override
  State<SignupPage> createState() => _SignupPageState();
}

class _SignupPageState extends State<SignupPage> with WidgetsBindingObserver, iOSKeyboardInsetUtil{
  final _formKey = GlobalKey<FormState>();
  final _firstNameController = TextEditingController();
  final _lastNameController = TextEditingController();
  final _emailController = TextEditingController();
  final _phoneController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  void dispose() {
    _firstNameController.dispose();
    _lastNameController.dispose();
    _emailController.dispose();
    _phoneController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  bool get isAndroidWeb {
    return (kIsWeb && defaultTargetPlatform == TargetPlatform.android);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: isAndroidWeb ? false : true,
      appBar: AppBar(
        title: const Text('Sign Up'),
        centerTitle: true,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const SizedBox(height: 50),
            Text('Change is good. Let\'s signup and be a part of this change',
            style: TextStyle(fontSize: 40)),
            const SizedBox(height: 80),
            TextFormField(
              controller: _firstNameController,
              textInputAction: TextInputAction.next,
              decoration: const InputDecoration(
                labelText: 'First Name',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.person),
              )
            ),
            const SizedBox(height: 30),
            TextFormField(
              controller: _lastNameController,
              textInputAction: TextInputAction.next,
              decoration: const InputDecoration(
                labelText: 'Last Name',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.person),
              )
            ),
            const SizedBox(height: 30),
            TextFormField(
              controller: _lastNameController,
              textInputAction: TextInputAction.done,
              decoration: const InputDecoration(
                labelText: 'Address',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.person),
              )
            ),
            const SizedBox(height: 60),
        
            ElevatedButton(
              onPressed: () {
                if (_formKey.currentState!.validate()) {
                  // Add your signup logic here
                  print('Form is valid');
                }
              },
              style: ElevatedButton.styleFrom(
                padding: const EdgeInsets.symmetric(vertical: 16),
              ),
              child: const Text(
                'Sign Up',
                style: TextStyle(fontSize: 16),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

///A Mixin to be applied to a type T that EXTENDS a stateful widget ON State of type T AND WidgetsBindingObserver
mixin iOSKeyboardInsetUtil<T extends StatefulWidget>
    on State<T>, WidgetsBindingObserver {

  bool _isKeyboardVisible = false;

  //1. Only apply if the app is running on (web AND iOS)
  bool get isIOSWeb {
    return kIsWeb && TargetPlatform.iOS == defaultTargetPlatform;
  }

  @override
  void initState() {
    super.initState();
    //2. Listen to WidgetsBinding
    if (isIOSWeb) {
      WidgetsBinding.instance.addObserver(this);
    }
  }

  @override
  void dispose() {
    if (isIOSWeb) {
      WidgetsBinding.instance.removeObserver(this);
    }
    super.dispose();
  }

  @override
  void didChangeMetrics() {
    //3. Listen to didChangeMetrics and detect the keyboard hide/show state via viewInsets.bottom.
    super.didChangeMetrics();
    if (isIOSWeb) {
      final bottomInset = View.of(context).viewInsets.bottom;

      //4. Store keyboard "opened" state based on the condition that viewInsets.bottom >0
      final newKeyboardState = bottomInset > 0;

      //5. Update the _isKeyboardVisible and use it in your parent widget if needed.
      if (_isKeyboardVisible != newKeyboardState) {
        setState(() {
          _isKeyboardVisible = newKeyboardState;
        });

        if (!newKeyboardState) {
          //6. If newKeyboardState is false(keyboard is hidden), unfocus from the widget.
          FocusScope.of(context).requestFocus(FocusNode());
        }
      }
    }
  }

  bool get isKeyboardVisible => _isKeyboardVisible;
}

8 Comments

  1. Thank you for sharing this workaround, it has been useful both to understand and solve the problem. But, just to be clear, is browser application run on android device still a problem? resizeToAvoidBottomInset set to false globally appears to not working correctly for me as, for example, when a Textfiel is instanciated within a dialog, the problem persist. I don’t know if I’m getting something wrong but this issue is causing to me lots of problem and I’m still looking for a good workaround (by the way I have to admit that I’m currently working on Flutter 3.22.2 and can’t update it at the moment). However thanks for sharing this in-depth study about keyboard flutter issues, appreciated!

    • resizeToAvoidBottomInset to false should fix the problem on web. 3.22.2 is definitely old and that might be causing the issue. Does the main.dart file shared in this tutorial works for you in 3.22.2? You might also want to do a quick switch to 3.27.0 and verify if that is the core issue. Switching flutter version is pretty easy and you can always revert back if you are using version control like git.

      • Right now I can’t spend more time to this issue as we have the application release next week (we currently suspended android web due to this issue). However I’m gonna give a try to the main.dart you shared in both Flutter version in order to understand if the problem is only related to the version of the framework. In addiction I’m gonna implement a dialog with a textfield to check if this second case is handled or not by this workaround. I’ll share update on this topic as soon as I can

        • I’ve tried the your application in 3.22.2 and 3.27.0 flutter versions and your workaround appears to work fine in both of them. Sadly it feels like my bug is strictly related to my Application scaffolding system… By the way thanks for sharing this article, it has been really useful for me to better understand the problem I’m facing with. Have a nice coding!

          • I understand. This issue kept me awake for many nights. Even I had to track back my scaffolding hierarchy and made sure resizeToAvoidBottomInset was set to false(for android) and not true for any inner scaffolds.

Leave a Reply

Your email address will not be published. Required fields are marked *