소스 검색

Verbindingsmelding herzien: overlay i.p.v. balk in de layout

De eerste opzet kon niet werken. HeaderButtonsComponent zit in een
FlexibleSpaceBar.background met een vaste hoogte van 74px; een balk die ruimte
vraagt geeft daar gegarandeerd een overflow. Twee valkuilen, allebei
opgetreden:

(a) De Column-wrap uit stap 4 was fout. Align > Row krijgt die 74px opgelegd,
    maar een Column geeft zijn kinderen onbegrensde hoogte - de knoppen-Row
    neemt dan zijn natuurlijke 200px, vandaar de 166px overflow. En stiller
    maar erger: de hamburger verdwijnt dan uit de header op alle elf pagina's,
    zonder enige melding.
(b) FlutterFlow's default Container van 200x40 eet de ruimte van de knoppen op
    en laat de tekst wrappen.

Het widget rendert nu SizedBox.shrink en toont de melding via de
ScaffoldMessenger. Het hoort als laatste kind in de bestaande Row, zonder
Container en met lege width/height.

Nagemeten op de emulator: offline 0 render-fouten met zichtbare hamburger, en
na netwerk terug plus een tik op Opnieuw is Home volledig hersteld.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bob 1 일 전
부모
커밋
0b85a9071c
3개의 변경된 파일143개의 추가작업 그리고 93개의 파일을 삭제
  1. 50 32
      TASKS.md
  2. 8 1
      snippets/LEESMIJ.md
  3. 85 60
      snippets/verbindings_banner.dart.txt

+ 50 - 32
TASKS.md

@@ -38,57 +38,64 @@ FlutterFlow-imports binnen (getest, compileert met én zonder).
   data-call), dus verwaarloosbaar. Een 401/404 telt als bereikbaar; alleen 5xx,
   timeouts en netwerkfouten zetten de vlag. Return Type **Boolean**, geen
   argumenten.
-- **Custom Widget `VerbindingsBanner`** — parameters `width` (double, optioneel),
-  `height` (double, optioneel), `tekst` (String, optioneel); alle drie leeg
-  laten volstaat. Toont niets zolang alles goed gaat (`SizedBox.shrink`, neemt
-  geen ruimte in), throttlet de automatische check op **max. 1 per 20 seconden
-  over alle pagina's samen**, en herlaadt bij een geslaagde tik de huidige route
-  zodat de data alsnog binnenkomt.
+- **Custom Widget `VerbindingsBanner`** — parameters `width`, `height`, `tekst`
+  (alle drie optioneel; **leeg laten**). Het widget rendert zelf **niets**
+  (`SizedBox.shrink`) en toont de melding via de **`ScaffoldMessenger`** over de
+  pagina heen — daardoor kan het in de vaste-hoogte-header staan zonder iets te
+  breken. Throttlet de automatische check op **max. 1 per 20 seconden over alle
+  pagina's samen**, en herlaadt bij een geslaagde tik op "Opnieuw" de huidige
+  route zodat de data alsnog binnenkomt.
 
 **Stappen in de builder:**
 1. App Values → App State → `+ Add App State Variable`: naam **`geenVerbinding`**,
    type **Boolean**, initial **false**, **Persisted uit**. Dit moet er zijn vóór
    stap 2, anders kan de action niet opslaan.
 2. Custom Code → `+` → **Action** → naam `checkVerbinding` → body plakken →
-   Return Type Boolean → **Ctrl+S** (zonder die expliciete save haalt het de
-   export niet, ook al staat er "Synced").
+   Return Type Boolean → **Ctrl+S**.
 3. Custom Code → `+` → **Widget** → naam `VerbindingsBanner` → body plakken →
    de drie parameters aanmaken → **Ctrl+S**.
-4. `HeaderButtonsComponent` openen. De banner moet **onder** de bestaande Row
-   met de knoppen, niet ernaast (anders verschuift de hamburger). Zit alles nu
-   in één `Row`: die eerst **Wrap Widget → Column**, dan het widget als tweede
-   kind erin slepen (Build-tab → Elements → zoeken op `VerbindingsBanner`).
+4. `HeaderButtonsComponent` openen en het widget **als laatste kind in de
+   bestaande `Row`** zetten (Build-tab → Elements → zoeken op
+   `VerbindingsBanner` → naar het canvas slepen). Laat `width`, `height` én
+   `tekst` **leeg**.
 5. Vier pagina's hebben géén `HeaderButtonsComponent`: `mijnProfiel`,
    `wachtwoordVergeten`, `stadsactiviteitAanmaken`, `uitgaansevenementAanmaken`.
