web_update_guard 0.1.0 copy "web_update_guard: ^0.1.0" to clipboard
web_update_guard: ^0.1.0 copied to clipboard

Stop Flutter web users running stale cached builds. A CLI stamps and cache-busts build/web; a runtime guard detects new deploys and reloads cleanly.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:web_update_guard/web_update_guard.dart';

/// Demo of web_update_guard.
///
/// 1. `flutter build web`
/// 2. `dart run web_update_guard stamp build/web`
/// 3. Serve build/web, open it, then rebuild + restamp: the running tab
///    detects the new deploy within the poll interval (or on refocus).
void main() => runApp(const DemoApp());

/// Root widget: owns the [WebUpdateGuard] and recreates it when the settings
/// change.
class DemoApp extends StatefulWidget {
  /// Creates the demo.
  const DemoApp({super.key});

  @override
  State<DemoApp> createState() => _DemoAppState();
}

class _DemoAppState extends State<DemoApp> {
  // `?autoReload=whenIdle` (or `immediately`) in the URL preselects a policy.
  AutoReloadPolicy _autoReload = AutoReloadPolicy.values.firstWhere(
    (AutoReloadPolicy p) => p.name == Uri.base.queryParameters['autoReload'],
    orElse: () => AutoReloadPolicy.never,
  );
  ComparisonPolicy _comparison = ComparisonPolicy.anyDifference;
  UpdatePresentation _presentation = UpdatePresentation.snackBar;
  bool _keepAppCaches = false;
  late WebUpdateGuard _guard;

  @override
  void initState() {
    super.initState();
    _guard = _createGuard();
  }

  WebUpdateGuard _createGuard() {
    final WebUpdateGuard guard = WebUpdateGuard(
      pollInterval: const Duration(seconds: 30),
      minCheckGap: const Duration(seconds: 5),
      autoReload: _autoReload,
      idleTimeout: const Duration(seconds: 20),
      comparison: _comparison,
      applyOptions: ApplyUpdateOptions(
        // Keep caches whose name starts with "app-" when asked to.
        cacheNameFilter: _keepAppCaches
            ? (String name) => !name.startsWith('app-')
            : null,
      ),
    );
    // Logged so the behaviour can be followed in the browser console.
    guard.statusStream.listen(
      (UpdateStatus s) => debugPrint('[web_update_guard] $s'),
    );
    debugPrint('[web_update_guard] initial ${guard.status}');
    return guard..start();
  }

  void _reconfigure(VoidCallback change) {
    setState(() {
      change();
      _guard.dispose();
      _guard = _createGuard();
    });
  }

  @override
  void dispose() {
    _guard.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'web_update_guard demo',
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      // WebUpdateListener sits below the MaterialApp's ScaffoldMessenger.
      builder: (BuildContext context, Widget? child) => WebUpdateListener(
        key: ValueKey<Object>(_guard),
        checker: _guard,
        presentation: _presentation,
        child: child!,
      ),
      home: DemoHome(
        guard: _guard,
        autoReload: _autoReload,
        comparison: _comparison,
        presentation: _presentation,
        keepAppCaches: _keepAppCaches,
        onAutoReload: (AutoReloadPolicy p) =>
            _reconfigure(() => _autoReload = p),
        onComparison: (ComparisonPolicy p) =>
            _reconfigure(() => _comparison = p),
        onPresentation: (UpdatePresentation p) =>
            setState(() => _presentation = p),
        onKeepAppCaches: (bool v) => _reconfigure(() => _keepAppCaches = v),
      ),
    );
  }
}

/// The demo page: an inline [UpdateBanner], live status and settings.
class DemoHome extends StatelessWidget {
  /// Creates the page.
  const DemoHome({
    super.key,
    required this.guard,
    required this.autoReload,
    required this.comparison,
    required this.presentation,
    required this.keepAppCaches,
    required this.onAutoReload,
    required this.onComparison,
    required this.onPresentation,
    required this.onKeepAppCaches,
  });

  /// The active guard.
  final WebUpdateGuard guard;

  /// Current auto-reload policy.
  final AutoReloadPolicy autoReload;

  /// Current comparison policy.
  final ComparisonPolicy comparison;

  /// Current listener presentation.
  final UpdatePresentation presentation;

  /// Whether caches named `app-*` survive [WebUpdateGuard.applyUpdate].
  final bool keepAppCaches;

  /// Changes the auto-reload policy.
  final ValueChanged<AutoReloadPolicy> onAutoReload;

  /// Changes the comparison policy.
  final ValueChanged<ComparisonPolicy> onComparison;

  /// Changes the listener presentation.
  final ValueChanged<UpdatePresentation> onPresentation;

