Skip to content

Commit a028171

Browse files
feat: Add caching mechanism for Stac JSONs. (#386)
1 parent 24bcb68 commit a028171

File tree

19 files changed

+1276
-15
lines changed

19 files changed

+1276
-15
lines changed

docs/concepts/caching.mdx

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
---
2+
title: "Caching & Offline Support"
3+
description: "Learn how Stac intelligently caches screens for offline access, faster loading, and smarter network usage"
4+
---
5+
6+
Stac includes a powerful caching system that stores screens locally, enabling offline access, instant loading, and more efficient network usage. The caching layer works automatically with the `Stac` widget and can be customized for different use cases.
7+
8+
## How Caching Works
9+
10+
When you use the `Stac` widget to fetch screens from Stac Cloud:
11+
12+
1. **First Load**: The screen is fetched from the network and stored in local cache
13+
2. **Subsequent Loads**: Based on your cache strategy, Stac returns cached data and/or fetches fresh data
14+
3. **Version Tracking**: Each cached screen stores its version number, enabling smart updates
15+
4. **Background Updates**: Fresh data can be fetched in the background without blocking the UI
16+
17+
## Cache Strategies
18+
19+
Stac provides five caching strategies to fit different use cases:
20+
21+
### 1. Optimistic (Default)
22+
23+
Returns cached data immediately while fetching updates in the background. Best for fast perceived performance.
24+
25+
```dart
26+
Stac(
27+
routeName: '/home',
28+
cacheConfig: StacCacheConfig(
29+
strategy: StacCacheStrategy.optimistic,
30+
),
31+
)
32+
```
33+
34+
**Behavior:**
35+
- ✅ Returns cached data instantly (even if expired)
36+
- ✅ Fetches fresh data in background
37+
- ✅ Updates cache for next load
38+
- ⚡ Fastest perceived loading
39+
40+
**Best for:** UI layouts, content screens, any screen where instant loading matters more than showing the absolute latest data.
41+
42+
### 2. Cache First
43+
44+
Uses cached data if available and valid, only fetches from network when cache is invalid or missing.
45+
46+
```dart
47+
Stac(
48+
routeName: '/home',
49+
cacheConfig: StacCacheConfig(
50+
strategy: StacCacheStrategy.cacheFirst,
51+
maxAge: Duration(hours: 24),
52+
),
53+
)
54+
```
55+
56+
**Behavior:**
57+
- ✅ Uses cache if valid (not expired)
58+
- ✅ Fetches from network only when cache is invalid
59+
- ✅ Optionally refreshes in background
60+
- 📱 Great for offline-first apps
61+
62+
**Best for:** Offline-first apps, content that doesn't change frequently, reducing network usage.
63+
64+
### 3. Network First
65+
66+
Always tries the network first, falls back to cache if network fails.
67+
68+
```dart
69+
Stac(
70+
routeName: '/home',
71+
cacheConfig: StacCacheConfig(
72+
strategy: StacCacheStrategy.networkFirst,
73+
),
74+
)
75+
```
76+
77+
**Behavior:**
78+
- ✅ Always fetches fresh data first
79+
- ✅ Falls back to cache on network error
80+
- ✅ Ensures latest content when online
81+
- 🌐 Requires network for best experience
82+
83+
**Best for:** Real-time data, frequently changing content, screens where freshness is critical.
84+
85+
### 4. Cache Only
86+
87+
Only uses cached data, never makes network requests. Throws an error if no cache exists.
88+
89+
```dart
90+
Stac(
91+
routeName: '/home',
92+
cacheConfig: StacCacheConfig(
93+
strategy: StacCacheStrategy.cacheOnly,
94+
),
95+
)
96+
```
97+
98+
**Behavior:**
99+
- ✅ Never makes network requests
100+
- ✅ Instant loading from cache
101+
- ❌ Fails if no cached data exists
102+
- 📴 Perfect for offline mode
103+
104+
**Best for:** Offline-only mode, airplane mode, when you've pre-cached screens.
105+
106+
### 5. Network Only
107+
108+
Always fetches from network, never uses or updates cache.
109+
110+
```dart
111+
Stac(
112+
routeName: '/home',
113+
cacheConfig: StacCacheConfig(
114+
strategy: StacCacheStrategy.networkOnly,
115+
),
116+
)
117+
```
118+
119+
**Behavior:**
120+
- ✅ Always fresh data
121+
- ❌ Fails without network
122+
- ❌ No offline support
123+
- 🔒 Good for sensitive data
124+
125+
**Best for:** Sensitive data that shouldn't be cached, real-time dashboards, authentication screens.
126+
127+
## Cache Configuration
128+
129+
### StacCacheConfig Properties
130+
131+
| Property | Type | Default | Description |
132+
|----------|------|---------|-------------|
133+
| `strategy` | `StacCacheStrategy` | `optimistic` | The caching strategy to use |
134+
| `maxAge` | `Duration?` | `null` | Maximum age before cache is considered expired. `null` means no time-based expiration |
135+
| `refreshInBackground` | `bool` | `true` | Whether to fetch fresh data in background when cache is valid |
136+
| `staleWhileRevalidate` | `bool` | `false` | Whether to use expired cache while fetching fresh data |
137+
138+
### Setting Cache Duration
139+
140+
Control how long cached data remains valid:
141+
142+
```dart
143+
// Cache valid for 1 hour
144+
Stac(
145+
routeName: '/home',
146+
cacheConfig: StacCacheConfig(
147+
strategy: StacCacheStrategy.cacheFirst,
148+
maxAge: Duration(hours: 1),
149+
),
150+
)
151+
152+
// Cache valid for 7 days
153+
Stac(
154+
routeName: '/settings',
155+
cacheConfig: StacCacheConfig(
156+
strategy: StacCacheStrategy.cacheFirst,
157+
maxAge: Duration(days: 7),
158+
),
159+
)
160+
161+
// Cache never expires (only version-based updates)
162+
Stac(
163+
routeName: '/static-page',
164+
cacheConfig: StacCacheConfig(
165+
strategy: StacCacheStrategy.cacheFirst,
166+
maxAge: null, // No time-based expiration
167+
),
168+
)
169+
```
170+
171+
### Background Refresh
172+
173+
Keep cache fresh without blocking the UI:
174+
175+
```dart
176+
Stac(
177+
routeName: '/home',
178+
cacheConfig: StacCacheConfig(
179+
strategy: StacCacheStrategy.cacheFirst,
180+
maxAge: Duration(hours: 1),
181+
refreshInBackground: true, // Fetch updates silently
182+
),
183+
)
184+
```
185+
186+
When `refreshInBackground` is `true`:
187+
- Valid cache is returned immediately
188+
- Fresh data is fetched in background
189+
- Cache is updated for next load
190+
- User sees instant loading with eventual consistency
191+
192+
### Stale While Revalidate
193+
194+
Show expired cache while fetching fresh data:
195+
196+
```dart
197+
Stac(
198+
routeName: '/home',
199+
cacheConfig: StacCacheConfig(
200+
strategy: StacCacheStrategy.cacheFirst,
201+
maxAge: Duration(hours: 1),
202+
staleWhileRevalidate: true, // Use expired cache while fetching
203+
),
204+
)
205+
```
206+
207+
This is useful when:
208+
- You prefer showing something over a loading spinner
209+
- Content staleness is acceptable for a brief period
210+
- Network is slow or unreliable
211+
212+
## Strategy Comparison
213+
214+
| Strategy | Initial Load | Subsequent Load | Offline Support | Best For |
215+
|----------|--------------|-----------------|-----------------|----------|
216+
| `optimistic` | Network → Cache | Cache (bg update) | ✅ Yes | Fast UX |
217+
| `cacheFirst` | Cache or Network | Cache | ✅ Yes | Offline apps |
218+
| `networkFirst` | Network | Network (cache fallback) | ⚠️ Fallback only | Fresh data |
219+
| `cacheOnly` | Cache only | Cache only | ✅ Yes | Offline mode |
220+
| `networkOnly` | Network only | Network only | ❌ No | Sensitive data |
221+
222+
## Version-Based Updates
223+
224+
Stac tracks version numbers for each cached screen. When you deploy updates to Stac Cloud:
225+
226+
1. The server assigns a new version number
227+
2. Background fetches detect the new version
228+
3. Cache is updated with new content
229+
4. Next load shows updated screen
230+
231+
This ensures users get updates without manual intervention while maintaining fast loading times.
232+

docs/docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"icon": "book",
3939
"pages": [
4040
"concepts/rendering_stac_widgets",
41+
"concepts/caching",
4142
"concepts/custom_widgets",
4243
"concepts/custom_actions"
4344
]

examples/counter_example/macos/Flutter/GeneratedPluginRegistrant.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import FlutterMacOS
66
import Foundation
77

88
import path_provider_foundation
9+
import shared_preferences_foundation
910
import sqflite_darwin
1011

1112
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
1213
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
14+
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
1315
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
1416
}

