DynamicTheme stores its state in a VaadinSession attribute that is only set by DynamicTheme.initialize(...), which runs while index.html is being generated (from AppShellConfigurator.configurePage or an IndexHtmlRequestListener). If a UI runs in a session that never served index.html, the attribute is missing and:
DynamicTheme.prepare(...) and DynamicTheme.apply(...) throw IllegalStateException: Dynamic theme switching has not been initialized
DynamicTheme.getCurrent() returns null
TabbedDemo hides the theme selector without any message, because it checks isFeatureInitialized()
This happens in production, not in local development. Two situations cause it:
- The server restarts or scales to zero while a page is open. The session is lost, and the next request from the open tab runs in a new session that never went through
index.html.
index.html is served from a cache (browser, proxy or CDN), so configurePage / the index HTML listener never runs for the session that serves the UI.
Stack trace (commons-demo 5.5.0-SNAPSHOT, Vaadin 25.3.0)
java.lang.IllegalStateException: Dynamic theme switching has not been initialized
at com.flowingcode.vaadin.addons.demo.DynamicTheme.assertFeatureInitialized(DynamicTheme.java:87)
at com.flowingcode.vaadin.addons.demo.DynamicTheme.prepare(DynamicTheme.java:213)
at ...(attach listener calling DynamicTheme.prepare(menuBar))
at com.vaadin.flow.component.ComponentUtil.onComponentAttach(ComponentUtil.java:316)
...
at com.vaadin.flow.component.UI.browserNavigate(UI.java:2205)
...
at com.vaadin.flow.server.communication.ServerRpcHandler.handleInvocations(...)
Steps to reproduce
- Call
DynamicTheme.LUMO.initialize(settings) in configurePage, and call DynamicTheme.prepare(component) on a component in the view.
- Open the app in production mode.
- Restart the server without reloading the browser tab. (Alternatively, serve
index.html from a cache.)
- Interact with or navigate in the open tab.
Expected: the view works with the default theme.
Actual: IllegalStateException from prepare (or a null from getCurrent()).
Suggested fix
Don't make "initialized" depend on the index.html request of the current session. For example:
initialize(...) could also save the chosen default somewhere that is not tied to one session (such as the VaadinContext, or a static field set when DynamicThemeInitializer runs), and getCurrent() could fall back to that default when the session attribute is missing, storing it in the session at that point.
- Alternatively,
DynamicThemeInitializer could register a SessionInitListener that sets the default theme for every new session.
Either way, prepare/apply would no longer throw in these situations. The page may still have a different theme stylesheet than the session thinks (for example, aura.css from an earlier session). That is harmless, because apply() enables and disables the <link> elements on the page regardless of which one is currently active.
Workaround
In the application, register a SessionInitListener that sets DynamicTheme.LUMO (or whichever theme is the default) as the session attribute when it is missing.
DynamicThemestores its state in aVaadinSessionattribute that is only set byDynamicTheme.initialize(...), which runs whileindex.htmlis being generated (fromAppShellConfigurator.configurePageor anIndexHtmlRequestListener). If a UI runs in a session that never servedindex.html, the attribute is missing and:DynamicTheme.prepare(...)andDynamicTheme.apply(...)throwIllegalStateException: Dynamic theme switching has not been initializedDynamicTheme.getCurrent()returnsnullTabbedDemohides the theme selector without any message, because it checksisFeatureInitialized()This happens in production, not in local development. Two situations cause it:
index.html.index.htmlis served from a cache (browser, proxy or CDN), soconfigurePage/ the index HTML listener never runs for the session that serves the UI.Stack trace (commons-demo 5.5.0-SNAPSHOT, Vaadin 25.3.0)
Steps to reproduce
DynamicTheme.LUMO.initialize(settings)inconfigurePage, and callDynamicTheme.prepare(component)on a component in the view.index.htmlfrom a cache.)Expected: the view works with the default theme.
Actual:
IllegalStateExceptionfromprepare(or anullfromgetCurrent()).Suggested fix
Don't make "initialized" depend on the
index.htmlrequest of the current session. For example:initialize(...)could also save the chosen default somewhere that is not tied to one session (such as theVaadinContext, or a static field set whenDynamicThemeInitializerruns), andgetCurrent()could fall back to that default when the session attribute is missing, storing it in the session at that point.DynamicThemeInitializercould register aSessionInitListenerthat sets the default theme for every new session.Either way,
prepare/applywould no longer throw in these situations. The page may still have a different theme stylesheet than the session thinks (for example,aura.cssfrom an earlier session). That is harmless, becauseapply()enables and disables the<link>elements on the page regardless of which one is currently active.Workaround
In the application, register a
SessionInitListenerthat setsDynamicTheme.LUMO(or whichever theme is the default) as the session attribute when it is missing.