app_lifecycle_executor 0.0.3
app_lifecycle_executor: ^0.0.3 copied to clipboard
A Flutter package for executing lifecycle callbacks on app install and version upgrades across all platforms.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:app_lifecycle_executor/app_lifecycle_executor.dart';
// Version class is exported by app_lifecycle_executor, no need to import separately
// Example: Using enum for action IDs
enum ActionId {
showV2Banner,
initNewFeature,
showNotification,
manualTrigger;
String get value => name;
}
// Example: Custom implementation of OneTimeAction
class ShowBannerAction implements OneTimeAction {
@override
String get id => ActionId.showV2Banner.value;
@override
Future<void> callback() async {
print('π Showing banner for v2.0.0 features (from custom class)');
await Future.delayed(Duration(milliseconds: 100));
}
}
// Example: Another custom implementation
class InitFeatureAction implements OneTimeAction {
@override
String get id => ActionId.initNewFeature.value;
@override
Future<void> callback() async {
print('π Initializing new feature (from custom class)');
await Future.delayed(Duration(milliseconds: 100));
}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Option 1: Register callbacks before init
AppLifecycleExecutor.onFirstInstall(() async {
print('π App installed for the first time!');
// Example: Initialize default settings
// Example: Show onboarding screens
// Example: Create initial database
await Future.delayed(Duration(milliseconds: 100));
print('β
First install setup completed');
});
AppLifecycleExecutor.onVersionChanged((oldVersion, newVersion) async {
print('π App upgraded from $oldVersion to $newVersion');
// Example: Handle version-specific migrations using Version comparison
if (oldVersion < Version(2, 0, 0) && newVersion >= Version(2, 0, 0)) {
print('Migrating to major version 2.x...');
await migrateDatabase();
}
// Example: Check specific version changes
if (oldVersion.major == 1 && newVersion.major == 2) {
print('Major version upgrade from 1.x to 2.x');
}
// Example: Show "What's New" dialog
print('β
Migration completed');
});
AppLifecycleExecutor.onError((error, stackTrace) {
print('β Error during lifecycle callback: $error');
// In production, you might want to log this to your error tracking service
});
// Register one-time actions that should run once per version upgrade
AppLifecycleExecutor.runOnce([
// Using custom implementations with enum-based IDs
ShowBannerAction(),
InitFeatureAction(),
// Using SimpleOneTimeAction for inline actions
SimpleOneTimeAction(
id: ActionId.showNotification.value,
callback: () async {
print('π’ Showing notification for v3.0.0 major update');
await Future.delayed(Duration(milliseconds: 100));
},
),
]);
// Initialize the app lifecycle executor
await AppLifecycleExecutor.init();
// Option 2: Pass callbacks directly to init (overrides registered callbacks)
// await AppLifecycleExecutor.init(
// firstInstallCallback: () async {
// print('π App installed for the first time!');
// },
// onVersionChanged: (oldVersion, newVersion) async {
// print('π App upgraded from $oldVersion to $newVersion');
// },
// onError: (error, stackTrace) {
// print('β Error during lifecycle callback: $error');
// },
// );
runApp(const MyApp());
}
Future<void> migrateDatabase() async {
// Simulate database migration
await Future.delayed(Duration(milliseconds: 200));
print('Database migration completed');
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App Lifecycle Executor Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
String _currentVersion = 'Loading...';
String? _lastVersion = 'Loading...';
bool _firstInstallExecuted = false;
List<String> _executedActionIds = [];
@override
void initState() {
super.initState();
_loadLifecycleInfo();
}
Future<void> _loadLifecycleInfo() async {
final currentVersion = await AppLifecycleExecutor.getCurrentVersion();
final lastVersion = await AppLifecycleExecutor.getLastVersion();
final firstInstallExecuted =
await AppLifecycleExecutor.hasFirstInstallExecuted();
final executedActionIds = await AppLifecycleExecutor.getExecutedActionIds();
setState(() {
_currentVersion = currentVersion;
_lastVersion = lastVersion ?? 'None (first install)';
_firstInstallExecuted = firstInstallExecuted;
_executedActionIds = executedActionIds;
});
}
Future<void> _resetLifecycle() async {
await AppLifecycleExecutor.reset();
await _loadLifecycleInfo();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Lifecycle state reset!')),
);
}
}
Future<void> _triggerOneTimeAction() async {
final wasExecuted = await AppLifecycleExecutor.runOnceNow(
SimpleOneTimeAction(
id: ActionId.manualTrigger.value,
callback: () async {
print('π― Manual one-time action triggered!');
await Future.delayed(Duration(milliseconds: 100));
},
),
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
wasExecuted
? 'β
Action executed successfully!'
: 'βΉοΈ Action already executed (skipped)',
),
backgroundColor: wasExecuted ? Colors.green : Colors.orange,
),
);
}
// Refresh the UI to show updated executed actions
await _loadLifecycleInfo();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('App Lifecycle Executor Demo'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Lifecycle Information',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 24),
_InfoRow(
label: 'Current Version:',
value: _currentVersion,
),
const SizedBox(height: 12),
_InfoRow(
label: 'Last Stored Version:',
value: _lastVersion ?? 'None',
),
const SizedBox(height: 12),
_InfoRow(
label: 'First Install Executed:',
value: _firstInstallExecuted ? 'Yes' : 'No',
),
const SizedBox(height: 12),
_InfoRow(
label: 'Is First Install (sync):',
value: AppLifecycleExecutor.isFirstInstall ? 'Yes' : 'No',
highlight: AppLifecycleExecutor.isFirstInstall,
),
const SizedBox(height: 12),
_InfoRow(
label: 'Is Initialized:',
value: AppLifecycleExecutor.isInitialized ? 'Yes' : 'No',
),
const SizedBox(height: 24),
const Text(
'One-Time Actions',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
_InfoRow(
label: 'Executed Actions:',
value: _executedActionIds.isEmpty
? 'None'
: _executedActionIds.join(', '),
),
const SizedBox(height: 24),
// Example: Conditional widget based on first install status
if (AppLifecycleExecutor.isFirstInstall)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: Row(
children: [
Icon(Icons.celebration, color: Colors.blue.shade700),
const SizedBox(width: 12),
Expanded(
child: Text(
'Welcome! This is your first time using the app.',
style: TextStyle(
color: Colors.blue.shade900,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
const SizedBox(height: 32),
Center(
child: ElevatedButton.icon(
onPressed: _triggerOneTimeAction,
icon: const Icon(Icons.play_arrow),
label: const Text('Run One-Time Action'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
),
const SizedBox(height: 16),
Center(
child: ElevatedButton(
onPressed: _resetLifecycle,
child: const Text('Reset Lifecycle State (for testing)'),
),
),
const SizedBox(height: 16),
const Center(
child: Text(
'After resetting, restart the app to trigger\nfirst install callback again.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12, color: Colors.grey),
),
),
],
),
),
),
);
}
}
class _InfoRow extends StatelessWidget {
final String label;
final String value;
final bool highlight;
const _InfoRow({
required this.label,
required this.value,
this.highlight = false,
});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 180,
child: Text(
label,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 16,
color: highlight ? Colors.blue.shade700 : null,
fontWeight: highlight ? FontWeight.bold : null,
),
),
),
],
);
}
}