examples/counter_example/pubspec.lock

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,11 @@ packages:
299299
description: flutter
300300
source: sdk
301301
version: "0.0.0"
302+
flutter_web_plugins:
303+
dependency: transitive
304+
description: flutter
305+
source: sdk
306+
version: "0.0.0"
302307
freezed:
303308
dependency: "direct dev"
304309
description:
@@ -627,6 +632,62 @@ packages:
627632
url: "https://pub.dev"
628633
source: hosted
629634
version: "0.28.0"
635+
shared_preferences:
636+
dependency: transitive
637+
description:
638+
name: shared_preferences
639+
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
640+
url: "https://pub.dev"
641+
source: hosted
642+
version: "2.5.3"
643+
shared_preferences_android:
644+
dependency: transitive
645+
description:
646+
name: shared_preferences_android
647+
sha256: "07d552dbe8e71ed720e5205e760438ff4ecfb76ec3b32ea664350e2ca4b0c43b"
648+
url: "https://pub.dev"
649+
source: hosted
650+
version: "2.4.16"
651+
shared_preferences_foundation:
652+
dependency: transitive
653+
description:
654+
name: shared_preferences_foundation
655+
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
656+
url: "https://pub.dev"
657+
source: hosted
658+
version: "2.5.6"
659+
shared_preferences_linux:
660+
dependency: transitive
661+
description:
662+
name: shared_preferences_linux
663+
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
664+
url: "https://pub.dev"
665+
source: hosted
666+
version: "2.4.1"
667+
shared_preferences_platform_interface:
668+
dependency: transitive
669+
description:
670+
name: shared_preferences_platform_interface
671+
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
672+
url: "https://pub.dev"
673+
source: hosted
674+
version: "2.4.1"
675+
shared_preferences_web:
676+
dependency: transitive
677+
description:
678+
name: shared_preferences_web
679+
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
680+
url: "https://pub.dev"
681+
source: hosted
682+
version: "2.4.3"
683+
shared_preferences_windows:
684+
dependency: transitive
685+
description:
686+
name: shared_preferences_windows
687+
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
688+
url: "https://pub.dev"
689+
source: hosted
690+
version: "2.4.1"
630691
shelf:
631692
dependency: transitive
632693
description:
@@ -726,7 +787,7 @@ packages:
726787
path: "../../packages/stac"
727788
relative: true
728789
source: path
729-
version: "1.1.0"
790+
version: "1.1.2"
730791
stac_core:
731792
dependency: "direct overridden"
732793
description:
@@ -925,5 +986,5 @@ packages:
925986
source: hosted
926987
version: "3.1.3"
927988
sdks:
928-
dart: ">=3.8.0-0 <4.0.0"
929-
flutter: ">=3.29.0"
989+
dart: ">=3.9.0 <4.0.0"
990+
flutter: ">=3.35.0"