-   Op de twee **aanmaakpagina's** is de banner wel zinvol (een formulier
-   verzenden zonder netwerk) — daar het widget los bovenaan de Column zetten.
+   Op de twee **aanmaakpagina's** is de melding wel zinvol — daar het widget
+   ergens in de bestaande layout zetten (het neemt toch geen ruimte in).
 6. Testen: `adb -s <device> shell svc wifi disable` + `svc data disable`, app
    herstarten, daarna weer `enable`.
 
-✅ **Live getest (2026-09-13, telefoon-emulator), alle vier de paden:**
-1. **online** — geen balk, layout exact als voorheen;
-2. **offline** — rode balk (`primary`, #9A141D) met wolkje en de tekst
-   "Geen verbinding — tik om het opnieuw te proberen";
-3. **netwerk terug + tik** — balk verdwijnt **en Home is volledig hersteld**:
-   slider met kaarten, tabs gevuld, zonder herstart;
-4. **geen nieuwe fouten** — de enige `NoSuchMethodError` in de log stamt van
-   19:18:57 (de offline start), de tik was om 19:19; de route-herlaad verliep
-   schoon, geen go_router-fouten.
-Screenshots van alle drie de toestanden staan in de chat.
-
-⚠️ **Punt 3 was de reden voor een extra ronde.** In de eerste versie verdween de
-balk wél maar bleef het scherm leeg — "tik om opnieuw te proberen" beloofde dus
-meer dan het deed. Daarom herlaadt het widget nu bij een geslaagde tik de
-huidige route (`context.pushReplacement` op het pad uit `GoRouterState`).
-**Alleen bij een handmatige tik**, niet bij de automatische check: dat zou een
-herlaadlus geven omdat de pagina het widget opnieuw opbouwt.
+🛑 **Twee valkuilen die 2026-09-13 allebei toegeslagen hebben — lees dit vóór
+stap 4.**
+
+**(a) Wrap de Row NIET in een Column.** Dat was Claude's eerste instructie en hij
+is fout. De header zit in een `FlexibleSpaceBar.background` met een **vaste
+hoogte van 74 px**; `Align > Row` krijgt die 74 px netjes opgelegd. Zet je er een
+`Column` omheen, dan geeft die zijn kinderen **onbegrensde** hoogte op de
+hoofdas: de knoppen-Row neemt dan zijn natuurlijke ±200 px en je krijgt
+`A RenderFlex overflowed by 166 pixels`. **Erger nog, en dit meldt niets: de
+hamburgerknop verdwijnt dan uit de header op alle elf pagina's.** Alleen de
+overflow staat in de log; de verdwenen knop zie je pas op een screenshot.
+Omdat het widget geen ruimte inneemt, is een Column ook nergens voor nodig —
+gewoon in de bestaande Row.
+
+**(b) Laat FlutterFlow geen Container met vaste maten om het widget zetten.**
+De builder vult standaard `width: 200, height: 40` in. Die 40 px eet in een
+header van 74 px de ruimte van de knoppen op, en op 200 px breedte wrapt de
+meldingstekst naar meerdere regels. **Maak beide velden leeg.**
+
+✅ **Alle drie de paden nagemeten op de emulator met de definitieve opstelling
+(widget in de Row, geen Container):** offline → **0** render-fouten, hamburger
+gewoon zichtbaar, rode balk onderin met "Opnieuw"; netwerk terug + tik → balk
+weg en Home **volledig hersteld** (slider met kaarten, tabs gevuld), opnieuw 0
+fouten. Screenshots staan in de chat.
 
 ⚠️ **Wat de banner NIET oplost:** het grijze blok op Home blijft staan, want dat
 blok ís de gecrashte widget (bevinding C hieronder). De banner maakt het alleen
 verklaarbaar in plaats van kapot. De vier crashplekken blijven dus een aparte
 taak.
 
-### 🛠 Bevinding C · offline-crash op Home/P-Uitgaan-slider — Eigenaar: Claude — bezig (2026-09-13 19:45)
+### 🛠 Bevinding C · offline-crash op Home/P-Uitgaan-slider — Eigenaar: Claude — bezig (2026-09-13 20:10, Bob akkoord)
 
 Aanpak: geen herbouw naar custom action, maar een **Predefined Path** op de
 API-call (`items` = `$[:]`, is-list) en Generate Dynamic Children daaraan
@@ -97,6 +104,17 @@ null-veilig, in tegenstelling tot `getJsonField(jsonBody, r'$').toList()`.
 Plekken: `HomeSliderCall` (Home-slider), `UitgaanSliderCall` (P-Uitgaan-slider),
 `HorecagelegenheidoverzichtCall` (horeca tab 6 — herbinding daar is voor Bob,
 Claude komt niet in dat paneel op die pagina).
+**Stand 20:05:** de drie `items`-paths staan erin (exportgeverifieerd, 3×
+`static List? items`), nog nergens gebruikt. De herbinding van Generate Dynamic
+Children is NIET gedaan — Bob meldde dat hij zelf aan de offline-crash bezig is
+(verbindingscheck); Bob gaf 20:10 akkoord om dit ernaast te doen.
+**Terugdraaien bij calamiteit (per component één dialoog):** Carousel
+selecteren → Generate Dynamic Children (4e icoontje) → Value-icoontje → Set
+from Variable → de eigen Backend-Query-response (`HomeSlider Response` resp.
+`UitgaanSlider Response`) → Available Options **"No Further Changes"** →
+Confirm → Save. Dat is exact de oude binding; nulmeting-export van vóór alle
+wijzigingen staat in `/tmp/ff-checkN0`. De drie `items`-JSON-paths mogen
+gewoon blijven staan, die doen ongebruikt niets.
 
 ### 🐞 Nieuw gevonden 2026-09-13 (Claude, code-audit op verse export)
 

+ 8 - 1
snippets/LEESMIJ.md

@@ -18,7 +18,14 @@ action gewoon op en meldt de builder niets, maar faalt de **Gradle-build** met
   Return Type Boolean, geen argumenten.
 - `verbindings_banner.dart.txt` — Custom **Widget** `VerbindingsBanner`,
   parameters `width` (double, optioneel), `height` (double, optioneel),
-  `tekst` (String, optioneel).
+  `tekst` (String, optioneel). **Laat die drie leeg bij het plaatsen.**
+
+⚠️ **Het widget neemt bewust GEEN ruimte in de layout in** (`SizedBox.shrink`)
+en toont de melding via de `ScaffoldMessenger` over de pagina heen. Reden: de
+header zit in een `FlexibleSpaceBar.background` met een vaste hoogte van 74 px.
+Zet het dus **als laatste kind in de bestaande Row** van
+`HeaderButtonsComponent` — **niet** in een Column eromheen en **niet** in een
+Container met een vaste hoogte. Zie de valkuil in `TASKS.md`.
 
 Beide horen bij de verbindingsmelding in de header; de bouwstappen staan in
 `TASKS.md`.

+ 85 - 60
snippets/verbindings_banner.dart.txt

@@ -10,17 +10,21 @@ import 'package:flutter/material.dart';
 // DO NOT REMOVE OR MODIFY THE CODE ABOVE!
 
 // ⬇⬇⬇ PLAK VANAF HIER ⬇⬇⬇
-// (deze import mag, maar is niet strikt nodig: go_router komt hier ook
-//  via de FlutterFlow-imports binnen — getest 2026-09-13)
-import 'package:go_router/go_router.dart';
 
-/// Balkje dat zichzelf toont zodra de server onbereikbaar is.
+/// Toont een melding zodra de server onbereikbaar is.
+///
+/// Het widget zelf neemt **geen ruimte** in de layout in (`SizedBox.shrink`):
+/// de header van dit project zit in een `FlexibleSpaceBar.background` met een
+/// vaste hoogte, dus een balk die wél ruimte vraagt geeft daar gegarandeerd een
+/// RenderFlex-overflow. In plaats daarvan gaat de melding via de
+/// `ScaffoldMessenger` over de pagina heen. Daardoor kan het widget op één
+/// plek in de gedeelde header staan en werkt het op elke pagina.
 ///
 /// Waarom een custom widget en geen Text met een Visibility-conditie: de
 /// widgets in dit project lezen App State via de singleton `FFAppState()`,
-/// niet via `context.watch`. Een gewone conditie zou dus pas opnieuw
-/// evalueren als er toevallig iets anders een rebuild veroorzaakt. Dit
-/// widget luistert zelf naar FFAppState en verschijnt daardoor direct.
+/// niet via `context.watch`. Een gewone conditie zou dus pas opnieuw evalueren
+/// als er toevallig iets anders een rebuild veroorzaakt. Dit widget luistert
+/// zelf naar FFAppState.
 class VerbindingsBanner extends StatefulWidget {
   const VerbindingsBanner({
     super.key,
@@ -38,17 +42,84 @@ class VerbindingsBanner extends StatefulWidget {
 }
 
 class _VerbindingsBannerState extends State<VerbindingsBanner> {
-  // Gedeeld over alle instanties: bij het doorklikken tussen pagina's wordt
-  // niet bij elke opbouw opnieuw een verzoek gedaan.
+  // Statisch, dus gedeeld over alle instanties: bij doorklikken tussen
+  // pagina's wordt niet bij elke opbouw opnieuw een verzoek gedaan, en er
+  // verschijnt nooit meer dan één balk tegelijk.
   static DateTime? _laatsteCheck;
   static const Duration _minimaalTussenChecks = Duration(seconds: 20);
+  static bool _balkStaatAl = false;
 
   bool _bezig = false;
 
   @override
   void initState() {
     super.initState();
-    WidgetsBinding.instance.addPostFrameCallback((_) => _checkIndienNodig());
+    FFAppState().addListener(_bijStateWijziging);
+    WidgetsBinding.instance.addPostFrameCallback((_) async {
+      await _checkIndienNodig();
+      _werkBalkBij();
+    });
+  }
+
+  @override
+  void dispose() {
+    FFAppState().removeListener(_bijStateWijziging);
+    super.dispose();
+  }
+
+  void _bijStateWijziging() {
+    if (mounted) _werkBalkBij();
+  }
+
+  void _werkBalkBij() {
+    final messenger = ScaffoldMessenger.maybeOf(context);
+    if (messenger == null) return;
+
+    if (!FFAppState().geenVerbinding) {
+      if (_balkStaatAl) {
+        _balkStaatAl = false;
+        messenger.hideCurrentSnackBar();
+      }
+      return;
+    }
+
+    if (_balkStaatAl) return;
+    _balkStaatAl = true;
+    messenger
+        .showSnackBar(
+          SnackBar(
+            backgroundColor: FlutterFlowTheme.of(context).primary,
+            // Praktisch oneindig: de balk hoort te blijven staan zolang er
+            // geen verbinding is, niet na een paar seconden te verdwijnen.
+            duration: const Duration(days: 1),
+            content: Row(
+              mainAxisSize: MainAxisSize.max,
+              children: [
+                const Icon(Icons.cloud_off, color: Colors.white, size: 18.0),
+                const SizedBox(width: 8.0),
+                Expanded(
+                  child: Text(
+                    widget.tekst ?? 'Geen verbinding met uitgaanskrant.com',
+                    style: const TextStyle(color: Colors.white, fontSize: 13.0),
+                  ),
+                ),
+              ],
+            ),
+            action: SnackBarAction(
+              // Bewust een vast label: een SnackBar bouwt niet opnieuw op, dus
+              // een status-afhankelijke tekst blijft hangen op de stand van het
+              // moment waarop hij getoond werd.
+              label: 'Opnieuw',
+              textColor: Colors.white,
+              onPressed: _check,
+            ),
+          ),
+        )
+        .closed
+        .then((_) {
+      // Ook als de gebruiker hem zelf wegveegt, mag er later weer één komen.
+      _balkStaatAl = false;
+    });
   }
 
   Future<void> _checkIndienNodig() async {
@@ -65,13 +136,13 @@ class _VerbindingsBannerState extends State<VerbindingsBanner> {
   /// opnieuw op, dat checkt weer, enzovoort.
   Future<void> _check({bool herlaadBijHerstel = true}) async {
     if (_bezig) return;
-    if (mounted) setState(() => _bezig = true);
+    _bezig = true;
     _laatsteCheck = DateTime.now();
     bool bereikbaar = false;
     try {
       bereikbaar = await checkVerbinding();
     } finally {
-      if (mounted) setState(() => _bezig = false);
+      _bezig = false;
     }
     if (bereikbaar && herlaadBijHerstel && mounted) {
       // De FutureBuilders op deze pagina zijn al klaar met een mislukte
@@ -84,53 +155,7 @@ class _VerbindingsBannerState extends State<VerbindingsBanner> {
 
   @override
   Widget build(BuildContext context) {
-    return AnimatedBuilder(
-      animation: FFAppState(),
-      builder: (context, _) {
-        if (!FFAppState().geenVerbinding) {
-          // Neemt geen ruimte in zolang alles in orde is.
-          return const SizedBox.shrink();
-        }
-        return Material(
-          color: Colors.transparent,
-          child: InkWell(
-            onTap: () => _check(),
-            child: Container(
-              width: widget.width ?? double.infinity,
-              height: widget.height,
-              color: FlutterFlowTheme.of(context).primary,
-              padding:
-                  const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
-              child: Row(
-                mainAxisSize: MainAxisSize.max,
-                children: [
-                  const Icon(Icons.cloud_off, color: Colors.white, size: 18.0),
-                  const SizedBox(width: 8.0),
-                  Expanded(
-                    child: Text(
-                      widget.tekst ??
-                          'Geen verbinding — tik om het opnieuw te proberen',
-                      style: const TextStyle(
-                        color: Colors.white,
-                        fontSize: 13.0,
-                      ),
-                    ),
-                  ),
-                  if (_bezig)
-                    const SizedBox(
-                      width: 14.0,
-                      height: 14.0,
-                      child: CircularProgressIndicator(
-                        strokeWidth: 2.0,
-                        color: Colors.white,
-                      ),
-                    ),
-                ],
-              ),
-            ),
-          ),
-        );
-      },
-    );
+    // Neemt bewust geen ruimte in: de melding gaat via de ScaffoldMessenger.
+    return const SizedBox.shrink();
   }
 }