canopy 0.1.0
canopy: ^0.1.0 copied to clipboard
Open-source code push for Flutter. Push Dart code updates to your apps instantly — without app store reviews. Self-hosted, no engine fork.
example/lib/main.dart
import 'package:canopy/canopy.dart';
import 'package:flutter/material.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// All config is read from canopy.yaml (app_id, server_url, public_key)
await CanopyUpdater.instance.initFromAsset();
// Check and apply updates automatically
final status = await CanopyUpdater.instance.checkForUpdate();
if (status == UpdateStatus.updateAvailable) {
await CanopyUpdater.instance.update();
}
CanopyUpdater.instance.reportBootSuccess();
runApp(const CanopyExampleApp());
}
class CanopyExampleApp extends StatelessWidget {
const CanopyExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Canopy Example',
theme: ThemeData(
colorSchemeSeed: Colors.green,
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
UpdateStatus? _status;
double _progress = 0;
String? _error;
bool _checking = false;
bool _updating = false;
@override
void initState() {
super.initState();
CanopyUpdater.instance.events.listen((event) {
if (!mounted) return;
setState(() {
switch (event) {
case UpdateCheckingEvent():
_checking = true;
_error = null;
case UpdateAvailableEvent(:final patchNumber, :final sizeBytes):
_checking = false;
_status = UpdateStatus.updateAvailable;
_showUpdateDialog(patchNumber, sizeBytes);
case UpdateDownloadingEvent(:final progress):
_progress = progress;
case UpdateAppliedEvent(:final patchNumber):
_updating = false;
_showSnackBar('Patch #$patchNumber staged. Restart to apply.');
case UpdateFailedEvent(:final error):
_checking = false;
_updating = false;
_error = error;
case UpdateUpToDateEvent():
_checking = false;
_status = UpdateStatus.upToDate;
}
});
});
}
Future<void> _checkForUpdate() async {
final status = await CanopyUpdater.instance.checkForUpdate();
if (status == UpdateStatus.upToDate && mounted) {
_showSnackBar('App is up to date!');
}
}
Future<void> _applyUpdate() async {
setState(() => _updating = true);
try {
await CanopyUpdater.instance.update(
onProgress: (p) => setState(() => _progress = p),
);
} catch (e) {
if (mounted) _showSnackBar('Update failed: $e');
}
}
void _showUpdateDialog(int patchNumber, int sizeBytes) {
final sizeMb = (sizeBytes / (1024 * 1024)).toStringAsFixed(1);
showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Update Available'),
content: Text('Patch #$patchNumber ($sizeMb MB)\nDownload and apply?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Later'),
),
FilledButton(
onPressed: () {
Navigator.pop(ctx);
_applyUpdate();
},
child: const Text('Update'),
),
],
),
);
}
void _showSnackBar(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
@override
Widget build(BuildContext context) {
final currentPatch = CanopyUpdater.instance.currentPatch;
return Scaffold(
appBar: AppBar(title: const Text('Canopy Example')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Current patch: ${currentPatch ?? "original release"}',
style: Theme.of(context).textTheme.titleMedium,
),
Text(
'Channel: ${CanopyUpdater.instance.currentChannel}',
style: Theme.of(context).textTheme.bodyMedium,
),
if (_status != null)
Text(
'Status: ${_status!.name}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 24),
if (_checking)
const Row(
children: [
SizedBox(
width: 16, height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(width: 12),
Text('Checking for updates...'),
],
),
if (_updating) ...[
Text('Downloading... ${(_progress * 100).toStringAsFixed(0)}%'),
const SizedBox(height: 8),
LinearProgressIndicator(value: _progress),
],
if (_error != null)
Text(
'Error: $_error',
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
const Spacer(),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _checking || _updating ? null : _checkForUpdate,
icon: const Icon(Icons.refresh),
label: const Text('Check for updates'),
),
),
],
),
),
);
}
}