AppTheme.isDark is een statische vlag: oppervlakken blijven na een themawissel in de vorige kleuren staan #814

Closed
opened 2026-07-24 18:26:28 +00:00 by brenno · 3 comments
Owner

Wat er aan de hand is

AppTheme.isDark is een statische vlag. De mode-afhankelijke tokens
(slate600, successBg, paper, …) zijn getters die hem uitlezen. Dat is
een bewuste keuze: een dia moet in een headless export-isolate identiek
renderen aan de preview, en daar bestaat geen BuildContext
(PENTEST_MIAUW §11).

Maar een statische vlag is geen InheritedWidget. Wie hem uitleest, krijgt
geen melding als hij verandert. Het gevolg: élke widget die zich uit
AppTheme kleurt en niet van Theme.of(context) afhangt, houdt na een
themawisseling de kleuren van het vórige thema vast
— tot iets anders hem
toevallig laat herbouwen, of tot een herstart.

Gevonden in #780 aan het slidekwaliteitspaneel: dat bleef donkergroen op een
lichte interface staan. Een andere dia kiezen hielp niet, in- en uitklappen
hielp niet. Dat paneel is gerepareerd; de klasse niet.

Wat er al staat

lib/theme/appearance_scope.dart publiceert de modus als InheritedWidget.
Een oppervlak sluit erop aan met één regel:

AppearanceScope.modeOf(context);

Het kwaliteitspaneel en zijn chip hebben die regel; test/appearance_scope_test.dart
bewaakt het mechanisme en een bronwacht in
test/slide_quality_panel_contrast_test.dart bewaakt dat díe twee hem houden.

Wat er níet werkt — niet nog eens proberen

De voor de hand liggende oplossing is de boom onder home: een sleutel op de
modus geven, zodat álles herbouwt. Dat is in #780 geprobeerd en teruggedraaid:

ProviderException: Tried to use DeckNotifier after `dispose` was called
#31 ConsumerStatefulElement.watch … warmTabDerivedProviders
#33 _TabContent.build (shell/tab_bar.dart:202)

De deck-providers hangen aan het tabblad. Die boom afbreken disposet
DeckNotifier terwijl er nog naar geluisterd wordt — en erger dan de crash is
wat eraan voorafgaat: dat is het niet-opgeslagen deck van de gebruiker. Dit kan
pas als de deckstaat boven die grens is getild, en dat is een aparte
verbouwing.

Wat er te kiezen valt

  1. De regel uitrollen. Elk oppervlak dat een AppTheme.*-getter leest
    krijgt AppearanceScope.modeOf(context), en een bronwacht dwingt dat af:
    een bestand dat een mode-afhankelijk token gebruikt in een widget-build
    moet de modus lezen. Mechanisch, meetbaar, en het houdt de statische
    getters intact — dus de export-isolate blijft werken. Nadeel: een regel
    ruis per widget, en de wacht moet onderscheid maken tussen een build en
    een hulpfunctie.

  2. De tokens uit de statische laag halen voor alles wat chrome is, en
    alleen de dia-tokens vast laten. Dat is de zuivere oplossing en de dure:
    ~200 gebruiksplekken, en de scheidslijn dia/chrome is precies wat #606 al
    eens handmatig heeft moeten trekken.

  3. De deckstaat boven het tabblad tillen en dan alsnog de sleutel op de
    modus zetten. Lost het in één klap op voor élke widget, maar raakt de
    provider-architectuur en de tabbladen.

Voorkeur van mij is 1: het is de enige die vandaag kan, en de bronwacht maakt
hem afdwingbaar in plaats van een goede gewoonte. 2 en 3 blijven daarna
mogelijk zonder dat 1 weggegooid hoeft te worden.

Hoe je het reproduceert

  1. Instellingen → App-thema → Donker → Opslaan.
  2. Open een deck en zet een oppervlak op het scherm dat zich uit AppTheme
    kleurt en niet meebeweegt.
  3. Instellingen → App-thema → Europa → Opslaan.
  4. Het oppervlak staat nog in de donkere kleuren.