  /// Toggles keeping `app-*` caches.
  final ValueChanged<bool> onKeepAppCaches;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('web_update_guard')),
      body: UpdateBanner(
        checker: guard,
        message: 'Inline UpdateBanner: a new build is deployed.',
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: <Widget>[
            StreamBuilder<UpdateStatus>(
              stream: guard.statusStream,
              initialData: guard.status,
              builder: (BuildContext context, AsyncSnapshot<UpdateStatus> s) =>
                  _StatusCard(status: s.data!),
            ),
            const SizedBox(height: 12),
            Wrap(
              spacing: 12,
              runSpacing: 12,
              children: <Widget>[
                FilledButton.icon(
                  onPressed: guard.checkNow,
                  icon: const Icon(Icons.refresh),
                  label: const Text('Check now'),
                ),
                OutlinedButton.icon(
                  onPressed: guard.isSupported ? guard.applyUpdate : null,
                  icon: const Icon(Icons.cleaning_services_outlined),
                  label: const Text('Purge caches & reload'),
                ),
              ],
            ),
            const SizedBox(height: 24),
            Text('Settings', style: Theme.of(context).textTheme.titleMedium),
            const SizedBox(height: 8),
            _Choice<AutoReloadPolicy>(
              label: 'Auto-reload',
              value: autoReload,
              values: AutoReloadPolicy.values,
              onChanged: onAutoReload,
            ),
            _Choice<ComparisonPolicy>(
              label: 'Comparison',
              value: comparison,
              values: ComparisonPolicy.values,
              onChanged: onComparison,
            ),
            _Choice<UpdatePresentation>(
              label: 'Listener style',
              value: presentation,
              values: UpdatePresentation.values,
              onChanged: onPresentation,
            ),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              title: const Text('Keep caches named "app-*" when purging'),
              value: keepAppCaches,
              onChanged: onKeepAppCaches,
            ),
          ],
        ),
      ),
    );
  }
}

class _StatusCard extends StatelessWidget {
  const _StatusCard({required this.status});

  final UpdateStatus status;

  @override
  Widget build(BuildContext context) {
    final BuildInfo? latest = status.latest;
    final List<(String, String)> rows = <(String, String)>[
      ('State', status.state.name + (status.checking ? ' (checking…)' : '')),
      ('Running build', status.runningBuildId ?? '—'),
      ('Server build', latest?.buildId ?? '—'),
      ('Server app version', latest?.appVersion ?? '—'),
      ('Server built at', latest?.builtAt?.toIso8601String() ?? '—'),
      ('Last check', status.checkedAt?.toIso8601String() ?? '—'),
      if (status.error != null) ('Last error', '${status.error}'),
    ];
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            for (final (String k, String v) in rows)
              Padding(
                padding: const EdgeInsets.symmetric(vertical: 2),
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    SizedBox(width: 150, child: Text(k)),
                    Expanded(child: SelectableText(v)),
                  ],
                ),
              ),
            if (status.state == UpdateState.unsupported)
              const Padding(
                padding: EdgeInsets.only(top: 8),
                child: Text(
                  'Not running on the web: the guard is a no-op here.',
                ),
              ),
            if (status.state == UpdateState.notStamped)
              const Padding(
                padding: EdgeInsets.only(top: 8),
                child: Text(
                  'index.html has no build-ID meta tag. Run '
                  '`dart run web_update_guard stamp build/web`.',
                ),
              ),
          ],
        ),
      ),
    );
  }
}

class _Choice<T extends Enum> extends StatelessWidget {
  const _Choice({
    required this.label,
    required this.value,
    required this.values,
    required this.onChanged,
  });

  final String label;
  final T value;
  final List<T> values;
  final ValueChanged<T> onChanged;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 6),
      child: Row(
        children: <Widget>[
          SizedBox(width: 120, child: Text(label)),
          Expanded(
            child: SegmentedButton<T>(
              segments: <ButtonSegment<T>>[
                for (final T v in values)
                  ButtonSegment<T>(value: v, label: Text(v.name)),
              ],
              selected: <T>{value},
              onSelectionChanged: (Set<T> s) => onChanged(s.single),
            ),
          ),
        ],
      ),
    );
  }
}
0
likes
160
points
0
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Stop Flutter web users running stale cached builds. A CLI stamps and cache-busts build/web; a runtime guard detects new deploys and reloads cleanly.

Repository (GitHub)
View/report issues

Topics

#web #cache #deployment #service-worker #update

License

MIT (license)

Dependencies

args, crypto, flutter, meta, path, web, yaml

More

Packages that depend on web_update_guard