update_checker_plus 1.0.0
update_checker_plus: ^1.0.0 copied to clipboard
The most powerful, developer-friendly app update checker for Flutter. Supports Play Store, App Store, Firebase Remote Config, Supabase, Appwrite, force updates, maintenance mode, beautiful adaptive di [...]
import 'package:flutter/material.dart';
import 'package:update_checker_plus/update_checker_plus.dart';
void main() {
// ── Global configuration (call once before runApp) ──────────────────────
UpdateChecker.configure(
UpdateConfig(
// Replace with your actual package name / App Store ID.
androidPackageName: 'com.example.myapp',
iosAppId: '123456789',
debugMode: true,
// Check at most once per day.
checkInterval: const Duration(hours: 24),
),
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'UpdateCheckerPlus Demo',
theme: ThemeData(
colorSchemeSeed: Colors.deepPurple,
useMaterial3: true,
),
darkTheme: ThemeData(
colorSchemeSeed: Colors.deepPurple,
brightness: Brightness.dark,
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
UpdateResult? _lastResult;
bool _isChecking = false;
@override
void initState() {
super.initState();
// Auto-check on startup.
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkDefault();
});
}
// ── 1. Minimal one-liner ──────────────────────────────────────────────────
Future<void> _checkDefault() async {
setState(() => _isChecking = true);
final result = await UpdateChecker.check(context: context);
if (mounted) setState(() {
_lastResult = result;
_isChecking = false;
});
}
// ── 2. HTTP Remote Config ─────────────────────────────────────────────────
Future<void> _checkHttpRemoteConfig() async {
setState(() => _isChecking = true);
final result = await UpdateChecker.check(
context: context,
config: UpdateConfig(
remoteConfigSource: HttpRemoteConfigSource(
url: 'https://yourapi.com/app-update-config.json',
),
debugMode: true,
onUpdateAvailable: (info) async {
debugPrint('Update available: ${info.latestVersion}');
return true; // Show default UI.
},
onUpdateAccepted: (info) => debugPrint('User accepted update'),
onUpdateIgnored: (info) => debugPrint('User ignored update'),
),
);
if (mounted) setState(() {
_lastResult = result;
_isChecking = false;
});
}
// ── 3. Force update simulation ────────────────────────────────────────────
Future<void> _simulateForceUpdate() async {
setState(() => _isChecking = true);
// We build a mock UpdateInfo directly for demo purposes.
// In production, your remote config drives this automatically.
final info = UpdateInfo(
latestVersion: '9.9.9',
currentVersion: '1.0.0',
updateType: UpdateType.force,
releaseNotes: '• Critical security patch\n• Performance improvements\n• New dashboard',
storeUrl: 'https://play.google.com/store',
canDismiss: false,
);
await showUpdateDialog(
context: context,
info: info,
strings: const UpdateStrings(),
);
if (mounted) setState(() => _isChecking = false);
}
// ── 4. Soft update — bottom sheet style ───────────────────────────────────
Future<void> _simulateSoftUpdateSheet() async {
setState(() => _isChecking = true);
final info = UpdateInfo(
latestVersion: '2.5.0',
currentVersion: '2.3.1',
updateType: UpdateType.soft,
releaseNotes: '• Brand-new onboarding flow\n• Dark mode fixes\n• 40% faster startup',
storeUrl: 'https://play.google.com/store',
);
await showUpdateBottomSheet(
context: context,
info: info,
strings: const UpdateStrings(),
theme: UpdateTheme.branded(Theme.of(context).colorScheme.primary),
);
if (mounted) setState(() => _isChecking = false);
}
// ── 5. Maintenance mode ───────────────────────────────────────────────────
Future<void> _simulateMaintenance() async {
setState(() => _isChecking = true);
final info = UpdateInfo(
latestVersion: '2.0.0',
currentVersion: '2.0.0',
updateType: UpdateType.maintenance,
maintenanceMessage:
'We are performing scheduled maintenance.\nWe\'ll be back in ~30 minutes.',
canDismiss: false,
);
await showUpdateDialog(
context: context,
info: info,
strings: const UpdateStrings(),
);
if (mounted) setState(() => _isChecking = false);
}
// ── 6. Custom UI ──────────────────────────────────────────────────────────
Future<void> _checkWithCustomUi() async {
setState(() => _isChecking = true);
final result = await UpdateChecker.check(
context: context,
config: UpdateConfig(
androidPackageName: 'com.example.myapp',
customUiBuilder: UpdateUiBuilder(
builder: (ctx, info, onUpdate, onDismiss) {
return _CustomUpdateCard(
info: info,
onUpdate: onUpdate,
onDismiss: onDismiss,
);
},
),
),
);
if (mounted) setState(() {
_lastResult = result;
_isChecking = false;
});
}
// ── 7. Dark theme ─────────────────────────────────────────────────────────
Future<void> _simulateDarkTheme() async {
setState(() => _isChecking = true);
final info = UpdateInfo(
latestVersion: '3.0.0',
currentVersion: '2.8.0',
updateType: UpdateType.soft,
releaseNotes: '• Redesigned home screen\n• New widgets\n• Stability improvements',
storeUrl: 'https://apps.apple.com/app/id123',
);
await showUpdateDialog(
context: context,
info: info,
strings: const UpdateStrings(),
theme: UpdateTheme.dark(),
);
if (mounted) setState(() => _isChecking = false);
}
// ── 8. Silent check ───────────────────────────────────────────────────────
Future<void> _silentCheck() async {
setState(() => _isChecking = true);
final result = await UpdateChecker.checkSilently(
config: UpdateConfig(
androidPackageName: 'com.example.myapp',
debugMode: true,
),
);
if (mounted) setState(() {
_lastResult = result;
_isChecking = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('UpdateCheckerPlus Demo'),
centerTitle: true,
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// Status card
if (_lastResult != null)
_StatusCard(result: _lastResult!),
const SizedBox(height: 16),
_Section(
title: '🚀 Quick Start',
children: [
_DemoTile(
title: 'One-liner check (default)',
subtitle: 'UpdateChecker.check(context: context)',
onTap: _isChecking ? null : _checkDefault,
),
_DemoTile(
title: 'Silent check (no UI)',
subtitle: 'UpdateChecker.checkSilently()',
onTap: _isChecking ? null : _silentCheck,
),
],
),
_Section(
title: '🌐 Remote Config',
children: [
_DemoTile(
title: 'HTTP endpoint',
subtitle: 'HttpRemoteConfigSource',
onTap: _isChecking ? null : _checkHttpRemoteConfig,
),
],
),
_Section(
title: '🎨 UI Styles',
children: [
_DemoTile(
title: 'Force update dialog',
subtitle: 'Material 3, non-dismissible',
onTap: _isChecking ? null : _simulateForceUpdate,
),
_DemoTile(
title: 'Soft update — bottom sheet',
subtitle: 'Branded theme',
onTap: _isChecking ? null : _simulateSoftUpdateSheet,
),
_DemoTile(
title: 'Maintenance mode',
subtitle: 'Non-dismissible dialog',
onTap: _isChecking ? null : _simulateMaintenance,
),
_DemoTile(
title: 'Dark theme dialog',
subtitle: 'UpdateTheme.dark()',
onTap: _isChecking ? null : _simulateDarkTheme,
),
_DemoTile(
title: 'Custom UI builder',
subtitle: 'Your own widget',
onTap: _isChecking ? null : _checkWithCustomUi,
),
],
),
_Section(
title: '🔧 Utilities',
children: [
_DemoTile(
title: 'Clear skipped version',
subtitle: 'Resets "Don\'t ask again" state',
onTap: () async {
await UpdateChecker.clearSkippedVersion();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Skipped version cleared.')),
);
}
},
),
],
),
if (_isChecking)
const Padding(
padding: EdgeInsets.all(24),
child: Center(child: CircularProgressIndicator()),
),
],
),
);
}
}
// ---------------------------------------------------------------------------
// Supporting widgets
// ---------------------------------------------------------------------------
class _Section extends StatelessWidget {
final String title;
final List<Widget> children;
const _Section({required this.title, required this.children});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
Card(
clipBehavior: Clip.antiAlias,
child: Column(children: children),
),
const SizedBox(height: 16),
],
);
}
}
class _DemoTile extends StatelessWidget {
final String title;
final String subtitle;
final VoidCallback? onTap;
const _DemoTile({
required this.title,
required this.subtitle,
this.onTap,
});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(title),
subtitle: Text(
subtitle,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 11,
color: Theme.of(context).colorScheme.outline,
),
),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
);
}
}
class _StatusCard extends StatelessWidget {
final UpdateResult result;
const _StatusCard({required this.result});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final isError = result.status == UpdateStatus.error;
final color = isError ? cs.errorContainer : cs.tertiaryContainer;
final textColor = isError ? cs.onErrorContainer : cs.onTertiaryContainer;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Last check result',
style: TextStyle(
fontWeight: FontWeight.bold,
color: textColor,
),
),
const SizedBox(height: 4),
Text(
'Status: ${result.status.name}',
style: TextStyle(color: textColor, fontSize: 13),
),
if (result.updateInfo != null)
Text(
'${result.updateInfo!.currentVersion} → ${result.updateInfo!.latestVersion}',
style: TextStyle(
fontFamily: 'monospace',
color: textColor,
fontSize: 13,
),
),
if (result.error != null)
Text(
'${result.error}',
style: TextStyle(color: textColor, fontSize: 12),
),
],
),
);
}
}
/// A fully custom update card for the custom UI builder demo.
class _CustomUpdateCard extends StatelessWidget {
final UpdateInfo info;
final VoidCallback onUpdate;
final void Function(String?) onDismiss;
const _CustomUpdateCard({
required this.info,
required this.onUpdate,
required this.onDismiss,
});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [cs.primary, cs.tertiary],
),
shape: BoxShape.circle,
),
child: const Icon(Icons.rocket_launch_rounded,
color: Colors.white, size: 40),
),
const SizedBox(height: 20),
Text(
'✨ v${info.latestVersion} is here!',
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
info.releaseNotes ?? 'New features and bug fixes.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: onUpdate,
icon: const Icon(Icons.download_rounded),
label: const Text('Update Now'),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
shape: const StadiumBorder(),
),
),
TextButton(
onPressed: () => onDismiss(null),
child: const Text('Maybe later'),
),
],
),
),
);
}
}