androidflutteroverlay

What is problem with this Flutter Overlay


There are other ways of handling this overlay, but AFAIK this should work, and it does sometimes, but not always on Android. On Windows it appeared to work perfectly. I have the latest Flutter (3.0.3) and the latest Dart (2.17.5) and Flutter doctor shows no problems. I have tested this on 2 physical devices (Android 6.0.1 - level 23, and Android 11 - level 30). I have also tested it on an emulator (Android 11 - level 30). The results on all three were very similar - the overlay doesn't always show. I also tested it on Windows and it worked perfectly for all attempts (30).

The code below is a cut-down version of the code to demonstrate the problem. The problem is that the overlay does not always display. After a reboot it will often work well for about 10-12 reloads of the program, and then it is inconsistent, sometimes showing the overlay and sometimes not. As I said, it worked perfectly on Windows.

import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';

const D_OVERLAY_HEIGHT = 290.0;
const D_BTN_HEIGHT = 45.0;

void main() => runApp(OverlayTestApp());

class OverlayTestApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: OverlayTest(),
    );
  }
}

class OverlayTest extends StatefulWidget {
  @override
  _PageOverlayTest createState() => _PageOverlayTest();
}

//==== THE MAIN PAGE FOR THE APP ====//
class _PageOverlayTest extends State<OverlayTest> {
  TextEditingController? _wController = TextEditingController();
  ClassOverlay? _clOverlay;
  @override
  Widget build(BuildContext context) {
    WidgetsFlutterBinding.ensureInitialized();
    WidgetsBinding.instance
        .addPostFrameCallback((_) => fnOnBuildComplete(context));
    return Scaffold(
        resizeToAvoidBottomInset: false,
        appBar: AppBar(
            leading: IconButton(
                icon: Icon(Icons.arrow_back), onPressed: () => fnExit()),
            title: Center(child: Text('Overlay Test'))),
        body: WillPopScope(
          onWillPop: () async => await _fnDispose(),
          child: Column(
            children: [
              SizedBox(height: 50),
              TextField(
                controller: _wController,
                decoration: InputDecoration(
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(3),
                  ),
                ),
                style: TextStyle(fontSize: 16),
                autofocus: true,
                showCursor: true,
                readOnly: true,
                toolbarOptions: ToolbarOptions(
                  copy: false,
                  cut: false,
                  paste: false,
                  selectAll: false,
                ),
              ),
            ],
          ),
        ));
  }

  fnExit() {
    dispose();
    SystemNavigator.pop();
    exit(0);
  }

  @override
  void dispose() {
    _fnDispose();
    super.dispose();
  }

  Future<bool> _fnDispose() async {
    if (_wController != null) {
      _wController!.dispose();
      _wController = null;
    }
    if (_clOverlay != null) {
      _clOverlay!.fnDispose();
    }
    return true;
  }

  void fnOnBuildComplete(BuildContext context) async {
    if (_clOverlay == null) {
      double dScreenWidth = MediaQuery.of(context).size.width;
      dScreenWidth = dScreenWidth < 510 ? dScreenWidth : 500;
      double dScreenHeight = MediaQuery.of(context).size.height;
      _clOverlay = ClassOverlay(dScreenWidth, dScreenHeight - 320);
    }
    _clOverlay!.fnInitContext(context, _wController!);
  }
}

//==== CLASS FOR THE OVERLAY ====//
class ClassOverlay {
  TextEditingController? _wCtlText;
  OverlayEntry? _wOverlayEntry;
  final double dScreenWidth;
  final double dOverlayTop;
  Material? _wOverlayWidget;
  ClassOverlay(this.dScreenWidth, this.dOverlayTop) {
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
        overlays: [SystemUiOverlay.bottom, SystemUiOverlay.top]);
    _wOverlayWidget = fnCreateOverlayWidget(dScreenWidth: dScreenWidth);
  }

  //==== AFTER BUILD OF CALLER INITIALIZE WITH CONTEXT AND OVERLAYSTATE ====//
  void fnInitContext(BuildContext context, TextEditingController wCtlText) {
    try {
      _wCtlText = wCtlText;

      if (_wOverlayEntry != null) {
        _wOverlayEntry!.remove();
        _wOverlayEntry = null;
      }
      if (_wOverlayWidget == null) {
        throw ('_wOverlayWidget is null');
      }
      _wOverlayEntry = OverlayEntry(builder: (context) {
        return Positioned(left: 0, top: dOverlayTop, child: _wOverlayWidget!);
      });
      OverlayState? wOverlayState = Navigator.of(context).overlay;
      if (wOverlayState == null) {
        throw ('Failed to get OverlayState');
      }
      if (_wOverlayEntry == null) {
        throw ('OverlayEntry is null');
      }
      wOverlayState.insert(_wOverlayEntry!);
      if (!(wOverlayState.mounted)) {
        wOverlayState.activate();
      }
    } catch (vError) {
      _wCtlText!.text = '**** Fatal Error ${vError.toString()}';
    }
  }

  void fnDispose() {
    if (_wOverlayEntry != null) {
      _wOverlayEntry!.remove();
      _wOverlayEntry = null;
    }
  }
}

//==== CLASS - OVERLAY WIDGET ====//
Material fnCreateOverlayWidget({required double dScreenWidth}) {
  final double dBtnWidth = (dScreenWidth - 60) / 10;
  final double dLineSeparator = 10;
  return Material(
      child: Container(
    height: (D_OVERLAY_HEIGHT),
    width: dScreenWidth - 10,
    padding: EdgeInsets.only(bottom: 10),
    decoration: BoxDecoration(
        color: Colors.white,
        border: Border.all(color: Colors.grey.shade500, width: 2),
        borderRadius: BorderRadius.circular(10)),
    child: Column(
      children: [
        fnCreateRow('1234567890', dBtnWidth),
        SizedBox(height: dLineSeparator),
        fnCreateRow('qwertyuiop', dBtnWidth),
        SizedBox(height: dLineSeparator),
        fnCreateRow('asdfghjkl', dBtnWidth),
        SizedBox(height: dLineSeparator),
        fnCreateRow(',zxcvbnm.', dBtnWidth),
        SizedBox(height: dLineSeparator),
        fnCreateRow('1234567890', dBtnWidth)
      ],
    ),
  ));
}

//==== FUNCTION - CREATE A ROW OF TEXT BUTTONS ====//
Row fnCreateRow(String sRow, double dBtnWidth) {
  List<Widget> wRow = [];

  double dSpace = sRow.length == 10 ? 0 : (dBtnWidth * (10 - sRow.length)) / 2;
  if (dSpace > 0) {
    wRow.add(SizedBox(width: dSpace));
  }
  for (int iNdx = 0; iNdx < sRow.length; iNdx++) {
    double dFontSize = sRow[iNdx] == '.' || sRow[iNdx] == ',' ? 30 : 20;
    wRow.add(SizedBox(
        height: D_BTN_HEIGHT,
        width: dBtnWidth,
        child: TextButton(
          onPressed: () {},
          child: Text(sRow[iNdx],
              style: TextStyle(
                  color: Colors.black,
                  fontSize: dFontSize,
                  fontWeight: FontWeight.w400)),
        )));
  }
  if (dSpace > 0) {
    wRow.add(SizedBox(width: dSpace));
  }
  return Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: wRow);
}


Solution

  • It took a lot of time to find the reason for this problem. I was not aware of the fact that MediaQuery can return zero for the height or width because of a timing issue, and the application program must cater for that. It appears that this was working correctly, but the height and/or width was set to zero. There must be a lot of programs that don't check for that.