dart_patch_updater 0.1.0
dart_patch_updater: ^0.1.0 copied to clipboard
In-app updates for Flutter without the App Store. Update business logic, feature flags, and configs from your own server or GitHub Releases. Includes SHA-256 checksum + RSA signature verification with [...]
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:dart_patch_updater/dart_patch_updater.dart';
/// Example app demonstrating dart_patch_updater usage
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize update manager
final updateManager = UpdateManager(
config: const UpdateConfig(
serverUrl: 'https://updates.example.com',
appId: 'com.example.myapp',
appVersion: '2.1.0',
// Your RSA public key for signature verification
publicKey: '''
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
''',
checkOnLaunch: true,
autoDownload: false,
autoApply: false,
maxBackupVersions: 3,
),
);
// Initialize and load existing patches
await updateManager.initialize();
runApp(MyApp(updateManager: updateManager));
}
class MyApp extends StatelessWidget {
final UpdateManager updateManager;
const MyApp({super.key, required this.updateManager});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Patch Updater Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: HomePage(updateManager: updateManager),
);
}
}
class HomePage extends StatefulWidget {
final UpdateManager updateManager;
const HomePage({super.key, required this.updateManager});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
UpdateState _state = UpdateState.initial();
String _statusMessage = 'Ready';
@override
void initState() {
super.initState();
// Listen to state changes
widget.updateManager.stateStream.listen((state) {
setState(() => _state = state);
});
}
Future<void> _checkForUpdates() async {
setState(() => _statusMessage = 'Checking for updates...');
final result = await widget.updateManager.checkForUpdates();
if (result.hasError) {
setState(() => _statusMessage = 'Error: ${result.error}');
} else if (result.updateAvailable) {
setState(
() => _statusMessage = 'Update available: ${result.patch?.version}',
);
_showUpdateDialog(result.patch);
} else {
setState(() => _statusMessage = 'No updates available');
}
}
Future<void> _downloadAndApply() async {
setState(() => _statusMessage = 'Downloading...');
final result = await widget.updateManager.downloadAndApply(
onProgress: (progress) {
setState(() => _statusMessage = progress.progressString);
},
);
if (result.success) {
setState(() => _statusMessage = 'Updated to ${result.newVersion}');
if (result.requiresRestart) {
_showRestartDialog();
}
} else {
setState(() {
_statusMessage = result.rolledBack
? 'Update failed, rolled back: ${result.error}'
: 'Update failed: ${result.error}';
});
}
}
void _showUpdateDialog(dynamic patch) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Update Available'),
content: Text(
'Version ${patch?.version} is available.\n\n'
'${patch?.releaseNotes ?? "No release notes"}',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Later'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_downloadAndApply();
},
child: const Text('Update Now'),
),
],
),
);
}
void _showRestartDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Restart Required'),
content: const Text(
'The update has been applied. Please restart the app to use the new features.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Later'),
),
ElevatedButton(
onPressed: () => widget.updateManager.restartApp(),
child: const Text('Restart Now'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Patch Updater Demo'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Status card
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Status',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
'Current Patch: ${_state.currentPatchVersion ?? "None"}',
),
Text('Status: ${_state.status.name}'),
Text(_statusMessage),
if (_state.downloadProgress != null)
LinearProgressIndicator(value: _state.downloadProgress),
],
),
),
),
const SizedBox(height: 16),
// Action buttons
ElevatedButton.icon(
onPressed: _state.isBusy ? null : _checkForUpdates,
icon: const Icon(Icons.refresh),
label: const Text('Check for Updates'),
),
const SizedBox(height: 8),
if (_state.hasUpdate)
ElevatedButton.icon(
onPressed: _state.isBusy ? null : _downloadAndApply,
icon: const Icon(Icons.download),
label: const Text('Download & Apply'),
),
const Spacer(),
// Business logic demo
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Patchable Business Logic Demo',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
_buildFeatureFlagsDemo(),
const SizedBox(height: 8),
_buildPricingDemo(),
],
),
),
),
],
),
),
);
}
Widget _buildFeatureFlagsDemo() {
// Read feature flags from patched modules
final isDarkModeEnabled =
widget.updateManager.getModuleData('feature_flags')?['dark_mode'] ??
false;
final isPremiumEnabled = widget.updateManager.getModuleData(
'feature_flags',
)?['premium_features'] ??
false;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Feature Flags:'),
Text(' • Dark Mode: ${isDarkModeEnabled ? "ON" : "OFF"}'),
Text(' • Premium Features: ${isPremiumEnabled ? "ON" : "OFF"}'),
],
);
}
Widget _buildPricingDemo() {
// Read pricing rules from patched modules
final pricingRules = widget.updateManager.getModuleData('pricing_rules');
final discount = pricingRules?['default_discount'] ?? 0;
final taxRate = pricingRules?['tax_rate'] ?? 0.1;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Pricing Rules:'),
Text(' • Default Discount: ${(discount * 100).toStringAsFixed(0)}%'),
Text(' • Tax Rate: ${(taxRate * 100).toStringAsFixed(0)}%'),
],
);
}
}