flutter_ota_kit 0.1.11 copy "flutter_ota_kit: ^0.1.11" to clipboard
flutter_ota_kit: ^0.1.11 copied to clipboard

PlatformAndroid

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'
    show FlutterOtaApp, FlutterPatcher;

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();
  // `FlutterOtaApp` hosts the SDK's forced-update progress overlay (no UI code
  // needed here); `autoApplyUpdates: true` in setupFlutterOta() applies + shows
  // it automatically for `shouldForceUpdate` bundles.
  runApp(const FlutterOtaApp(child: 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]),
              ),
            ),
          ),
        ],
      ),
    ),
  );
}