sfwf 2.0.9 copy "sfwf: ^2.0.9" to clipboard
sfwf: ^2.0.9 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 #

pub package Flutter Dart License: MIT 160/160


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
Home Projects Blog Contact Dark

Demo Video #

Video Link
Full App Demo SFWF Demo
SEO Configuration SEO

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.9
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:

  1. Split both the pattern and URL by /
  2. Compare segment-by-segment
  3. If pattern segment starts with :, capture as a parameter
  4. If segments differ and no : prefix, reject match
  5. 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() in build() method
  • Ensure SFWFApp wraps the widget tree
  • Verify baseUrl is set in SFWFConfig

SSR not working #

  • Install Puppeteer: npx puppeteer browsers install chrome
  • Verify ssrMode is not SsrMode.off
  • For pre-rendering, use static routes (no dynamic params)

Route navigation not working #

  • Define routes in both routes and routeDefinitions for 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())

Page Screenshot
Home Page Home
Projects Projects
Blog Blog
Contact Contact
Dark Mode Dark
SEO Meta Tags SEO
PWA Install Prompt PWA
SSR Pre-rendered SSR
Responsive Layout Responsive
Image Optimization Images
Example App Example

Demo Videos #

Topic Video
Full App Demo Demo
SEO Configuration SEO

Author #

Bahy Osama — Full-stack Flutter developer and creator of SFWF.

Link
Website https://appsyntro.netlify.app/
Email dev.bahy1@gmail.com
LinkedIn https://www.linkedin.com/in/bahy-osama
GitHub https://github.com/BahyOsama
Portfolio https://www.canva.com/design/DAFwCHH89oY/c6VgSlLvJ8Pp4f5AXdaWEQ/view

License #

MIT

6
likes
160
points
532
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

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.

Homepage
Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

args, crypto, flutter, flutter_riverpod, flutter_web_plugins, hive, hive_flutter, http, image, meta, path, puppeteer, riverpod, shelf, shelf_router, web, xml

More

Packages that depend on sfwf

Packages that implement sfwf