Skip to main content

once_new 0.0.1 copy "once_new: ^0.0.1" to clipboard
once_new: ^0.0.1 copied to clipboard

Run a block once, or at most once per hour / day / week / month / year / app version. The bookkeeping lives in SharedPreferences; there is nothing to initialize.

example/lib/main.dart

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

/// Stands in for whatever the app already knows about its own version —
/// `package_info_plus`, a generated constant, a build-time `--dart-define`.
/// Edit it and restart to watch the version gate fire once.
const appVersion = '1.0.0';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // Warm the records before the first frame so `Once.claimSync` is usable
  // inside `build` / `initState`.
  await Once.warmUp();
  runApp(const OnceDemoApp());
}

class OnceDemoApp extends StatelessWidget {
  const OnceDemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'once_new',
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      home: const DemoPage(),
    );
  }
}

class Demo {
  const Demo({
    required this.key,
    required this.title,
    required this.source,
    required this.policy,
  });

  final String key;
  final String title;
  final String source;
  final OncePolicy policy;
}

const demos = <Demo>[
  Demo(
    key: 'demo.once',
    title: 'Once, ever',
    source: 'OncePolicy.once()',
    policy: OncePolicy.once(),
  ),
  Demo(
    key: 'demo.every10s',
    title: 'Once per 10 seconds',
    source: 'OncePolicy.every(Duration(seconds: 10))',
    policy: OncePolicy.every(Duration(seconds: 10)),
  ),
  Demo(
    key: 'demo.daily',
    title: 'Once per calendar day',
    source: 'OncePolicy.everyDay()',
    policy: OncePolicy.everyDay(),
  ),
  Demo(
    key: 'demo.version',
    title: 'Once per app version',
    source: "OncePolicy.onChange('$appVersion')",
    policy: OncePolicy.onChange(appVersion),
  ),
];

class DemoPage extends StatefulWidget {
  const DemoPage({super.key});

  @override
  State<DemoPage> createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> {
  /// Result of the most recent claim per key. `null` means "not claimed since
  /// the record was cleared".
  final _claimed = <String, bool?>{};

  @override
  void initState() {
    super.initState();
    // The records are already in memory, so this decides in the first frame.
    for (final demo in demos) {
      _claimed[demo.key] = Once.claimSync(demo.key, demo.policy);
    }
  }

  void _claimAgain(Demo demo) {
    setState(() => _claimed[demo.key] = Once.claimSync(demo.key, demo.policy));
  }

  Future<void> _clear(Demo demo) async {
    await Once.clear(demo.key);
    if (!mounted) return;
    setState(() => _claimed[demo.key] = null);
  }

  Future<void> _clearAll() async {
    await Once.clearAll();
    if (!mounted) return;
    setState(() {
      for (final demo in demos) {
        _claimed[demo.key] = null;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('once_new')),
      body: ListView(
        padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
        children: [
          Text(
            'Every gate below was claimed once when this page opened. '
            'Restart the app to see which ones survive a cold start.',
            style: Theme.of(context).textTheme.bodyMedium,
          ),
          const SizedBox(height: 16),
          for (final demo in demos)
            _DemoCard(
              demo: demo,
              claimed: _claimed[demo.key],
              onClaimAgain: () => _claimAgain(demo),
              onClear: () => _clear(demo),
            ),
          const SizedBox(height: 8),
          OutlinedButton.icon(
            onPressed: _clearAll,
            icon: const Icon(Icons.delete_sweep_outlined),
            label: const Text('Clear all records'),
          ),
        ],
      ),
    );
  }
}

class _DemoCard extends StatelessWidget {
  const _DemoCard({
    required this.demo,
    required this.claimed,
    required this.onClaimAgain,
    required this.onClear,
  });

  final Demo demo;
  final bool? claimed;
  final VoidCallback onClaimAgain;
  final VoidCallback onClear;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final (label, color) = switch (claimed) {
      true => ('fired', theme.colorScheme.primary),
      false => ('skipped', theme.colorScheme.outline),
      null => ('cleared', theme.colorScheme.outline),
    };

    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: Padding(
        padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Expanded(
                  child: Text(demo.title, style: theme.textTheme.titleMedium),
                ),
                Container(
                  padding:
                      const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
                  decoration: BoxDecoration(
                    color: color.withValues(alpha: 0.12),
                    borderRadius: BorderRadius.circular(999),
                  ),
                  child: Text(
                    label,
                    style: theme.textTheme.labelMedium?.copyWith(color: color),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 4),
            Text(
              demo.source,
              style: theme.textTheme.bodySmall?.copyWith(
                fontFamily: 'monospace',
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
            // Asking without recording, so the row can say what the next claim
            // would answer.
            Text(
              Once.isDueSync(demo.key, demo.policy) ? 'due now' : 'not due yet',
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
            Row(
              children: [
                TextButton(
                    onPressed: onClaimAgain, child: const Text('Claim again')),
                TextButton(onPressed: onClear, child: const Text('Clear')),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
0
likes
160
points
258
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Run a block once, or at most once per hour / day / week / month / year / app version. The bookkeeping lives in SharedPreferences; there is nothing to initialize.

Repository (GitHub)
View/report issues

Topics

#once #throttle #scheduling #onboarding #shared-preferences

License

MIT (license)

Dependencies

flutter, shared_preferences

More

Packages that depend on once_new