flutter_ota_kit 0.1.6
flutter_ota_kit: ^0.1.6 copied to clipboard
Open-source, self-hosted code push (OTA updates) for Flutter Android. Patches Dart AOT libapp.so and assets on cold start with integrity checks and crash rollback.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter_ota_kit/flutter_ota_kit.dart';
import 'flutter_ota_kit_setup.dart';
import 'diag_card.dart';
import 'log_panel.dart';
const _demoImage = 'assets/patch_demo.png';
const _bundledAssetPatch = 'assets/asset_patch_preload.zip';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Wire the OTA backend. `setupFlutterOta()` is generated by
// `flutter-ota init`; it reads config from `.flutter_ota_kit/` (set up during
// init) and/or `--dart-define`/`.env` (environment wins). The bundled
// asset-patch demo works even without a backend configured.
await setupFlutterOta();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'flutter_ota_kit example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const Demo(),
);
}
class Demo extends StatefulWidget {
const Demo({super.key});
@override
State<Demo> createState() => _DemoState();
}
class _DemoState extends State<Demo> {
final _log = LogController();
Future<void> _applyBundledAssetPatch() async {
_log.log('loading bundled patch.zip...');
final bytes = (await rootBundle.load(
_bundledAssetPatch,
)).buffer.asUint8List();
final result = await FlutterPatcher.applyPatchBytes(
bytes,
version: 'asset-demo-1',
onProgress: (p) => _log.log(' [${p.phase.name}]'),
);
_log.log(
result.ok
? 'APPLIED: force-stop and reopen to see the image replacement'
: 'failed: ${result.error?.name} / ${result.message}',
);
DiagCard.refresh();
}
Future<void> _rollback() async {
await FlutterPatcher.rollback();
_log.log('ROLLED BACK: force-stop and reopen to restore the APK image');
DiagCard.refresh();
}
/// Manual alternative to the zero-click launch path: re-check immediately
/// (the backend is already configured in [main]) and apply the latest bundle.
Future<void> _checkWithSupabase() async {
try {
_log.log('Supabase: checking production channel (appVersion 1.0.0)...');
final apply = await FlutterPatcher.checkAndApplyUpdates(
onProgress: (p) => _log.log(' [${p.phase.name}]'),
);
if (apply == null) {
_log.log('Supabase: no update (or already on latest)');
} else if (apply.ok) {
_log.log('Supabase APPLIED: force-stop/reopen or auto-restart if forced');
} else {
_log.log('Supabase failed: ${apply.error?.name} / ${apply.message}');
}
DiagCard.refresh();
} catch (e) {
_log.log('Supabase error: $e');
}
}
void _snack(String label, Color color) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$label button from OTA code patch!',
style: TextStyle(color: color, fontWeight: FontWeight.bold)),
duration: const Duration(seconds: 2),
),
);
}
Widget _otaHeader() => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ShaderMask(
shaderCallback: (r) => const LinearGradient(
colors: [
Color(0xFFFF5252),
Color(0xFFFFC107),
Color(0xFF4CAF50),
Color(0xFF2979FF),
],
).createShader(r),
child: const Text(
'CODE PATCH ota-code-5 — NEW SCREEN ADDED!',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w900),
),
),
const SizedBox(height: 4),
const Center(
child: Chip(
label: Text('v2.0 · delivered by OTA',
style: TextStyle(fontWeight: FontWeight.bold)),
avatar: Icon(Icons.new_releases, size: 16),
),
),
const SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FilledButton(
onPressed: () => _snack('RED', Colors.red),
style: FilledButton.styleFrom(
backgroundColor: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 10),
),
child: const Text('RED'),
),
FilledButton(
onPressed: () => _snack('GREEN', Colors.green),
style: FilledButton.styleFrom(
backgroundColor: Colors.green,
padding: const EdgeInsets.symmetric(horizontal: 10),
),
child: const Text('GREEN'),
),
FilledButton(
onPressed: () => _snack('BLUE', Colors.blue),
style: FilledButton.styleFrom(
backgroundColor: Colors.blue,
padding: const EdgeInsets.symmetric(horizontal: 10),
),
child: const Text('BLUE'),
),
],
),
const SizedBox(height: 12),
],
);
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('flutter_ota_kit example')),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_otaHeader(),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 96,
height: 54,
child: Image.asset(_demoImage, fit: BoxFit.contain),
),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'asset key',
style: TextStyle(fontSize: 11, color: Colors.grey),
),
SizedBox(height: 2),
Text(
_demoImage,
style: TextStyle(fontFamily: 'monospace', fontSize: 12),
),
],
),
),
],
),
const SizedBox(height: 12),
const DiagCard(),
const SizedBox(height: 12),
FilledButton(
onPressed: _applyBundledAssetPatch,
child: const Text('Apply patch'),
),
const SizedBox(height: 8),
FilledButton.tonal(
onPressed: _checkWithSupabase,
child: const Text('Check via Supabase'),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const NewScreen()),
),
icon: const Icon(Icons.open_in_new),
label: const Text('Open New Screen (OTA)'),
),
const SizedBox(height: 8),
OutlinedButton(onPressed: _rollback, child: const Text('Rollback')),
const SizedBox(height: 16),
Expanded(child: LogPanel(controller: _log)),
],
),
),
),
);
}
/// New screen delivered entirely through the OTA code patch.
class NewScreen extends StatefulWidget {
const NewScreen({super.key});
@override
State<NewScreen> createState() => _NewScreenState();
}
class _NewScreenState extends State<NewScreen> {
int _counter = 0;
final _items = <String>[
'Added by OTA',
'Stateful counter',
'Text field below',
'Scrollable list',
];
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('New Screen (OTA)')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Card(
color: Theme.of(context).colorScheme.primaryContainer,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text('OTA counter', style: TextStyle(fontSize: 14)),
Text('$_counter',
style: const TextStyle(
fontSize: 40, fontWeight: FontWeight.bold)),
FilledButton.icon(
onPressed: () => setState(() => _counter++),
icon: const Icon(Icons.add),
label: const Text('Increment'),
),
],
),
),
),
const SizedBox(height: 12),
TextField(
controller: _controller,
decoration: const InputDecoration(
labelText: 'Type something',
border: OutlineInputBorder(),
),
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 8),
Text('You typed: "${_controller.text}"',
style: const TextStyle(fontStyle: FontStyle.italic)),
const SizedBox(height: 12),
Expanded(
child: ListView.separated(
itemCount: _items.length,
separatorBuilder: (_, _) => const Divider(),
itemBuilder: (_, i) => ListTile(
leading: const Icon(Icons.check_circle_outline),
title: Text(_items[i]),
),
),
),
],
),
),
);
}