From 0718d6e4cbbba8f0bc096b3e6ca495cb1b2b40f5 Mon Sep 17 00:00:00 2001 From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:25:32 -0300 Subject: [PATCH 1/4] fix: fall back to an application-wide default dynamic theme Close #172 --- .../vaadin/addons/demo/DynamicTheme.java | 50 +++++++++++++++++-- .../addons/demo/DynamicThemeInitializer.java | 20 +++++--- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java index 6319247..c66ac6e 100644 --- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java +++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java @@ -90,12 +90,43 @@ private static void assertFeatureInitialized() { /** * Checks if the dynamic theme feature has been initialized for the current session. + *
+ * The feature is initialized if a theme has been set for the current session, or if a default + * theme has been registered for the application. + *
* * @return {@code true} if the feature is supported and initialized; {@code false} otherwise. */ public static boolean isFeatureInitialized() { return isFeatureSupported() - && VaadinSession.getCurrent().getAttribute(DynamicTheme.class) != null; + && (getSessionTheme() != null + || getDefault(VaadinService.getCurrent().getContext()) != null); + } + + private static DynamicTheme getSessionTheme() { + return VaadinSession.getCurrent().getAttribute(DynamicTheme.class); + } + + private static DynamicTheme getDefault(VaadinContext context) { + DefaultTheme defaultTheme = context.getAttribute(DefaultTheme.class); + return defaultTheme != null ? defaultTheme.theme : null; + } + + // Registers this theme as the application-wide default, which is used for sessions that did not + // go through the initialization of index.html (e.g. after a server restart, or when index.html + // was served from a cache). + void setDefault(VaadinContext context) { + context.setAttribute(DefaultTheme.class, new DefaultTheme(this)); + } + + private void setDefaultIfAbsent(VaadinContext context) { + // getAttribute(type, supplier) stores the supplied value when the attribute is absent + context.getAttribute(DefaultTheme.class, () -> new DefaultTheme(this)); + } + + @RequiredArgsConstructor + private static final class DefaultTheme { + private final DynamicTheme theme; } private static void assertNotLegacyTheme() { @@ -115,7 +146,8 @@ private static void assertNotLegacyTheme() { */ public static DynamicTheme getCurrent() { assertFeatureSupported(); - return VaadinSession.getCurrent().getAttribute(DynamicTheme.class); + DynamicTheme theme = getSessionTheme(); + return theme != null ? theme : getDefault(VaadinService.getCurrent().getContext()); } /** @@ -126,6 +158,10 @@ public static DynamicTheme getCurrent() { * as the session default. Subsequently, it injects the corresponding CSS stylesheet * link into the document head. * + *+ * If no application-wide default has been registered, this instance is also registered + * as the default for sessions that did not go through this initialization. + *
* * @param settings the application shell settings to be modified * @throws UnsupportedOperationException if the runtime Vaadin version is older than 25 @@ -136,7 +172,8 @@ public void initialize(AppShellSettings settings) { assertFeatureSupported(); assertNotLegacyTheme(); - DynamicTheme theme = getCurrent(); + setDefaultIfAbsent(VaadinService.getCurrent().getContext()); + DynamicTheme theme = getSessionTheme(); if (theme == null) { theme = this; VaadinSession.getCurrent().setAttribute(DynamicTheme.class, theme); @@ -162,6 +199,10 @@ public void initialize(AppShellSettings settings) { * as the session default. Subsequently, it injects the corresponding CSS stylesheet * link into the document head. * + *+ * If no application-wide default has been registered, this instance is also registered + * as the default for sessions that did not go through this initialization. + *
* * @param response the index HTML response to be modified * @throws UnsupportedOperationException if the runtime Vaadin version is older than 25 @@ -172,7 +213,8 @@ public void initialize(IndexHtmlResponse response) { assertFeatureSupported(); assertNotLegacyTheme(); - DynamicTheme theme = getCurrent(); + setDefaultIfAbsent(VaadinService.getCurrent().getContext()); + DynamicTheme theme = getSessionTheme(); if (theme == null) { theme = this; VaadinSession.getCurrent().setAttribute(DynamicTheme.class, theme); diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicThemeInitializer.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicThemeInitializer.java index fad5616..b556ef3 100644 --- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicThemeInitializer.java +++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicThemeInitializer.java @@ -36,8 +36,8 @@ ** If the dynamic theme feature is supported, this listener checks for the presence of a * {@code /META-INF/dynamic-theme.properties} file. If found, it reads the {@code theme} property - * (e.g., {@code theme=LUMO}) and registers an {@link IndexHtmlRequestListener} to initialize the - * theme for all requests. + * (e.g., {@code theme=LUMO}), registers it as the default theme of the application, and registers + * an {@link IndexHtmlRequestListener} to initialize the theme for all requests. *
*/ @SuppressWarnings("serial") @@ -52,13 +52,19 @@ public void serviceInit(ServiceInitEvent event) { if (DynamicTheme.isFeatureSupported()) { try { Enumeration+ * This annotation must be placed on the {@link AppShellConfigurator} of the application. It + * replaces calling {@link DynamicTheme#initialize(com.vaadin.flow.server.AppShellSettings) + * DynamicTheme.initialize} from {@link AppShellConfigurator#configurePage configurePage}. Since + * the annotation is read when the application starts, the default theme also applies to sessions + * that did not load {@code index.html} (for instance, after a server restart, or when + * {@code index.html} was served from a cache). + *
+ *+ * Dynamic theme switching is only available with Vaadin 25+. With older versions this annotation + * has no effect, so an {@link AppShellConfigurator} that runs on several Vaadin versions can be + * annotated as well. The {@link AppShellConfigurator} must not be annotated with the legacy + * {@link com.vaadin.flow.theme.Theme @Theme} annotation. + *
+ *+ * The annotation is ignored if it is placed on a class that is not the {@link AppShellConfigurator} + * of the application. It is inherited by subclasses of the annotated class. + *
+ */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DefaultDynamicTheme { + + /** + * The default theme. + * + * @return the default theme + */ + DynamicTheme value(); + +} diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java index 8785b9c..549f08c 100644 --- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java +++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java @@ -131,8 +131,10 @@ private static final class DefaultTheme { private static void assertNotLegacyTheme() { VaadinContext context = VaadinService.getCurrent().getContext(); - Class extends AppShellConfigurator> appShellClass = - AppShellRegistry.getInstance(context).getShell(); + assertNotLegacyTheme(AppShellRegistry.getInstance(context).getShell()); + } + + static void assertNotLegacyTheme(Class extends AppShellConfigurator> appShellClass) { if (appShellClass != null && appShellClass.getAnnotation(Theme.class) != null) { throw new IllegalStateException("App shell is configured with legacy @Theme annotation"); } diff --git a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicThemeInitializer.java b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicThemeInitializer.java index b556ef3..4263de9 100644 --- a/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicThemeInitializer.java +++ b/base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicThemeInitializer.java @@ -19,6 +19,8 @@ */ package com.flowingcode.vaadin.addons.demo; +import com.vaadin.flow.component.page.AppShellConfigurator; +import com.vaadin.flow.server.AppShellRegistry; import com.vaadin.flow.server.ServiceInitEvent; import com.vaadin.flow.server.VaadinServiceInitListener; import com.vaadin.flow.server.communication.IndexHtmlRequestListener; @@ -34,10 +36,12 @@ /** * Service initialization listener that automatically applies a dynamic theme. *- * If the dynamic theme feature is supported, this listener checks for the presence of a - * {@code /META-INF/dynamic-theme.properties} file. If found, it reads the {@code theme} property - * (e.g., {@code theme=LUMO}), registers it as the default theme of the application, and registers - * an {@link IndexHtmlRequestListener} to initialize the theme for all requests. + * If the dynamic theme feature is supported, this listener checks whether the + * {@link AppShellConfigurator} is annotated with {@link DefaultDynamicTheme}. Otherwise, it checks + * for the presence of a {@code /META-INF/dynamic-theme.properties} file, and if found, it reads the + * {@code theme} property (e.g., {@code theme=LUMO}). The theme is registered as the default theme + * of the application, and an {@link IndexHtmlRequestListener} is registered to initialize the theme + * for all requests. *
*/ @SuppressWarnings("serial") @@ -50,6 +54,23 @@ public class DynamicThemeInitializer implements VaadinServiceInitListener { @Override public void serviceInit(ServiceInitEvent event) { if (DynamicTheme.isFeatureSupported()) { + Class extends AppShellConfigurator> appShellClass = + AppShellRegistry.getInstance(event.getSource().getContext()).getShell(); + if (appShellClass == null) { + logger.debug("No AppShellConfigurator is registered, @DefaultDynamicTheme is not applied"); + } + DefaultDynamicTheme annotation = appShellClass != null + ? appShellClass.getAnnotation(DefaultDynamicTheme.class) + : null; + if (annotation != null) { + DynamicTheme.assertNotLegacyTheme(appShellClass); + DynamicTheme theme = annotation.value(); + logger.info("Applying dynamic theme '{}' from {}", theme, appShellClass.getName()); + theme.setDefault(event.getSource().getContext()); + event.addIndexHtmlRequestListener(theme::initialize); + return; + } + try { Enumeration