## Wat er aan de hand is `AppTheme.isDark` is een **statische vlag**. De mode-afhankelijke tokens (`slate600`, `successBg`, `paper`, …) zijn getters die hem uitlezen. Dat is een bewuste keuze: een dia moet in een headless export-isolate identiek renderen aan de preview, en daar bestaat geen `BuildContext` (PENTEST_MIAUW §11). Maar een statische vlag is geen `InheritedWidget`. Wie hem uitleest, krijgt geen melding als hij verandert. Het gevolg: **élke widget die zich uit `AppTheme` kleurt en niet van `Theme.of(context)` afhangt, houdt na een themawisseling de kleuren van het vórige thema vast** — tot iets anders hem toevallig laat herbouwen, of tot een herstart. Gevonden in #780 aan het slidekwaliteitspaneel: dat bleef donkergroen op een lichte interface staan. Een andere dia kiezen hielp niet, in- en uitklappen hielp niet. Dat paneel is gerepareerd; de klasse niet. ## Wat er al staat `lib/theme/appearance_scope.dart` publiceert de modus als `InheritedWidget`. Een oppervlak sluit erop aan met één regel: ```dart AppearanceScope.modeOf(context); ``` Het kwaliteitspaneel en zijn chip hebben die regel; `test/appearance_scope_test.dart` bewaakt het mechanisme en een bronwacht in `test/slide_quality_panel_contrast_test.dart` bewaakt dat díe twee hem houden. ## Wat er níet werkt — niet nog eens proberen De voor de hand liggende oplossing is de boom onder `home:` een sleutel op de modus geven, zodat álles herbouwt. Dat is in #780 geprobeerd en teruggedraaid: ``` ProviderException: Tried to use DeckNotifier after `dispose` was called #31 ConsumerStatefulElement.watch … warmTabDerivedProviders #33 _TabContent.build (shell/tab_bar.dart:202) ``` De deck-providers hangen aan het tabblad. Die boom afbreken disposet `DeckNotifier` terwijl er nog naar geluisterd wordt — en erger dan de crash is wat eraan voorafgaat: dat is het niet-opgeslagen deck van de gebruiker. Dit kan pas als de deckstaat boven die grens is getild, en dat is een aparte verbouwing. ## Wat er te kiezen valt 1. **De regel uitrollen.** Elk oppervlak dat een `AppTheme.*`-getter leest krijgt `AppearanceScope.modeOf(context)`, en een bronwacht dwingt dat af: een bestand dat een mode-afhankelijk token gebruikt in een widget-`build` moet de modus lezen. Mechanisch, meetbaar, en het houdt de statische getters intact — dus de export-isolate blijft werken. Nadeel: een regel ruis per widget, en de wacht moet onderscheid maken tussen een `build` en een hulpfunctie. 2. **De tokens uit de statische laag halen** voor alles wat chrome is, en alleen de dia-tokens vast laten. Dat is de zuivere oplossing en de dure: ~200 gebruiksplekken, en de scheidslijn dia/chrome is precies wat #606 al eens handmatig heeft moeten trekken. 3. **De deckstaat boven het tabblad tillen** en dan alsnog de sleutel op de modus zetten. Lost het in één klap op voor élke widget, maar raakt de provider-architectuur en de tabbladen. Voorkeur van mij is 1: het is de enige die vandaag kan, en de bronwacht maakt hem afdwingbaar in plaats van een goede gewoonte. 2 en 3 blijven daarna mogelijk zonder dat 1 weggegooid hoeft te worden. ## Hoe je het reproduceert 1. Instellingen → App-thema → *Donker* → Opslaan. 2. Open een deck en zet een oppervlak op het scherm dat zich uit `AppTheme` kleurt en niet meebeweegt. 3. Instellingen → App-thema → *Europa* → Opslaan. 4. Het oppervlak staat nog in de donkere kleuren.
Author
Owner

Nagekeken vóór oppakken: dit kan nog niet. lib/theme/appearance_scope.dart staat niet op maingrep -rn AppearanceScope lib/ test/ op 5db87945 geeft nul treffers, en test/appearance_scope_test.dart en test/slide_quality_panel_contrast_test.dart bestaan daar evenmin.

Het mechanisme waar alle drie de opties op leunen zit dus nog op de tak van #780 en is niet gemerged. Optie 1 uitrollen betekent nu: het zelf een tweede keer bouwen, naast een versie die al bestaat — precies de dubbele oplossing die een conflict wordt.

Niet geclaimd, geen in-progress. Dit wacht op de merge van #780; daarna is het een mechanische ronde plus de bronwacht.

Nagekeken vóór oppakken: **dit kan nog niet.** `lib/theme/appearance_scope.dart` staat niet op `main` — `grep -rn AppearanceScope lib/ test/` op `5db87945` geeft nul treffers, en `test/appearance_scope_test.dart` en `test/slide_quality_panel_contrast_test.dart` bestaan daar evenmin. Het mechanisme waar alle drie de opties op leunen zit dus nog op de tak van #780 en is niet gemerged. Optie 1 uitrollen betekent nu: het zelf een tweede keer bouwen, naast een versie die al bestaat — precies de dubbele oplossing die een conflict wordt. Niet geclaimd, geen `in-progress`. Dit wacht op de merge van #780; daarna is het een mechanische ronde plus de bronwacht.
Author
Owner

De blokkade uit de vorige reactie is weg: #780 is gemerged (ac997733), dus lib/theme/appearance_scope.dart staat op main en het mechanisme is er.

Opgepakt. Tak: fix/apptheme-statische-vlag-814.

