Imagine that it’s blinking…

… is probably an article I should have published on April 1st. Still, here it is. The Amiga had an iconic way to display fatal errors: The guru meditation alert. Let’s recreate this for Flutter.

Here is how I want to display the alert:

showAlert(context, 42);

Instead of complicated and talkative text messages, I will display succient hexadecimal error codes which need no translation or localization. I’m utterly convinced that this is the future.

import 'package:flutter/widgets.dart'; void showAlert(BuildContext context, int code) {

final hex = code.toRadixString(16).toUpperCase().padLeft(8, '0'); ... }

Notice, how I not try to include a library for left-padding my string for improved stability and protection against rage-quitting developers.

To display anything above the normal UI, an OverlayEntry which is then added to the current Overlay can be used. The such an Overlay is automatically created as part of a WidgetsApp or MaterialApp widget. The overlay entry must be explicitly removed again. I’m using a GestureDetector so that the user has to tap the overlay to make it go away.

void showAlert(BuildContext context, int code) {

final hex = code.toRadixString(16).toUpperCase().padLeft(8, '0');

final black = Color(0xFF000000);

final red = Color(0xFFFF0000);

OverlayEntry overlay;

overlay = OverlayEntry(builder: (context) {

return Positioned(

left: 0,

right: 0,

child: GestureDetector(

onTap: () => overlay.remove(),

child: Container(

color: black,

height: 128,

),

),

);

});

Overlay.of(context).insert(overlay);

}

Because I don’t want to depend on material.dart I create my own Color objects. And because I need to refer to the overlay variable inside of the onTap handler of the builder function I cannot use the usual final variable definition that declares and initializes a variable in one step. Overlays are a bit special.

The Overlay widget is basically a Stack so I can use a Positioned widget to position my alert (currently just a black Container wrapped inside a GestureDetector ) at the top edge of the screen.

At this point, I can display a black overlay which is removed if I tap it.

Because an OverlayEntry isn’t automatically wrapped with a DefaultTextStyle , Text widgets without style or with a TextStyle that does not explicitly prohibits inheritance are displayed with a very ugly default text style (large redish text that has yellow double-underlining). Therefore, it is important to add an inherit: false property like so:

Container(

alignment: Alignment.center,

color: black,

height: 128,

child: Text(

'Software Failure. Tap to continue.



'

'Guru Meditation #48454C50.$hex',

textAlign: TextAlign.center,

style: TextStyle(

color: red,

fontFamily: 'Courier',

fontSize: 16,

fontWeight: FontWeight.bold,

inherit: false,

),

),

)

It starts to look like the real deal (please ignore the notch for now):

Let’s add the iconic red border next. There’s one problem, though. I don’t want the text to wrap (which would happen on smaller devices). Furthermore, if you compare the Courier font I used with the original Amiga font, it runs too wide. So let’s transform the Text widget by scaling it horizontally to 75%. To keep it at the same width, I have to enlarge the width by 133% at the same time. The text should now always fit the screen.

Container(

alignment: Alignment.center,

decoration: BoxDecoration(

color: black,

border: Border.all(color: red, width: 8),

),

padding: EdgeInsets.symmetric(

horizontal: 8,

vertical: 16,

),

child: FractionallySizedBox(

widthFactor: 4 / 3,

child: Transform(

alignment: Alignment.center,

transform: Matrix4.identity().scaled(3 / 4, 1),

child: Text(

...

),

),

),

),

We’re almost there…

As you probably see on my screen shots, if the device has a notch, the alert doesn’t look right. I’d like to move the box below the notch but cover everything above in black. The Amiga also displays a small black border around the red border, so let’s add this, too. A SafeArea widget inside another Container can do both. I need to disable its bottom margin, though.

return Positioned(

left: 0,

right: 0,

child: Container(

color: black,

child: SafeArea(

minimum: EdgeInsets.all(8),

bottom: false,

child: GestureDetector(

...

),

),

),

);

And there, it is, perfectly aligned:

For the final and most important step, I will add blinking.

The following might be a bit hacky, I don’t know, but it works and I don’t have to create a custom StatefulWidget . I’m using a StatefulBuilder instead. Each time, that widget asks its builder function to recreate the UI, I setup a timer to toggle the border color from red to black and back again after 700ms. Then, I’m using an AnimatedContainer to implicitly animate this color change within 300ms. Some things are just too easy in Flutter.

GestureDetector(

onTap: () {

blink = null;

overlay.remove();

},

child: StatefulBuilder(

builder: (context, setState) {

Future.delayed(Duration(milliseconds: 700))

.then((_) {

if (blink != null) setState(() => blink = !blink);

});

return AnimatedContainer(

duration: Duration(milliseconds: 300),

curve: Curves.easeInOut,

alignment: Alignment.center,

decoration: BoxDecoration(

color: black,

border: Border.all(

color: blink ? red : black, width: 8),

),

...

);

},

),

),

Here is the hacky part: Because Flutter throws an exception if I tap the alert and setState is then called on a disposed widget, I need to protect myself against this case by setting blink to null and explicitly checking for it. Perhaps, I should have used a Timer instead of a Future because that timer could be cancelled, I think. The future is inevitable.

Back to the future…

And there you have it, an alert box that looks much better than the usual modal dialog. And at least with old folk like myself, people will have a positive nostalgic feeling instead of cold-blooded anger because your app didn’t work for them as expected.

Here is the source code, by the way.