sfwf 2.0.8
sfwf: ^2.0.8 copied to clipboard
Smart Flutter Web Framework v2.0 - SEO, SSR, pre-rendering, AI optimization for Flutter. The ultimate platform for building production-grade web apps with Flutter.
Smart Flutter Web Framework (SFWF) v2 #
SFWF is a production-ready Flutter web framework solving 10 major Flutter web problems — SEO, Server-Side Rendering, PWA, routing, performance, and cross-platform compatibility. It scores 160/160 on pub.dev with full WASM support.
Demo Preview #
| Home Page | Projects | Blog | Contact | Dark Mode |
|---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
Demo Video #
| Video | Link |
|---|---|
| Full App Demo | |
| SEO Configuration |
The 10 Problems SFWF Solves #
| # | Problem | Without SFWF | With SFWF |
|---|---|---|---|
| 1 | No SEO | Search engines see a blank canvas | Dynamic meta tags, Open Graph, JSON-LD, sitemap.xml |
| 2 | Hash-based URLs | #/products/123 ugly URLs |
Clean URLs /products/123 |
| 3 | No SSR | Slow initial load, poor SEO | Puppeteer SSR + pre-rendering |
| 4 | Heavy bundle | Slow first paint | Lazy loading, code splitting |
| 5 | No PWA | Can't install or work offline | Service Worker + manifest.json |
| 6 | Unoptimized images | Slow page loads | Auto resize + compress + WebP |
| 7 | No responsive | Desktop-only layout | DeviceDetector + AdaptiveBuilder |
| 8 | No meta per page | Same title everywhere | SeoController per page |
| 9 | No offline | No data without internet | CacheManager + OfflineProvider |
| 10 | Plugin conflicts | dart:html fails on mobile |
PluginFallback |
Installation #
From pub.dev #
dependencies:
sfwf: ^2.0.8
flutter pub get
From GitHub (bleeding edge) #
dependencies:
sfwf:
git: https://github.com/BahyOsama/sfwf.git
Platform Support #
| Platform | Status | Notes |
|---|---|---|
| Web | Full | SEO, SSR, PWA, all features |
| Android | Full | All features except SSR |
| iOS | Full | All features except SSR |
| Windows | Full | All features except SSR |
| Linux | Full | All features except SSR |
| macOS | Full | All features except SSR |
| WASM | Full | Conditional stubs for dart:io features |
Quick Start #
Step 1: Create entry point #
// main.dart
import 'package:flutter/material.dart';
import 'package:sfwf/sfwf.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return SFWFApp(
config: const SFWFConfig(
appName: 'My App',
baseUrl: 'https://example.com',
),
routes: {
'/': (ctx) => const HomePage(),
'/about': (ctx) => const AboutPage(),
},
);
}
}
Step 2: Add SEO per page #
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
SeoController.of(context).updatePage(const SeoData(
title: 'Home - My App',
description: 'Welcome to My App built with SFWF',
ogType: 'website',
));
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: const Center(child: Text('Hello SFWF!')),
);
}
}
Step 3: Run #
flutter run -d chrome
Real Algorithms & Production Code #
1. Route Matching Algorithm (Segmented Pattern Matcher) #
File: lib/router/smart_router.dart:104-133
The router uses a segmented pattern-matching algorithm with O(n*m) complexity where n = route definitions and m = path segments.
Route: /users/:id/posts/:postId
URL: /users/42/posts/567
Match: { id: '42', postId: '567' }
Algorithm:
- Split both the pattern and URL by
/ - Compare segment-by-segment
- If pattern segment starts with
:, capture as a parameter - If segments differ and no
:prefix, reject match - Return named route or matched params
// Production route matching algorithm
RouteMatch? _matchRoute(String path) {
if (routes.containsKey(path)) {
return RouteMatch(path, params: {});
}
for (final definition in routeDefinitions) {
final pattern = definition.path;
final patternSegments = pattern.split('/');
final pathSegments = path.split('/');
if (patternSegments.length != pathSegments.length) continue;
bool match = true;
final params = <String, String>{};
for (int i = 0; i < patternSegments.length; i++) {
if (patternSegments[i].startsWith(':')) {
// Capture dynamic segment (e.g., :id -> '42')
params[patternSegments[i].substring(1)] = pathSegments[i];
} else if (patternSegments[i] != pathSegments[i]) {
match = false;
break;
}
}
if (match) {
return RouteMatch(definition.name ?? path, params: params);
}
}
return null;
}
Production usage with guards and middleware:
// Complete production router setup
SFWFApp(
config: const SFWFConfig(
appName: 'Blog',
baseUrl: 'https://blog.example.com',
),
routes: {
'/': (ctx) => const HomePage(),
'/blog': (ctx) => const BlogListPage(),
'/blog/:slug': (ctx) {
final args = ModalRoute.of(ctx)?.settings.arguments;
final params = args is Map<String, String> ? args : <String, String>{};
return BlogDetailPage(slug: params['slug'] ?? '');
},
'/dashboard': (ctx) => const DashboardPage(),
'/admin/users/:id': (ctx) {
final args = ModalRoute.of(ctx)?.settings.arguments;
final params = args is Map<String, String> ? args : <String, String>{};
return UserProfilePage(userId: params['id'] ?? '');
},
},
routeDefinitions: [
const RouteDefinition(path: '/blog/:slug', name: 'blog-detail'),
const RouteDefinition(path: '/admin/users/:id', name: 'user-profile'),
],
guards: [
RouteGuard(
guard: (context) {
// Check authentication for protected routes
final isAuthenticated = AuthService.isLoggedIn;
if (!isAuthenticated) {
// Redirect logic handled internally
return false;
}
return true;
},
),
],
preProcessors: [
(context, route) async {
// Analytics tracking
await Analytics.logPageView(route);
// Dynamic title generation
SeoController.of(context).updatePage(SeoData(
title: 'Blog - ${route.replaceAll('/', '').replaceAll('-', ' ')}',
));
},
],
notFoundBuilder: (ctx) => const NotFoundPage(),
customTransitions: {
'/': (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
'/blog': (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.05),
end: Offset.zero,
).animate(CurvedAnimation(parent: animation, curve: Curves.easeOut)),
child: child,
);
},
},
);
2. SEO Meta Injection Algorithm (Create-or-Update DOM Strategy) #
File: lib/seo/dom_operations_web.dart:8-101
The SEO engine uses a create-or-update DOM algorithm: query for an existing meta tag by name; if found, update its content; if not found, create and append it.
Algorithm:
1. Merge SeoData with defaults (null-safe field override)
2. Set document.title directly
3. For each meta tag: querySelector('meta[name="..."])
→ exists? update content attribute
→ not exist? createElement('meta'), setAttribute, appendChild
4. For JSON-LD: querySelector('#sfwf-json-ld')
→ exists? update textContent
→ not exist? create script, set id + type, appendChild, set textContent
5. For canonical URL: querySelector('link[rel="canonical"]')
→ same create-or-update pattern
// Production SEO meta injection algorithm
class DomOperations {
void applyToDom(SeoData data, SFWFConfig config) {
if (!kIsWeb) return;
// 1. Build title with suffix
web.document.title = data.title?.isNotEmpty == true
? '${data.title}${config.seoDefaults.titleSuffix}'
: config.appName;
// 2. Open Graph tags
_setMeta('description', data.description ?? config.seoDefaults.defaultDescription);
_setMeta('og:title', data.title ?? config.appName);
_setMeta('og:description', data.description ?? config.seoDefaults.defaultDescription);
_setMeta('og:image', data.image ?? config.seoDefaults.defaultImage);
_setMeta('og:url', config.baseUrl + web.window.location.pathname);
_setMeta('og:type', data.ogType ?? 'website');
_setMeta('og:site_name', config.appName);
// 3. Twitter Cards
_setMeta('twitter:card', data.twitterCard ?? 'summary_large_image');
_setMeta('twitter:title', data.title ?? config.appName);
_setMeta('twitter:description', data.description ?? config.seoDefaults.defaultDescription);
_setMeta('twitter:image', data.image ?? config.seoDefaults.defaultImage);
_setMeta('twitter:site', config.seoDefaults.twitterHandle);
// 4. Additional tags
if (data.keywords != null) _setMeta('keywords', data.keywords!);
if (data.themeColor != null) {
_setMeta('theme-color', data.themeColor!);
_setMeta('msapplication-TileColor', data.themeColor!);
}
if (data.canonicalUrl != null) _setLink('canonical', data.canonicalUrl!);
if (data.structuredData != null) _injectJsonLd(data.structuredData!);
if (data.noIndex == true) _setMeta('robots', 'noindex, nofollow');
}
// Create-or-update algorithm for meta tags
void _setMeta(String name, String content) {
var meta = web.document.querySelector('meta[name="$name"]');
if (meta == null) {
meta = web.document.createElement('meta');
meta.setAttribute('name', name);
web.document.head!.appendChild(meta);
}
meta.setAttribute('content', content);
}
// Single JSON-LD element strategy (avoids duplicate structured data)
void _injectJsonLd(Map<String, dynamic> json) {
var script = web.document.getElementById('sfwf-json-ld');
if (script == null) {
script = web.document.createElement('script');
script.id = 'sfwf-json-ld';
script.setAttribute('type', 'application/ld+json');
web.document.head!.appendChild(script);
}
script.textContent = jsonEncode(json);
}
}
Production SEO configuration with all fields:
// Complete SEO setup
void main() {
runApp(
SFWFApp(
config: SFWFConfig(
appName: 'My Store',
baseUrl: 'https://mystore.com',
seoDefaults: SeoDefaults(
titleSuffix: ' | My Store',
defaultDescription: 'Shop the best products online.',
defaultImage: 'https://mystore.com/og-default.png',
twitterHandle: '@mystore',
),
),
),
);
}
// Per-page SEO
class ProductPage extends StatelessWidget {
final Product product;
const ProductPage({required this.product, super.key});
@override
Widget build(BuildContext context) {
SeoController.of(context).updatePage(SeoData(
title: product.name,
description: product.description.substring(0, 160), // SEO best practice
image: product.imageUrl,
ogType: 'product',
canonicalUrl: 'https://mystore.com/products/${product.id}',
structuredData: {
'@context': 'https://schema.org',
'@type': 'Product',
'name': product.name,
'description': product.description,
'image': product.imageUrl,
'sku': product.sku,
'brand': {'@type': 'Brand', 'name': product.brand},
'offers': {
'@type': 'Offer',
'url': 'https://mystore.com/products/${product.id}',
'priceCurrency': 'USD',
'price': product.price,
'availability': product.inStock
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
},
},
));
return Scaffold(
appBar: AppBar(title: Text(product.name)),
body: ProductDetail(product: product),
);
}
}
3. Cache Manager Algorithm (TTL-Based Expiry with JSON Serialization) #
File: lib/data/cache_manager_real.dart:1-59
The cache uses a TTL-based expiry algorithm with JSON-serialized CacheEntry objects persisted via Hive.
CacheEntry {
value: dynamic (any JSON-serializable data)
expiry: DateTime (creation time + TTL)
}
Set(key, value, ttl):
entry = CacheEntry(value, now + ttl)
hiveBox.put(key, jsonEncode(entry.toJson()))
Get(key):
raw = hiveBox.get(key)
if raw is null → return null
entry = CacheEntry.fromJson(jsonDecode(raw))
if entry.expiry > now → return entry.value
else → remove(key), return null
// Production cache manager with TTL expiry
class CacheManager {
static final CacheManager instance = CacheManager._internal();
late Box<String> _cacheBox;
bool _initialized = false;
CacheManager._internal();
Future<void> init() async {
if (_initialized) return;
await Hive.initFlutter();
_cacheBox = await Hive.openBox<String>('sfwf_cache');
_initialized = true;
}
void set(String key, dynamic value, {Duration ttl = const Duration(minutes: 10)}) {
final entry = CacheEntry(value, DateTime.now().add(ttl));
_cacheBox.put(key, jsonEncode(entry.toJson()));
}
dynamic get(String key) {
final raw = _cacheBox.get(key);
if (raw == null) return null;
try {
final entry = CacheEntry.fromJson(jsonDecode(raw));
if (entry.expiry.isAfter(DateTime.now())) {
return entry.value;
}
remove(key); // Auto-clean expired entries
} catch (_) {
remove(key); // Clean corrupted entries
}
return null;
}
void remove(String key) => _cacheBox.delete(key);
Future<void> clear() => _cacheBox.clear();
}
// CacheEntry with serialization
class CacheEntry {
final dynamic value;
final DateTime expiry;
CacheEntry(this.value, this.expiry);
Map<String, dynamic> toJson() => {
'value': value,
'expiry': expiry.toIso8601String(),
};
factory CacheEntry.fromJson(Map<String, dynamic> json) => CacheEntry(
json['value'],
DateTime.parse(json['expiry']),
);
}
Production caching patterns:
// Pattern 1: Cache user session
CacheManager.instance.set(
'session_${user.id}',
user.toJson(),
ttl: const Duration(hours: 24),
);
// Pattern 2: Cache API responses
Future<ApiResponse> fetchProducts() async {
const cacheKey = 'api_products';
// Check cache first
final cached = CacheManager.instance.get(cacheKey);
if (cached != null) return ApiResponse.fromJson(cached);
// Fetch from network
final response = await http.get(Uri.parse('https://api.example.com/products'));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
CacheManager.instance.set(cacheKey, data, ttl: const Duration(minutes: 5));
return ApiResponse.fromJson(data);
}
throw Exception('Failed to load products');
}
// Pattern 3: Cache with fallback
dynamic getCachedOrFetch(String key, Future<dynamic> Function() fetcher,
{Duration ttl = const Duration(minutes: 5)}) async {
final cached = CacheManager.instance.get(key);
if (cached != null) return cached;
try {
final data = await fetcher();
CacheManager.instance.set(key, data, ttl: ttl);
return data;
} catch (e) {
final cached = CacheManager.instance.get(key);
if (cached != null) return cached;
rethrow;
}
}
4. Offline-First Fetch Algorithm (Cache → Network → Cache-Fallback Chain) #
File: lib/data/offline_provider.dart:1-24
Algorithm:
1. Try CacheManager.get(url) — instant return if found
2. If not cached → HTTP GET the URL
3. On success (200) → CacheManager.set(url, body, ttl), return body
4. On failure → try CacheManager.get(url) one more time (fallback)
5. If still no cache → rethrow
// Production offline-first provider
class OfflineProvider {
static Future<dynamic> fetchOrCache(String url, Duration ttl) async {
// First: check cache (instant)
final cached = CacheManager.instance.get(url);
if (cached != null) return cached;
try {
// Second: fetch from network
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
// Cache for future offline use
CacheManager.instance.set(url, response.body, ttl: ttl);
return response.body;
}
throw Exception('Failed to load data: ${response.statusCode}');
} catch (e) {
// Third: fallback to cache on failure
final cached = CacheManager.instance.get(url);
if (cached != null) return cached;
rethrow;
}
}
}
Production usage with state management:
// Riverpod provider with offline-first strategy
final postsProvider = FutureProvider<List<Post>>((ref) async {
const cacheKey = 'posts_list';
// 1. Try cache
final cached = CacheManager.instance.get(cacheKey);
if (cached != null) {
return (cached as List).map((e) => Post.fromJson(e)).toList();
}
// 2. Fetch network
final raw = await OfflineProvider.fetchOrCache(
'https://api.example.com/posts',
const Duration(minutes: 15),
);
final List<dynamic> jsonList = jsonDecode(raw);
final posts = jsonList.map((e) => Post.fromJson(e)).toList();
// 3. Cache parsed data
CacheManager.instance.set(cacheKey, posts.map((e) => e.toJson()).toList(),
ttl: const Duration(minutes: 15));
return posts;
});
5. Image Optimization Pipeline (Resize → Compress → WebP) #
File: lib/performance/image_optimizer_io.dart:7-65
The image optimizer uses a recursive directory traversal algorithm with progressive compression:
Algorithm:
1. Recursively list all files in input directory
2. Filter by extension: .jpg, .jpeg, .png, .webp, .bmp, .gif
3. For each image:
a. Read raw bytes
b. Decode image
c. If width > maxWidth (1200px): resize proportionally
d. Re-encode: PNG → PNG, everything else → JPEG (quality 85%)
e. Write to output directory
f. Log savings percentage
4. Report: "X optimized, Y skipped"
// Production image optimization pipeline
class ImageOptimizer {
static Future<void> optimizeAll(
String inputDir,
String outputDir, {
int maxWidth = 1200,
int quality = 85,
}) async {
final input = Directory(inputDir);
if (!await input.exists()) return;
await Directory(outputDir).create(recursive: true);
int optimized = 0, skipped = 0;
await for (var entity in input.list(recursive: true)) {
if (entity is File && _isImage(entity.path)) {
try {
final bytes = await entity.readAsBytes();
final image = img.decodeImage(bytes);
if (image != null) {
// Step 1: Resize if wider than maxWidth
final resized = image.width > maxWidth
? img.copyResize(image, width: maxWidth)
: image;
// Step 2: Re-encode with compression
final ext = p.extension(entity.path).toLowerCase();
final newFileName =
'${p.basenameWithoutExtension(entity.path)}${ext == '.png' ? '.png' : '.jpg'}';
final optimizedBytes = ext == '.png'
? img.encodePng(resized)
: img.encodeJpg(resized, quality: quality);
// Step 3: Write output
await File(p.join(outputDir, newFileName)).writeAsBytes(optimizedBytes);
// Step 4: Report savings
final savedPercent =
((1 - optimizedBytes.length / bytes.length) * 100).toStringAsFixed(1);
log('Optimized: ${p.basename(entity.path)} (saved $savedPercent%)');
optimized++;
} else {
skipped++;
}
} catch (_) {
skipped++;
}
}
}
log('Completed: $optimized optimized, $skipped skipped.');
}
static bool _isImage(String path) {
final ext = p.extension(path).toLowerCase();
return ['.jpg', '.jpeg', '.png', '.webp', '.bmp', '.gif'].contains(ext);
}
}
Usage:
dart run sfwf build --optimize-images
Typical results:
- JPEG: 60-80% size reduction
- PNG: 40-60% size reduction
- Large images auto-resized to 1200px max width
6. Service Worker Algorithm (SHA-256 Cache Versioning) #
File: lib/performance/service_worker_io.dart:7-94
Uses a content-addressed caching algorithm with SHA-256 hashing:
Algorithm:
1. Recursively list all files in web build output
2. For each file:
a. Read content as bytes
b. Compute SHA-256 hash
c. Truncate to first 8 hex characters
d. Store in versionHashes map: { path: hash }
3. Generate service_worker.js with:
- CACHE_NAME: 'sfwf-cache-v2'
- urlsToCache: all file paths
- versionHashes: path → hash mapping
- Install event: cache all URLs
- Fetch event: stale-while-revalidate
- Activate event: delete old caches
// Generated service worker algorithm
const CACHE_NAME = 'sfwf-cache-v2';
const urlsToCache = ['/index.html', '/main.dart.js', ...];
const versionHashes = {'/index.html': 'a1b2c3d4', ...};
// Install: cache all files
self.addEventListener('install', event => {
self.skipWaiting();
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return Promise.all(
urlsToCache.map(url => cache.add(url).catch(() => {}))
);
})
);
});
// Fetch: stale-while-revalidate
self.addEventListener('fetch', event => {
if (event.request.method !== 'GET') return;
event.respondWith(
caches.match(event.request).then(cached => {
const fetchPromise = fetch(event.request).then(networkResponse => {
if (networkResponse.ok) {
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, networkResponse.clone());
});
}
return networkResponse;
}).catch(() => cached);
return cached || fetchPromise;
})
);
});
// Activate: clean old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) return caches.delete(cacheName);
})
);
}).then(() => self.clients.claim())
);
});
7. SSR Rendering Algorithm (Puppeteer Headless Browser) #
File: lib/ssr/ssr_renderer_puppeteer.dart:1-38
Algorithm:
1. Launch Puppeteer headless Chrome (lazy init on first call)
2. Open new browser page
3. Navigate to target URL with wait: networkIdle
(waits until network is idle — all AJAX/fonts/images loaded)
4. Extract full page HTML via page.content
5. Close the page
6. Return HTML string for server response or file write
class PuppeteerSsrRenderer implements SsrRenderer {
Browser? _browser;
bool _initialized = false;
Future<void> _ensureInit() async {
if (_initialized && _browser != null) return;
_browser = await puppeteer.launch(headless: true, args: ['--no-sandbox']);
_initialized = true;
}
@override
Future<String> renderToString(String url) async {
await _ensureInit();
final page = await _browser!.newPage();
try {
await page.goto(url, wait: Until.networkIdle);
return await page.content ?? '';
} finally {
await page.close(); // Always clean up
}
}
@override
Future<void> close() async {
if (_browser != null) {
await _browser!.close();
_browser = null;
_initialized = false;
}
}
}
Production SSR server:
// SSR server with caching (server.dart)
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:sfwf/ssr/ssr_renderer_puppeteer.dart';
Future<void> main() async {
final renderer = PuppeteerSsrRenderer();
final cache = <String, String>{};
final app = Router();
app.get('/<path|.*>', (request) async {
final url = 'http://localhost:8080${request.url.path}';
// Check SSR cache (5 min TTL)
final cached = cache[url];
if (cached != null) {
return Response.ok(cached, headers: {'Content-Type': 'text/html'});
}
// Render via Puppeteer
final html = await renderer.renderToString(url);
cache[url] = html;
return Response.ok(html, headers: {'Content-Type': 'text/html'});
});
final server = await io.serve(app, 'localhost', 8080);
print('SSR Server running on http://${server.address.host}:${server.port}');
}
8. Device Detection Algorithm (Platform-Based Type Inference) #
File: lib/device/device_detector.dart:1-40
Algorithm:
1. Check cached type (returns instantly after first call)
2. If not web:
- Android/iOS → DeviceType.mobile
- macOS/Windows/Linux/Fuchsia → DeviceType.desktop
- (tablet detection requires screen size analysis externally)
3. If web → DeviceType.desktop (default; user agent parsing optional)
4. Cache result for subsequent calls
class DeviceDetector {
static DeviceType? _cachedType;
static DeviceType get type {
if (_cachedType != null) return _cachedType!;
if (!kIsWeb) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
_cachedType = DeviceType.mobile;
case TargetPlatform.macOS:
case TargetPlatform.windows:
case TargetPlatform.linux:
case TargetPlatform.fuchsia:
_cachedType = DeviceType.desktop;
}
} else {
_cachedType = DeviceType.desktop;
}
return _cachedType!;
}
static bool get isMobile => type == DeviceType.mobile;
static bool get isDesktop => type == DeviceType.desktop;
static bool get isTablet => type == DeviceType.tablet;
}
Production responsive layout:
class AppLayout extends StatelessWidget {
const AppLayout({super.key});
@override
Widget build(BuildContext context) {
return AdaptiveBuilder(
builder: (ctx, type) {
switch (type) {
case DeviceType.desktop:
return const Row(
children: [
NavigationRail(), // Persistent sidebar
Expanded(child: PageContent()),
],
);
case DeviceType.tablet:
return const Column(
children: [
CompactAppBar(),
Expanded(child: PageContent()),
BottomNavigationBar(),
],
);
case DeviceType.mobile:
return const Scaffold(
body: PageContent(),
bottomNavigationBar: BottomNavigationBar(),
);
}
},
);
}
}
9. State Management Algorithm (Observer Pattern via ValueNotifier) #
File: lib/data/state_bridge.dart:1-35
Algorithm:
1. Interface: StateBridge<T> with update(), state, notifier
2. Implementation: SimpleStateBridge uses ValueNotifier<T>
3. On update(T newState): sets _notifier.value = newState
→ This triggers all registered listeners automatically
4. Consumers can listen via notifier.addListener()
5. Dispose pattern: dispose() removes all listeners
abstract class StateBridge<T> {
ValueNotifier<T> get notifier;
void update(T newState);
T get state;
}
class SimpleStateBridge<T> extends StateBridge<T> {
final ValueNotifier<T> _notifier;
SimpleStateBridge(T initialState) : _notifier = ValueNotifier<T>(initialState);
@override
ValueNotifier<T> get notifier => _notifier;
@override
T get state => _notifier.value;
@override
void update(T newState) {
_notifier.value = newState; // Triggers all listeners
}
}
Production state patterns:
// Pattern 1: Reactive UI
final cartCounter = SimpleStateBridge<int>(0);
// Widget rebuilds on change
ValueListenableBuilder<int>(
valueListenable: cartCounter.notifier,
builder: (ctx, count, _) => Text('Cart: $count items'),
);
// Pattern 2: Computed state
final firstName = SimpleStateBridge<String>('');
final lastName = SimpleStateBridge<String>('');
String get fullName => '${firstName.state} ${lastName.state}'.trim();
// Pattern 3: Async state
enum LoadingState { idle, loading, loaded, error }
final pageState = SimpleStateBridge<LoadingState>(LoadingState.idle);
Future<void> loadData() async {
pageState.update(LoadingState.loading);
try {
await fetchData();
pageState.update(LoadingState.loaded);
} catch (_) {
pageState.update(LoadingState.error);
}
}
10. SeoData Merge Algorithm (Null-Safe Field Override) #
File: lib/seo/seo_data.dart:28-40
Algorithm:
SeoData.merge(other):
For each field: other.field ?? this.field
→ Only overrides non-null fields from `other`
→ Preserves existing values when `other` has null
SeoData merge(SeoData other) => SeoData(
title: other.title ?? title,
description: other.description ?? description,
image: other.image ?? image,
ogType: other.ogType ?? ogType,
twitterCard: other.twitterCard ?? twitterCard,
keywords: other.keywords ?? keywords,
themeColor: other.themeColor ?? themeColor,
canonicalUrl: other.canonicalUrl ?? canonicalUrl,
locale: other.locale ?? locale,
noIndex: other.noIndex ?? noIndex,
structuredData: other.structuredData ?? structuredData,
);
11. Middleware/Guard Pipeline Algorithm (Sequential Async Execution) #
File: lib/router/smart_router.dart:280-333
Pipeline Algorithm:
1. Execute all preProcessors sequentially (async)
- Each receives (context, routeName)
- Used for analytics, logging, dynamic SEO
2. Execute all guards sequentially (async)
- Each returns bool
- If any returns false → block navigation, render nothing
- If all return true → proceed to page render
3. Render guarded widget
Future<void> _execute() async {
// Phase 1: Pre-processors (analytics, logging, SEO)
for (final pre in widget.preProcessors) {
if (!mounted) return;
await pre(context, widget.routeName);
}
// Phase 2: Guards (authentication, permissions)
for (final guard in widget.guards) {
if (!mounted) return;
final result = await guard(context, widget.routeName);
if (!result) {
setState(() => _allowed = false);
return; // Block navigation
}
}
// Phase 3: Render
if (mounted) setState(() => _loading = false);
}
Complete Production Example: E-Commerce App #
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sfwf/sfwf.dart';
// --- Providers ---
final themeProvider = SimpleStateBridge<ThemeMode>(ThemeMode.system);
final productsProvider = FutureProvider<List<Product>>((ref) async {
final raw = await OfflineProvider.fetchOrCache(
'https://api.store.com/products',
const Duration(minutes: 10),
);
final List<dynamic> jsonList = jsonDecode(raw);
return jsonList.map((e) => Product.fromJson(e)).toList();
});
// --- Entry Point ---
void main() {
runApp(const ProviderScope(child: StoreApp()));
}
class StoreApp extends ConsumerWidget {
const StoreApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeProvider);
return SFWFApp(
config: const SFWFConfig(
appName: 'My Store',
baseUrl: 'https://store.example.com',
seoDefaults: SeoDefaults(
titleSuffix: ' | My Store',
defaultDescription: 'Best online store for quality products.',
defaultImage: 'https://store.example.com/og.png',
twitterHandle: '@mystore',
),
ssrMode: SsrMode.hybrid,
enablePwa: true,
supportedLocales: [Locale('en'), Locale('ar')],
),
themeMode: themeMode,
routes: {
'/': (ctx) => const HomePage(),
'/products': (ctx) => const ProductsPage(),
'/products/:id': (ctx) {
final args = ModalRoute.of(ctx)?.settings.arguments;
final params = args is Map<String, String> ? args : {};
return ProductDetailPage(productId: params['id'] ?? '');
},
'/cart': (ctx) => const CartPage(),
'/checkout': (ctx) => const CheckoutPage(),
'/profile': (ctx) => const ProfilePage(),
},
routeDefinitions: [
const RouteDefinition(path: '/products/:id', name: 'product-detail'),
],
guards: [
RouteGuard(
guard: (context) {
// Protect /checkout and /profile
final route = ModalRoute.of(context)?.settings.name ?? '';
if (route.startsWith('/checkout') || route.startsWith('/profile')) {
return AuthService.isLoggedIn;
}
return true;
},
),
],
lifecycleHooks: [
LoggerHook(),
AnalyticsHook(),
],
notFoundBuilder: (ctx) => const NotFoundPage(),
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: Colors.indigo,
brightness: Brightness.light,
),
darkTheme: ThemeData(
useMaterial3: true,
colorSchemeSeed: Colors.indigo,
brightness: Brightness.dark,
),
);
}
}
// --- Product Detail with Full SEO ---
class ProductDetailPage extends ConsumerWidget {
final String productId;
const ProductDetailPage({required this.productId, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final productAsync = ref.watch(productsProvider);
return productAsync.when(
data: (products) {
final product = products.firstWhere(
(p) => p.id == productId,
orElse: () => Product.fallback(),
);
// SEO per page
SeoController.of(context).updatePage(SeoData(
title: product.name,
description: product.description.length > 160
? '${product.description.substring(0, 157)}...'
: product.description,
image: product.imageUrl,
ogType: 'product',
canonicalUrl: 'https://store.example.com/products/${product.id}',
structuredData: {
'@context': 'https://schema.org',
'@type': 'Product',
'name': product.name,
'description': product.description,
'image': product.imageUrl,
'sku': product.sku,
'brand': {'@type': 'Brand', 'name': product.brand},
'offers': {
'@type': 'Offer',
'url': 'https://store.example.com/products/${product.id}',
'priceCurrency': 'USD',
'price': product.price.toString(),
'availability': product.inStock
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
},
},
));
return Scaffold(
appBar: AppBar(title: Text(product.name)),
body: ProductDetailContent(product: product),
);
},
loading: () => const Scaffold(body: Center(child: CircularProgressIndicator())),
error: (e, _) => Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text('Failed to load product: $e'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => ref.invalidate(productsProvider),
child: const Text('Retry'),
),
],
),
),
),
);
}
}
CLI Reference #
# Create a new project
dart run sfwf create my_app
# Build with all optimizations
dart run sfwf build --prerender --optimize-images --generate-sw --analyze
# Start SSR server
dart run sfwf serve --port=8080
# Generate sitemap
dart run sfwf sitemap --base-url=https://example.com
# Analyze built site SEO
dart run sfwf analyze
# Show all commands
dart run sfwf --help
Configuration Reference #
SFWFConfig #
SFWFConfig(
appName: 'My App', // Required
baseUrl: 'https://example.com', // Required
seoDefaults: SeoDefaults(
titleSuffix: ' | My App',
defaultDescription: 'App description',
defaultImage: 'https://example.com/og.png',
twitterHandle: '@myapp',
),
ssrMode: SsrMode.hybrid, // off | ssrOnly | hybrid | prerenderOnly
enableAI: false,
enablePwa: true,
cacheDuration: Duration(minutes: 10),
supportedLocales: [Locale('en')],
)
SsrMode #
| Mode | Description |
|---|---|
SsrMode.off |
No SSR, client-side only |
SsrMode.ssrOnly |
Server renders all pages |
SsrMode.hybrid |
SSR + client hydration (recommended) |
SsrMode.prerenderOnly |
Static HTML generated at build time |
WASM Compatibility #
SFWF is fully WASM-compatible (scores 160/160 on pub.dev).
| Feature | IO (real) | WASM (stub) |
|---|---|---|
| Cache Manager | cache_manager_real.dart (Hive) |
cache_manager_stub.dart (in-memory) |
| Offline Provider | offline_provider.dart |
offline_provider_stub.dart |
| Image Optimizer | image_optimizer_io.dart |
image_optimizer_stub.dart |
| Service Worker | service_worker_io.dart |
service_worker_stub.dart |
| Sitemap | sitemap_generator.dart |
sitemap_stub.dart |
| Robots.txt | robots_generator.dart |
robots_stub.dart |
| SEO DOM | dom_operations_web.dart |
dom_operations_stub.dart |
| SSR Hydrator | ssr_hydrator_web.dart |
ssr_hydrator_stub.dart |
| SSR Renderer | ssr_renderer_puppeteer.dart |
ssr_renderer_stub.dart |
| Prerender | prerender_cli.dart |
prerender_stub.dart |
Project Structure #
lib/
├── core/
│ ├── config.dart # SFWFConfig, SeoDefaults, SsrMode
│ ├── sfwf_app.dart # Main SFWFApp widget
│ └── lifecycle.dart # App lifecycle hooks
├── router/
│ ├── smart_router.dart # Clean URL router with segmented matching
│ ├── route_definition.dart # Route definition class
│ ├── middleware.dart # RouteGuard & Middleware typedefs
│ └── guards.dart # AuthGuard implementation
├── seo/
│ ├── seo_controller.dart # Dynamic meta tags & JSON-LD
│ ├── seo_data.dart # SeoData with null-safe merge
│ ├── seo_provider.dart # InheritedWidget provider
│ ├── dom_operations.dart # DOM ops (conditional)
│ ├── dom_operations_stub.dart # No-op stub
│ ├── dom_operations_web.dart # Web DOM ops via package:web
│ ├── sitemap.dart # Sitemap (conditional)
│ ├── sitemap_generator.dart # Real sitemap generator
│ ├── sitemap_stub.dart # Sitemap stub
│ ├── robots.dart # Robots.txt (conditional)
│ ├── robots_generator.dart # Real robots.txt generator
│ └── robots_stub.dart # Robots.txt stub
├── ssr/
│ ├── ssr_renderer.dart # SSR renderer (conditional)
│ ├── ssr_renderer_base.dart # Abstract SSR renderer
│ ├── ssr_renderer_puppeteer.dart # Puppeteer SSR
│ ├── ssr_renderer_stub.dart # SSR stub
│ ├── ssr_client.dart # Client-side hydration
│ ├── ssr_hydrator.dart # Hydrator (conditional)
│ ├── ssr_hydrator_stub.dart # No-op hydrator stub
│ └── ssr_hydrator_web.dart # Web hydrator via package:web
├── ai/
│ ├── ai_analyzer.dart # OpenAI SEO analysis
│ └── ai_suggestions.dart # AI suggestion models
├── data/
│ ├── cache_manager.dart # TTL-based caching (conditional)
│ ├── cache_manager_real.dart # Hive-backed cache
│ ├── cache_manager_stub.dart # In-memory cache stub
│ ├── offline_provider.dart # Offline-first fetch (conditional)
│ ├── offline_provider_stub.dart# Offline stub
│ └── state_bridge.dart # ValueNotifier state management
├── device/
│ ├── device_detector.dart # Platform detection with caching
│ └── adaptive_builder.dart # Responsive layout builder
├── performance/
│ ├── lazy_loader.dart # Lazy loading component
│ ├── image_optimizer.dart # Image opt (conditional)
│ ├── image_optimizer_io.dart # Real image optimizer
│ ├── image_optimizer_stub.dart # Image opt stub
│ ├── service_worker.dart # SW (conditional)
│ ├── service_worker_io.dart # Real SW with SHA-256
│ └── service_worker_stub.dart # SW stub
├── plugins/
│ └── compatibility_layer.dart # PluginFallback
├── prerender/
│ ├── prerender.dart # Pre-render (conditional)
│ ├── prerender_cli.dart # Real pre-render CLI
│ └── prerender_stub.dart # Pre-render stub
├── sfwf.dart # Main export barrel
└── sfwf_web.dart # Web plugin registration
Troubleshooting #
Build fails with dart:io errors on WASM #
Use SFWF 2.0.6+ — all dart:io dependencies use conditional exports.
SEO meta tags not appearing #
- Call
SeoController.of(context).updatePage()inbuild()method - Ensure
SFWFAppwraps the widget tree - Verify
baseUrlis set inSFWFConfig
SSR not working #
- Install Puppeteer:
npx puppeteer browsers install chrome - Verify
ssrModeis notSsrMode.off - For pre-rendering, use static routes (no dynamic params)
Route navigation not working #
- Define routes in both
routesandrouteDefinitionsfor dynamic params - Use
Navigator.pushNamed(context, '/products/123')for direct paths - Use
Navigator.pushNamed(context, 'product-detail', arguments: {'id': '123'})for named routes
Cache not persisting #
- Call
CacheManager.instance.init()before first use - Ensure Hive is initialized (done automatically in
init())
Screenshots Gallery #
| Page | Screenshot |
|---|---|
| Home Page | ![]() |
| Projects | ![]() |
| Blog | ![]() |
| Contact | ![]() |
| Dark Mode | ![]() |
| SEO Meta Tags | ![]() |
| PWA Install Prompt | ![]() |
| SSR Pre-rendered | ![]() |
| Responsive Layout | ![]() |
| Image Optimization | ![]() |
| Example App | ![]() |
Demo Videos #
| Topic | Video |
|---|---|
| Full App Demo | |
| SEO Configuration |
Author #
Bahy Osama — Full-stack Flutter developer and creator of SFWF.
| Link | |
|---|---|
| Website | https://appsyntro.netlify.app/ |
| dev.bahy1@gmail.com | |
| https://www.linkedin.com/in/bahy-osama | |
| GitHub | https://github.com/BahyOsama |
| Portfolio | https://www.canva.com/design/DAFwCHH89oY/c6VgSlLvJ8Pp4f5AXdaWEQ/view |
License #
MIT










