once_new 0.0.1
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.
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')),
],
),
],
),
),
);
}
}