K8 : retour automatique à l'accueil après 5 min sans interaction

flutter build apk --debug vert. Un visiteur part sans fermer l'écran ; le
suivant trouvait le contenu du précédent.

Écrit une seule fois, dans section_page_detail : il enveloppe les treize
types et portait déjà les crochets initState/dispose des statistiques.
L'écrire dans les écrans eux-mêmes l'aurait fait écrire treize fois.

Bénéfice de bord, et ce n'est pas un détail : le retour automatique passe
par le même dispose, donc il émet un sectionLeave avec sa durée réelle.
Sans lui, une consultation abandonnée restait ouverte jusqu'au visiteur
suivant et gonflait la durée mesurée d'autant. K8 répare donc une
statistique en plus d'un écran.

Listener plutôt que GestureDetector pour réarmer le minuteur : il observe
les pointeurs sans entrer en concurrence avec les gestes des écrans en
dessous. Une carte qu'on fait glisser ou un quiz qu'on tape ne doivent
rien perdre. onPointerDown et onPointerMove, donc le défilement d'un long
article compte comme une interaction.

Délai dans constants.dart (kKioskIdleTimeout) plutôt qu'en dur : c'est un
réglage de comportement de borne, pas une valeur d'implémentation.

Non vérifié : le comportement réel après 5 minutes, qui demande un device
et cinq minutes d'attente. popUntil(isFirst) suppose que l'accueil est
bien la première route de la pile — à confirmer au §0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Fransolet 2026-08-12 15:57:43 +02:00
parent 7c29b4bb76
commit 6efd67611f
2 changed files with 32 additions and 1 deletions

View File

@ -49,11 +49,13 @@ class _SectionPageDetailState extends State<SectionPageDetail> {
bool init = false;
DateTime? _sectionOpenTime;
StatisticsService? _statisticsService;
Timer? _idleTimer;
@override
void initState() {
super.initState();
_sectionOpenTime = DateTime.now();
_restartIdleTimer();
WidgetsBinding.instance.addPostFrameCallback((_) {
final tabletAppContext = Provider.of<AppContext>(context, listen: false).getContext() as TabletAppContext;
_statisticsService = tabletAppContext.statisticsService;
@ -64,8 +66,25 @@ class _SectionPageDetailState extends State<SectionPageDetail> {
});
}
/// Un visiteur part sans fermer l'écran ; le suivant trouve le contenu du
/// précédent. Le minuteur vit ici parce que cet écran enveloppe les treize
/// types : l'écrire dans les écrans eux-mêmes le ferait écrire treize fois.
void _restartIdleTimer() {
_idleTimer?.cancel();
_idleTimer = Timer(kKioskIdleTimeout, _returnHome);
}
void _returnHome() {
if (!mounted) return;
Navigator.of(context).popUntil((route) => route.isFirst);
}
@override
void dispose() {
_idleTimer?.cancel();
// Le retour automatique passe par ce dispose, donc il émet un sectionLeave
// avec sa durée réelle. Sans lui, une consultation abandonnée resterait
// ouverte jusqu'au visiteur suivant et fausserait la statistique.
final duration = _sectionOpenTime != null
? DateTime.now().difference(_sectionOpenTime!).inSeconds
: null;
@ -85,7 +104,14 @@ class _SectionPageDetailState extends State<SectionPageDetail> {
return Scaffold(
resizeToAvoidBottomInset: false,
body: SingleChildScrollView(
// Listener plutôt que GestureDetector : il observe les pointeurs sans
// entrer en concurrence avec les gestes des écrans en dessous une carte
// qu'on fait glisser ou un quiz qu'on tape ne doivent rien perdre.
body: Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (_) => _restartIdleTimer(),
onPointerMove: (_) => _restartIdleTimer(),
child: SingleChildScrollView(
child: Container(
height: size.height,
width: size.width,
@ -236,6 +262,7 @@ class _SectionPageDetailState extends State<SectionPageDetail> {
),
),
),
),
),
/*floatingActionButtonLocation: FloatingActionButtonLocation.endTop,
floatingActionButton: Padding(

View File

@ -17,6 +17,10 @@ const kBackgroundLight = Color(0xfff3f3f3);
const List<String> languages = ["FR", "NL", "EN", "DE", "IT", "ES", "PL", "CN", "AR", "UK"]; // hmmmm depends on config..
/// Délai sans interaction au bout duquel la borne revient à l'accueil, pour que
/// le visiteur suivant ne trouve pas le contenu du précédent.
const Duration kKioskIdleTimeout = Duration(minutes: 5);
const kTitleSize = 32.0;
const kDescriptionSize = 22.0;