How to display a dismissable tutorial overlay in my Flutter app?

I’d like to display a tutorial overlay on a screen in my Flutter app. I even found a good candidate which almost does what I’d want: that’s the https://pub.dev/packages/overlay_tutorial/ plugin. The user would press a help button on the screen, the overlay would pop up with the cut outs. I don’t want anything to receive clicks though (https://github.com/TabooSun/overlay_tutorial/issues/26), however the overlay should disappear if the user clicks anywhere on the screen.

I tried to use AbsorbPointer and it successfully intercepts the clicks and there’s no more click through and things happening bellow the overlay. See my fork: https://github.com/CsabaConsulting/overlay_tutorial/commit/9d809d51bcf55b9c8044d07d151c696bdb55abe6 However now there’s no way to click away the overlay either.

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        AbsorbPointer(
          absorbing: widget.enabled && widget.absorbPointer,
          ignoringSemantics: true,
          child: _OverlayTutorialBackbone(
            overlayColor: widget.overlayColor,
            enabled: widget.enabled,
            overlayTutorialHoles: _overlayTutorialHoles,
            onEntryRectCalculated: () {
              _updateChildren();
            },
            child: widget.child,
          ),
        ),
        if (widget.enabled) ...[
          ..._overlayTutorialHoles.entries
              .map((entry) {

What I’d need is an onTap or an onClick handler for the AbsorbPointer. I could have wrap stuff into a GestureDetector, but that would intercept clicks and gestures all the time. What’s a good solution here?

*this answer follows from my comment

If you paste this code into DartPad, you can see that when one GestureDetector is above another in the widget tree, only the onTap of the lower one gets executed. But, if the lower GestureDetector is wrapped in AbsorbPointer with absorbing: true only then does the parent GestureDetector get called. Thus, you should be able to get away with following what I suggested in the comments. Even if this weren’t true, you could still just conditionally set the onTap of your parent GestureDetector to null.

import 'package:flutter/material.dart';

final Color darkBlue = Color.fromARGB(255, 18, 32, 47);

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: MyWidget(),
        ),
      ),
    );
  }
}

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  bool absorbing = false;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        GestureDetector(
          onTap: () {
            print('GestureDetector 1');
          },
          child: AbsorbPointer(
            absorbing: absorbing,
            child: GestureDetector(
              onTap: () {
                print('GestureDetector 2');
              },
              child: Container(
                height: 200.0,
                width: 200.0,
                color: Colors.red,
                alignment: Alignment.center,
                child: Text(
                  'Press me and check console',
                  style: TextStyle(
                    color: Colors.white,
                  ),
                ),
              ),
            ),
          ),
        ),
        GestureDetector(
          onTap: () {
            setState(() {
              absorbing = !absorbing;
            });
          },
          child: Container(
            height: 200.0,
            width: 200.0,
            color: Colors.blue,
            alignment: Alignment.center,
            child: Text(
              'Change absorbn(absorbing: $absorbing)',
              textAlign: TextAlign.center,
              style: TextStyle(
                color: Colors.white,
              ),
            ),
          ),
        ),
      ],
    );
  }
}

Leave a Comment