examples/movie_app/ios/Podfile.lock

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,33 @@ PODS:
33
- path_provider_foundation (0.0.1):
44
- Flutter
55
- FlutterMacOS
6+
- shared_preferences_foundation (0.0.1):
7+
- Flutter
8+
- FlutterMacOS
69
- sqflite_darwin (0.0.4):
710
- Flutter
811
- FlutterMacOS
912

1013
DEPENDENCIES:
1114
- Flutter (from `Flutter`)
1215
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
16+
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
1317
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
1418

1519
EXTERNAL SOURCES:
1620
Flutter:
1721
:path: Flutter
1822
path_provider_foundation:
1923
:path: ".symlinks/plugins/path_provider_foundation/darwin"
24+
shared_preferences_foundation:
25+
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
2026
sqflite_darwin:
2127
:path: ".symlinks/plugins/sqflite_darwin/darwin"
2228

2329
SPEC CHECKSUMS:
2430
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
2531
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
32+
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
2633
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
2734

2835
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e

examples/movie_app/macos/Flutter/GeneratedPluginRegistrant.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import FlutterMacOS
66
import Foundation
77

88
import path_provider_foundation
9+
import shared_preferences_foundation
910
import sqflite_darwin
1011

1112
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
1213
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
14+
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
1315
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
1416
}

0 commit comments

Comments
 (0)