Comment afficher un Toast ou des Notifications en superposition? Notion d'Overlay

Compatibilité
Date révision
30 mars 2023
Publié le
26 juin 2018
Flutter
v 3.13.x
Dart
v 3.1.x

Contexte

Dernièrement, alors que je développais une routine pour utiliser avec WebSockets, j'ai eu besoin d'afficher une icône en superposition de n'importe quelle page afin d'informer l'utilisateur qu'une notification, message, avait été envoyée par le serveur.

J'ai essayé d'utiliser PopupRoute, showDialog... mais le résultat que j'obtenais ne correspondait pas à mes attentes.

Lorsque l'on utilise des Routes, l'écran est entièrement recouvert par cette route et l'utilisateur perd la possibilité d'utiliser normalement la page recouverte. Cela provient du fait que chaque action (Tap, Drag...) n'a plus lieu sur la page recouverte mais sur celle qui la recouvre. J'ai dès lors continué mes recherches et suis tombé sur les notions de Overlay et OverlayEntry.

En lisant le code source de Flutter, j'ai découvert l'utilisation d'un OverlayEntry pour afficher l'avatar lors d'un Drag. J'en ai donc conclu que c'était ce que je recherchais.

Overlay

La documentation officielle mentionne: "Un 'overlay' est une pile (=Stack) d'entrées qui peuvent être gérées indépendamment. Les 'Overlays' permettent à des Widgets enfants de faire 'flotter' des éléments visuels au-dessus d'autres Widgets...". Eh bien ... pas très explicite.

Maintenant, en utilisant mes propres mots.

A première vue, un Overlay n'est rien d'autre qu'une simple couche (= layer), StatefulWidget, qui se situe au-dessus de tout, composé d'un Stack dans lequel on peut mettre n'importe quel Widget.

Ces Widgets sont appelés OverlayEntry

OverlayState

Un Overlay est un StatefulWidget. Un OverlayState est le State d'une instance spécifique d'un Widget de type Overlay. Un OverlayState est donc responsable du Build (= rendu).

OverlayEntry

Un OverlayEntry est un Widget que l'on insère dans un Overlay. Un OverlayEntry ne peut appartenir qu'à un seul Overlay à la fois.

Comme un Overlay utilise un Stack pour son rendu, nous pouvons utiliser les Widgets Positioned et AnimatedPositioned pour positionner précisément sur un Overlay.

C'est exactement ce dont j'ai besoin pour pouvoir afficher mon icône de notification n'importe où sur l'écran.

Plongeons directement dans du code

La classe suivante affiche une icône aux coordonnées (50.0, 50.0) et la supprime après 2 secondes.


1
2import 'package:flutter/material.dart';
3import 'dart:async';
4
5class ShowNotificationIcon {
6
7    void show(BuildContext context) async {
8        final OverlayState overlayState = Overlay.of(context);
9        final OverlayEntry overlayEntry = OverlayEntry(builder: _build);
10
11        overlayState.insert(overlayEntry);
12
13        await Future.delayed(const Duration(seconds: 2));
14
15        overlayEntry.remove();
16    }
17
18    Widget _build(BuildContext context){
19      return Positioned(
20        top: 50.0,
21        left: 50.0,
22        child: Material(
23            color: Colors.transparent,
24            child: Icon(Icons.warning, color: Colors.purple),
25        ),
26      );
27    }
28}
29

Comme vous pouvez le voir, pour pouvoir montrer l'icône, nous devons fournir un context. Pourquoi?

La documentation officielle n'explique pas cela mais, en regardant le code source, nous voyons qu'un Overlay est créé pour chaque Route et appartient donc à un arborescence de contextes (voir mon article qui traite de la notion de contexte). Il est donc compréhensible de devoir retrouver l'instance du OverlayState qui corresponde à un certain context (Ligne #7).

C'est tout! De cet exemple, nous pouvons dériver n'importe quel type de contenu et de comportement de superposition.

Afin d'illustrer cela, construisons une sorte de blinking Toast (message clignotant) qui affiche un widget à une certaine position sur l'écran, au-dessus de n'importe quel contenu et qui disparaît après une certaine durée.

Exemple: Blinking Toast

Cette première classe est une généralisation de l'exemple précédent qui permet d'externaliser la construction du Widget à afficher.



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

class BlinkingToast {
    bool _isVisible = false;

    ///
    /// BuildContext context: the context from which we need to retrieve the Overlay
    /// WidgetBuilder externalBuilder: (compulsory) external routine that builds the Widget to be displayed
    /// Duration duration: (optional) duration after which the Widget will be removed
    /// Offset position: (optional) position where you want to show the Widget
    ///
    void show({
        required BuildContext context,
        required WidgetBuilder externalBuilder, 
        Duration duration = const Duration(seconds: 2),
        Offset position = Offset.zero,
        }) async {

        // Prevent from showing multiple Widgets at the same time
        if (_isVisible){
            return;
        }

        _isVisible = true;

        final OverlayState overlayState = Overlay.of(context);
        final OverlayEntry overlayEntry = OverlayEntry(
            builder: (BuildContext context) => BlinkingToastWidget(
                widget: externalBuilder(context),
                position: position,
            ),
        );
        overlayState.insert(overlayEntry);

        await Future.delayed(duration);

        overlayEntry.remove();

        _isVisible = false;
    }
}

Cette deuxième classe affiche le widget à une certaine position sur l'écran et le fait clignoter.



class BlinkingToastWidget extends StatefulWidget {
  const BlinkingToastWidget({
      super.key,
      required this.widget,
      required this.position,
  });

  final Widget widget;
  final Offset position;

  
  State<BlinkingToastWidget> createState() => new _BlinkingToastWidgetState();
}

class _BlinkingToastWidgetState extends State<BlinkingToastWidget>
  with SingleTickerProviderStateMixin {

  late AnimationController _controller;
  late Animation<double> _animation;

  
  void initState() {
    super.initState();
    _controller = AnimationController(
        duration: const Duration(milliseconds: 500), vsync: this);
    _animation = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.0, 0.5)
    ))
      ..addListener(() {
        if (mounted){
          setState(() {
            // Refresh
          });
        }
      })
      ..addStatusListener((AnimationStatus status){
        if (status == AnimationStatus.completed){
          _controller.reverse().orCancel;
        } else if (status == AnimationStatus.dismissed){
          _controller.forward().orCancel;
        }
      });
    _controller.forward().orCancel;
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Positioned(
        top:  widget.position.dy,
        left: widget.position.dx,
        child: IgnorePointer(
          child: Material(
            color: Colors.transparent,
            child: Opacity(
              opacity: _animation.value,
              child: widget.widget,
            ),
          ),
        ),
      );
    }
  }
}

Pour utiliser le BlinkingToast:



BlinkingToast toast = BlinkingToast();

toast.show(
    context: context,
    externalBuilder: (BuildContext context){
        return Icon(Icons.warning, color: Colors.purple);
    },
    duration: Duration(seconds: 5),
    position: Offset(50.0, 50.0),
);

Conclusion

Ceci est un article très court, visant uniquement à partager la façon d'afficher un widget en superposition sur n'importe quel écran.

En espérant que vous aurez trouvé cet article utile.

Restez à l'écoute pour les prochains articles et bon codage.

0 Commentaires
Soyez le premier à faire un commentaire...
© 2024 - Flutteris
email: info@flutteris.com

Flutteris



Quand l'excellence rencontre l'innovation
"votre satisfaction est notre priorité"

© 2024 - Flutteris
email: info@flutteris.com