Eerst gemeten wat optie 1 werkelijk kost: 139 bestanden, 776 gebruiksplekken van een mode-afhankelijk AppTheme-token. Dat is de omvang van de mechanische ronde. Voor ik daaraan begin kijk ik naar een route die niet in de drie hierboven stond — de elementenboom van de wortel af markeren als needsBuild zonder hem weg te gooien. Dat behoudt élke State (dus geen DeckNotifier-dispose, dus geen verlies van het niet-opgeslagen deck) en laat toch elke build opnieuw lopen. Als dat werkt is het een reparatie van tientallen regels in plaats van 139 bestanden; als het niet werkt, is optie 1 er nog.

Ik meld hier wat het wordt vóór ik het uitrol.

De blokkade uit de vorige reactie is weg: #780 is gemerged (`ac997733`), dus `lib/theme/appearance_scope.dart` staat op main en het mechanisme is er. Opgepakt. Tak: `fix/apptheme-statische-vlag-814`. Eerst gemeten wat optie 1 werkelijk kost: **139 bestanden, 776 gebruiksplekken** van een mode-afhankelijk `AppTheme`-token. Dat is de omvang van de mechanische ronde. Voor ik daaraan begin kijk ik naar een route die niet in de drie hierboven stond — de elementenboom van de wortel af markeren als `needsBuild` zonder hem weg te gooien. Dat behoudt élke `State` (dus geen `DeckNotifier`-dispose, dus geen verlies van het niet-opgeslagen deck) en laat toch elke `build` opnieuw lopen. Als dat werkt is het een reparatie van tientallen regels in plaats van 139 bestanden; als het niet werkt, is optie 1 er nog. Ik meld hier wat het wordt vóór ik het uitrol.
Author
Owner

Gemerged in 266275db (PR #818), en op main geverifieerd — appearance_scope_test draait daar groen.

Eén ding hoort in het issue en niet alleen in de PR: de diagnose hierboven klopte niet. Ik schreef dat de scope zijn verandering niet meldt en dat elk oppervlak daarom een eigen aansluiting nodig had. De scope herbouwt wél; wat stopt is de laag eronder, want Element.updateChild slaat een herbouw over zodra het nieuwe widget identiek is aan het oude — en twee const-instanties van hetzelfde zíjn identiek.

Daardoor is optie 1 (139 bestanden, 776 plekken) niet nodig gebleken en is geen van de drie routes uit dit issue gevolgd. Wat het wel werd: bij een moduswissel élk element onder de scope als 'moet opnieuw bouwen' markeren zonder er een weg te gooien. Elke build loopt opnieuw, en géén State sneuvelt — dat laatste is precies waarom het márkeren is en geen sleutel op de modus, die in #780 het niet-opgeslagen deck meenam.

Netto is er meer weg dan bij: de aansluitregel in het kwaliteitspaneel, de bronwacht die hem afdwong, modeOf en de InheritedWidget eronder zijn allemaal geschrapt.

En een waarschuwing voor wie hier later komt: de toets die met #780 meekwam kon deze bug per constructie niet vangen. Twee keer pumpWidget vervangt de wortel en herbouwt de hele boom, dus het blad kleurde 'vanzelf' goed. De nieuwe opstelling volgt de app — modus boven de MaterialApp, blad eronder achter een const kind — en wordt rood zodra de markering eruit gaat.

Gemerged in `266275db` (PR #818), en op main geverifieerd — `appearance_scope_test` draait daar groen. Eén ding hoort in het issue en niet alleen in de PR: **de diagnose hierboven klopte niet.** Ik schreef dat de scope zijn verandering niet meldt en dat elk oppervlak daarom een eigen aansluiting nodig had. De scope herbouwt wél; wat stopt is de laag eronder, want `Element.updateChild` slaat een herbouw over zodra het nieuwe widget identiek is aan het oude — en twee `const`-instanties van hetzelfde zíjn identiek. Daardoor is optie 1 (139 bestanden, 776 plekken) niet nodig gebleken en is geen van de drie routes uit dit issue gevolgd. Wat het wel werd: bij een moduswissel élk element onder de scope als 'moet opnieuw bouwen' markeren zonder er een weg te gooien. Elke `build` loopt opnieuw, en géén `State` sneuvelt — dat laatste is precies waarom het márkeren is en geen sleutel op de modus, die in #780 het niet-opgeslagen deck meenam. Netto is er meer weg dan bij: de aansluitregel in het kwaliteitspaneel, de bronwacht die hem afdwong, `modeOf` en de `InheritedWidget` eronder zijn allemaal geschrapt. En een waarschuwing voor wie hier later komt: **de toets die met #780 meekwam kon deze bug per constructie niet vangen.** Twee keer `pumpWidget` vervangt de wortel en herbouwt de hele boom, dus het blad kleurde 'vanzelf' goed. De nieuwe opstelling volgt de app — modus boven de `MaterialApp`, blad eronder achter een `const` kind — en wordt rood zodra de markering eruit gaat.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
LibreKAT/Ocideck#814
No description provided.