-
-
Notifications
You must be signed in to change notification settings - Fork 82
feat: Add theme support to Stac framework with cloud loading #393
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
Add comprehensive theme support to the Stac framework, enabling themes to be loaded from Stac Cloud similar to screens. This includes: - Add StacArtifactType enum to support both screens and themes - Add StacCloudTheme annotation for marking theme builder methods - Add StacAppTheme helper class for loading themes from cloud, network, or JSON - Extend StacApp to support FutureOr<StacTheme?> for async theme loading - Refactor StacCacheService to be artifact-type agnostic (supports screens and themes) - Refactor StacCloud to support fetching both screens and themes with unified caching Themes can now be loaded asynchronously from Stac Cloud using the same caching strategies as screens (optimistic, cache-first, network-first, etc.), with proper loading states handled in StacApp.
WalkthroughThis PR introduces asynchronous theme loading and resolution for the Stac framework. It generalizes the caching and cloud-fetch system from screen-only artifacts to support multiple types (screens, themes), adds a new StacAppTheme utility class for loading themes from cloud/network/JSON, and exports a new StacCloudTheme annotation for marking theme definition methods. Changes
Sequence Diagram(s)sequenceDiagram
participant App as StacApp
participant Builder as _withResolvedThemes
participant Resolver as _resolveThemes
participant Theme as StacAppTheme
participant Network as Network/Cloud
participant Material as MaterialApp
App->>Builder: build with FutureOr themes
Builder->>Resolver: resolve theme & darkTheme
Resolver->>Theme: theme.runtimeType check
alt theme is Future
Theme->>Network: fetch theme (async)
Network-->>Theme: StacTheme
else theme is StacTheme
Theme-->>Resolver: immediate value
end
Theme-->>Resolver: resolved theme
Resolver-->>Builder: _ResolvedStacThemes
Builder->>Builder: await resolution via FutureBuilder
opt still loading
Builder->>App: show _ThemeFutureLoading
end
Builder->>Material: MaterialApp with parsed themes
Material-->>App: rendered app
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
🧹 Nitpick comments (9)
packages/stac/lib/src/framework/stac_app.dart (2)
270-280: Consider resolving theme futures in parallel.Both theme futures are awaited sequentially. Using
Future.waitwould allow parallel resolution.return Future<_ResolvedStacThemes>(() async { - final resolvedTheme = - await (themeFuture ?? Future<StacTheme?>.value(themeValue)); - final resolvedDarkTheme = - await (darkThemeFuture ?? Future<StacTheme?>.value(darkThemeValue)); + final results = await Future.wait([ + themeFuture ?? Future<StacTheme?>.value(themeValue), + darkThemeFuture ?? Future<StacTheme?>.value(darkThemeValue), + ]); return _ResolvedStacThemes( - theme: resolvedTheme, - darkTheme: resolvedDarkTheme, + theme: results[0], + darkTheme: results[1], ); });
317-324: Loading widget has no theme context.This widget renders before
MaterialAppis built, so there's no inherited theme. TheMaterialwidget will use fallback colors which may cause a visual flash when the app loads. Consider specifying explicit colors.@override Widget build(BuildContext context) { - return const Material(child: Center(child: CircularProgressIndicator())); + return const ColoredBox( + color: Colors.white, // Or Colors.transparent for less jarring transition + child: Center(child: CircularProgressIndicator()), + ); }packages/stac/lib/src/framework/stac_app_theme.dart (1)
16-33: Silent null returns may hinder debugging.Multiple failure paths return
nullwithout any indication of what went wrong. Consider adding debug logging to help diagnose theme loading issues in development.static Future<StacTheme?> fromCloud({required String themeName}) async { final response = await StacCloud.fetchTheme(themeName: themeName); if (response == null) { + assert(() { debugPrint('StacAppTheme.fromCloud: fetchTheme returned null for "$themeName"'); return true; }()); return null; } final rawData = response.data; if (rawData is! Map<String, dynamic>) { + assert(() { debugPrint('StacAppTheme.fromCloud: response.data is not Map<String, dynamic>'); return true; }()); return null; }packages/stac/lib/src/services/stac_cache_service.dart (2)
19-29: UseStacArtifactTypeenum instead of string for type safety.The
artifactTypeparameter accepts any string, but only'screen'and'theme'are valid. Using theStacArtifactTypeenum would provide compile-time safety and prevent runtimeArgumentError.- static String _getCachePrefix(String artifactType) { - switch (artifactType) { - case 'screen': + static String _getCachePrefix(StacArtifactType artifactType) { + switch (artifactType) { + case StacArtifactType.screen: return 'stac_screen_cache_'; - case 'theme': + case StacArtifactType.theme: return 'stac_theme_cache_'; - default: - throw ArgumentError('Unknown artifact type: $artifactType'); } }This would require updating all method signatures to accept
StacArtifactTypeinstead ofString.
48-48:StacScreenCacheclass used for theme artifacts.The
StacScreenCacheclass is now used to cache both screens and themes, which is a naming inconsistency. Consider renaming toStacArtifactCachefor clarity, or document that the class is intentionally reused.Also applies to: 68-73
packages/stac/lib/src/models/stac_artifact_type.dart (1)
1-8: Inconsistent artifact type handling betweenStacCloudandStacCacheService.The
StacArtifactTypeenum is actively used throughoutStacCloud(via switch statements and type-safe parameters), but whenStacCloudcallsStacCacheServicemethods (e.g.,clearScreenCache,clearThemeCache), it passes hardcoded strings ('screen','theme') instead of enum values. Meanwhile,StacCacheService._getCachePrefix()switches on these string literals. Consider havingStacCacheServiceacceptStacArtifactTypeinstead ofStringto maintain type consistency and avoid string literal brittleness at the boundary.packages/stac/lib/src/services/stac_cloud.dart (3)
69-150: Tighten strategy handling in_fetchArtifactto avoid confusing dead branchThe overall flow (early handling of
networkOnly/cacheOnly, then switching on the remaining strategies) is good, but the finalcacheOnly/networkOnlycases in theswitchare now unreachable and contradict the comment:case StacCacheStrategy.cacheOnly: case StacCacheStrategy.networkOnly: // Already handled above return _fetchArtifactFromNetwork( artifactType: artifactType, artifactName: artifactName, saveToCache: false, );Because both strategies are already fully handled by the
ifbranches above (return/throw), this code will never run, but if someone refactors out the earlyifs later it would silently changecacheOnlyto “network-only, no cache”.Consider either:
- Moving
networkOnly/cacheOnlyhandling entirely into theswitchand dropping the earlyifs, or- Keeping the early
ifs and turning these two cases into an explicit unreachable/StateErrorso any future misuse is caught quickly.For example:
case StacCacheStrategy.optimistic: return _handleArtifactOptimistic( artifactType: artifactType, artifactName: artifactName, cachedArtifact: cachedArtifact, isCacheValid: isCacheValid, config: cacheConfig, ); case StacCacheStrategy.cacheOnly: case StacCacheStrategy.networkOnly: - // Already handled above - return _fetchArtifactFromNetwork( - artifactType: artifactType, - artifactName: artifactName, - saveToCache: false, - ); + // These should have been handled by the early returns above. + throw StateError( + 'Unexpected strategy ${cacheConfig.strategy} in _fetchArtifact; ' + 'cacheOnly/networkOnly should be handled before the switch.', + );Functionally nothing is broken today, but this makes the intent clearer and safer for future changes.
174-266: Strategy helpers read correctly and match the intended semantics
_handleArtifactNetworkFirst,_handleArtifactCacheFirst, and_handleArtifactOptimisticcorrectly implement the advertised behaviors (network-first with cache fallback, cache-first with optional background refresh and stale-while-revalidate, and optimistic with background update). Logging withartifactType.nameis helpful for debugging.The only (very minor) nit is that
StacScreenCacheis now effectively an artifact cache model for both screens and themes; if you ever generalize more artifact types, you might consider renaming it to something likeStacArtifactCachefor clarity, but that would be a larger cross-file change.
381-421: Theme fetch/clear helpers mirror screen APIs; consider removing hard-coded type stringsThe new
fetchTheme,clearThemeCache, andclearAllThemeCachehelpers align well with the existing screen APIs and the artifact-type abstraction.For the cache-clear methods, you’re still using string literals
'screen'/'theme':static Future<bool> clearScreenCache(String routeName) { return StacCacheService.removeArtifact(routeName, 'screen'); } static Future<bool> clearAllCache() { return StacCacheService.clearAllArtifacts('screen'); } static Future<bool> clearThemeCache(String themeName) { return StacCacheService.removeArtifact(themeName, 'theme'); } static Future<bool> clearAllThemeCache() { return StacCacheService.clearAllArtifacts('theme'); }Since you already have
_getArtifactTypeString, you can avoid duplicating these strings and keep everything enum-driven:static Future<bool> clearScreenCache(String routeName) { - return StacCacheService.removeArtifact(routeName, 'screen'); + return StacCacheService.removeArtifact( + routeName, + _getArtifactTypeString(StacArtifactType.screen), + ); } static Future<bool> clearAllCache() { - return StacCacheService.clearAllArtifacts('screen'); + return StacCacheService.clearAllArtifacts( + _getArtifactTypeString(StacArtifactType.screen), + ); } static Future<bool> clearThemeCache(String themeName) { - return StacCacheService.removeArtifact(themeName, 'theme'); + return StacCacheService.removeArtifact( + themeName, + _getArtifactTypeString(StacArtifactType.theme), + ); } static Future<bool> clearAllThemeCache() { - return StacCacheService.clearAllArtifacts('theme'); + return StacCacheService.clearAllArtifacts( + _getArtifactTypeString(StacArtifactType.theme), + ); }This keeps the cache-layer contract tied to the enum and avoids subtle drift if the underlying type strings ever change.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
packages/stac/lib/src/framework/framework.dart(1 hunks)packages/stac/lib/src/framework/stac_app.dart(7 hunks)packages/stac/lib/src/framework/stac_app_theme.dart(1 hunks)packages/stac/lib/src/models/stac_artifact_type.dart(1 hunks)packages/stac/lib/src/services/stac_cache_service.dart(4 hunks)packages/stac/lib/src/services/stac_cloud.dart(7 hunks)packages/stac_core/lib/annotations/annotations.dart(1 hunks)packages/stac_core/lib/annotations/stac_cloud_theme.dart(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: analyze
🔇 Additional comments (9)
packages/stac_core/lib/annotations/annotations.dart (1)
3-4: Newstac_cloud_theme.dartexport looks consistentExporting the new annotation alongside
stac_screen.dartkeeps the annotations barrel file up to date and is aligned with how other annotations are exposed.packages/stac_core/lib/annotations/stac_cloud_theme.dart (1)
13-18: Annotation shape is simple and appropriate for intended useConst constructor with a required
themeNameand a singlefinalfield is a clean, idiomatic way to model this annotation and should work well for discovery/registration of theme builders.packages/stac/lib/src/framework/stac_app.dart (1)
113-114: Good backward-compatible API change.The change from
StacTheme?toFutureOr<StacTheme?>?maintains backward compatibility - existing synchronous theme assignments continue to work while enabling async loading.packages/stac/lib/src/framework/framework.dart (1)
5-5: LGTM!Export added to expose the new
StacAppThemeutility class through the framework's public API surface.packages/stac/lib/src/framework/stac_app_theme.dart (1)
39-49: LGTM!The
fromNetworkmethod correctly delegates toStacNetworkService.requestand handles null responses gracefully. TheBuildContextusage is appropriate as it's passed directly to the service.packages/stac/lib/src/services/stac_cache_service.dart (2)
130-139: LGTM!The cache validity logic is clear and handles edge cases properly - null artifact returns false, null maxAge means no expiration. The age comparison is straightforward.
97-110: LGTM!Good use of
Future.waitfor parallel key removal, and prefix-based filtering correctly isolates artifacts by type.packages/stac/lib/src/services/stac_cloud.dart (2)
26-52: Helper extraction for artifact URLs and params looks solidCentralizing
_baseUrl,_getFetchUrl,_getQueryParamName, and_getArtifactTypeStringkeeps screen/theme handling consistent and makes it easy to extend to new artifact types. No changes requested here.
268-331: Network + cache integration for artifacts looks consistent
_makeArtifactRequest,_fetchArtifactFromNetwork, and_buildArtifactCacheResponsenicely generalize the old screen-only path:
- Request URL and query param are derived from the artifact type.
- Cache read/write uses
_getArtifactTypeString(artifactType)consistently, so screens and themes share the same caching mechanism with type partitioning.- Cached responses are shaped identically to network ones (
name,stacJson,version), which keeps downstream consumers simple.No issues from a correctness standpoint; just ensure the Cloud endpoints for both screens and themes always return that
{ name, stacJson, version }shape so caching works uniformly.
| /// Example usage: | ||
| /// ```dart | ||
| /// @StacThemeAnnotation(themeName: 'darkTheme') | ||
| /// ThemeData buildDarkTheme() { | ||
| /// return ThemeData.dark(); | ||
| /// } | ||
| /// ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doc example uses the wrong annotation name
The example shows @StacThemeAnnotation but this file defines StacCloudTheme. That mismatch can confuse users and docs/tools.
Consider updating the example to use the actual annotation:
-/// @StacThemeAnnotation(themeName: 'darkTheme')
+/// @StacCloudTheme(themeName: 'darkTheme')📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /// Example usage: | |
| /// ```dart | |
| /// @StacThemeAnnotation(themeName: 'darkTheme') | |
| /// ThemeData buildDarkTheme() { | |
| /// return ThemeData.dark(); | |
| /// } | |
| /// ``` | |
| /// Example usage: | |
| /// |
🤖 Prompt for AI Agents
In packages/stac_core/lib/annotations/stac_cloud_theme.dart around lines 6 to
12, the documentation example uses the wrong annotation name
(`@StacThemeAnnotation`) which doesn't match the defined annotation
`StacCloudTheme`; update the example to use the actual annotation (e.g.,
`@StacCloudTheme(themeName: 'darkTheme')`) and ensure the annotated function
signature in the example matches the library's expected usage so docs and tools
are consistent.
| if (payload is String) { | ||
| final decoded = jsonDecode(payload); | ||
| if (decoded is Map<String, dynamic>) { | ||
| return decoded; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jsonDecode can throw FormatException on invalid JSON.
If payload is a malformed JSON string, jsonDecode throws an exception that isn't caught, propagating up to the caller unexpectedly.
if (payload is String) {
- final decoded = jsonDecode(payload);
- if (decoded is Map<String, dynamic>) {
- return decoded;
+ try {
+ final decoded = jsonDecode(payload);
+ if (decoded is Map<String, dynamic>) {
+ return decoded;
+ }
+ } on FormatException {
+ return null;
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (payload is String) { | |
| final decoded = jsonDecode(payload); | |
| if (decoded is Map<String, dynamic>) { | |
| return decoded; | |
| } | |
| } | |
| if (payload is String) { | |
| try { | |
| final decoded = jsonDecode(payload); | |
| if (decoded is Map<String, dynamic>) { | |
| return decoded; | |
| } | |
| } on FormatException { | |
| return null; | |
| } | |
| } |
🤖 Prompt for AI Agents
In packages/stac/lib/src/framework/stac_app_theme.dart around lines 73 to 78,
jsonDecode(payload) can throw a FormatException for malformed JSON; wrap the
jsonDecode call in a try/catch that catches FormatException (and optionally any
other parsing errors), handle the error by returning null (or the function's
appropriate fallback) and/or log the parse failure so the exception doesn't
propagate; ensure existing behavior still returns the decoded Map when parsing
succeeds.
| Widget _withResolvedThemes( | ||
| BuildContext context, | ||
| Widget Function(BuildContext, _ResolvedStacThemes) builder, | ||
| ) { | ||
| final resolved = _resolveThemes(); | ||
| if (resolved is Future<_ResolvedStacThemes>) { | ||
| return FutureBuilder<_ResolvedStacThemes>( | ||
| future: resolved, | ||
| builder: (futureContext, snapshot) { | ||
| if (snapshot.connectionState == ConnectionState.waiting) { | ||
| return const _ThemeFutureLoading(); | ||
| } | ||
| if (snapshot.hasError) { | ||
| return const _ThemeFutureLoading(); | ||
| } | ||
| final themes = snapshot.data; | ||
| if (themes == null) { | ||
| return const _ThemeFutureLoading(); | ||
| } | ||
| return builder(futureContext, themes); | ||
| }, | ||
| ); | ||
| } | ||
| return builder(context, resolved); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Future is recreated on every widget rebuild.
_resolveThemes() is called inside _withResolvedThemes during build(). When theme or darkTheme is a Future, a new Future object is created on each rebuild. FutureBuilder uses reference equality, so it will restart the async operation and show the loading state again whenever StacApp rebuilds.
Consider caching the resolved future in a stateful widget or using AsyncSnapshot's connectionState more carefully, or lift the resolution to an ancestor widget/state.
-class StacApp extends StatelessWidget {
+class StacApp extends StatefulWidget {
const StacApp({
...
});
+
+ @override
+ State<StacApp> createState() => _StacAppState();
+}
+
+class _StacAppState extends State<StacApp> {
+ late final FutureOr<_ResolvedStacThemes> _resolvedThemes;
+
+ @override
+ void initState() {
+ super.initState();
+ _resolvedThemes = _resolveThemes();
+ }
+
+ // Move build logic here, using _resolvedThemes instead of calling _resolveThemes()Committable suggestion skipped: line range outside the PR's diff.
| if (snapshot.hasError) { | ||
| return const _ThemeFutureLoading(); | ||
| } | ||
| final themes = snapshot.data; | ||
| if (themes == null) { | ||
| return const _ThemeFutureLoading(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Error state is silently swallowed with no feedback.
When theme loading fails, the same loading indicator is shown indefinitely. Consider logging the error and/or falling back to a default theme rather than showing a perpetual loading state.
if (snapshot.hasError) {
+ // Log error for debugging
+ debugPrint('Theme loading failed: ${snapshot.error}');
- return const _ThemeFutureLoading();
+ // Fall back to no theme rather than infinite loading
+ return builder(futureContext, const _ResolvedStacThemes(theme: null, darkTheme: null));
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In packages/stac/lib/src/framework/stac_app.dart around lines 295-300 the
snapshot.hasError branch currently returns the same loading widget and swallows
the error; change this to log the snapshot.error (using debugPrint or the app
logger) and return a sensible fallback (for example a default ThemeData-based
widget or an error/fallback _Theme widget) instead of _ThemeFutureLoading so the
app doesn't show a perpetual spinner; ensure snapshot.error and
snapshot.stackTrace are included in the log and that the fallback provides safe
default colors/fonts so UI remains usable.
| StacArtifactType.theme: {}, | ||
| }; | ||
|
|
||
| /// Fetches an artifact from Stac Cloud with intelligent caching. | ||
| /// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix _backgroundFetchInProgress value literals (currently map literals, not sets)
Right now the values in _backgroundFetchInProgress use {}, which is a Map literal in Dart, but the field type is Map<StacArtifactType, Set<String>>. This won’t compile:
static final Map<StacArtifactType, Set<String>> _backgroundFetchInProgress = {
StacArtifactType.screen: {},
StacArtifactType.theme: {},
};You need set literals here:
- static final Map<StacArtifactType, Set<String>> _backgroundFetchInProgress = {
- StacArtifactType.screen: {},
- StacArtifactType.theme: {},
- };
+ static final Map<StacArtifactType, Set<String>> _backgroundFetchInProgress = {
+ StacArtifactType.screen: <String>{},
+ StacArtifactType.theme: <String>{},
+ };Given _fetchAndUpdateArtifactInBackground does _backgroundFetchInProgress[artifactType]!, any missing key will also throw at runtime; if you expect more artifact types later, consider building this map from StacArtifactType.values to stay future-proof.
Also applies to: 337-378
🤖 Prompt for AI Agents
packages/stac/lib/src/services/stac_cloud.dart lines 54-58 (and similarly lines
337-378): the map is typed as Map<StacArtifactType, Set<String>> but uses {}
which creates Map literals and not Set<String> — replace the value literals with
explicit set literals like <String>{} (e.g. StacArtifactType.screen: <String>{},
StacArtifactType.theme: <String>{}); to be future-proof you can build the map
from StacArtifactType.values (e.g.
Map.fromEntries(StacArtifactType.values.map((t) => MapEntry(t, <String>{})))) or
ensure every enum key is initialized so
_backgroundFetchInProgress[artifactType]! never returns null (alternatively use
putIfAbsent before accessing).
Description
This PR adds comprehensive theme support to the Stac framework, enabling themes to be loaded from Stac Cloud similar to how screens are currently loaded. The implementation includes async theme loading, intelligent caching, and proper context handling.
Key Changes
New Features:
StacArtifactTypeenum to support both screens and themes as fetchable artifactsStacCloudThemeannotation for marking theme builder methodsStacAppThemehelper class with methods to load themes from:fromCloud)fromNetwork)fromJson)StacAppto supportFutureOr<StacTheme?>for boththemeanddarkThemeparameters, enabling async theme loading_ThemeFutureLoadingwidget when themes are being fetchedRefactoring:
StacCacheServiceto be artifact-type agnostic:getCachedArtifact,saveArtifact, etc.)'screen'and'theme'artifact typesStacCloudto support fetching both screens and themes:_fetchArtifactmethodfetchThememethod as public APIclearThemeCache,clearAllThemeCache)Type of Change
Summary by CodeRabbit
Release Notes
✏️ Tip: You can customize this high-level summary in your review settings.