flutter_ota_kit 0.1.15 copy "flutter_ota_kit: ^0.1.15" to clipboard
flutter_ota_kit: ^0.1.15 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_ota_kit/flutter_ota_kit.dart';
import 'chat_list_screen.dart';
import 'game_screen.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  FlutterPatcher.configureSupabase(
    SupabaseUpdateConfig(
      supabaseUrl: const String.fromEnvironment(
        'SUPABASE_URL',
        defaultValue: 'https://your-project.supabase.co',
      ),
      anonKey: const String.fromEnvironment(
        'SUPABASE_ANON_KEY',
        defaultValue: '',
      ),
      bucket: const String.fromEnvironment(
        'SUPABASE_BUCKET',
        defaultValue: 'bundles',
      ),
      channel: const String.fromEnvironment(
        'CHANNEL',
        defaultValue: 'production',
      ),
      platform: Platform.android,
      updateStrategy: UpdateStrategy.appVersion,
      appVersion: const String.fromEnvironment(
        'APP_VERSION',
        defaultValue: '1.0.0',
      ),
    ),
  );

  await FlutterPatcher.init(autoApplyUpdates: true);

  runApp(const FlutterOtaApp(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
    navigatorKey: FlutterPatcher.navigatorKey,
    title: 'Flutter OTA Kit',
    theme: ThemeData(
      colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      useMaterial3: true,
    ),
    home: const HomePage(),
  );
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  String _status = 'Ready';
  String _lastResult = '';

  Future<void> _checkForUpdate() async {
    setState(() {
      _status = 'Checking for update...';
      _lastResult = '';
    });

    try {
      final result = await FlutterPatcher.checkForUpdate();
      if (!mounted) return;

      if (result.hasUpdate && result.patch != null) {
        setState(() {
          _status = 'Update available';
          _lastResult =
              'Version: ${result.id}\n'
              'Force: ${result.shouldForceUpdate}\n'
              'Message: ${result.message ?? "(none)"}';
        });
      } else {
        setState(() {
          _status = 'Up to date';
          _lastResult = 'No update available.';
        });
      }
    } catch (e) {
      if (!mounted) return;
      setState(() {
        _status = 'Error';
        _lastResult = e.toString();
      });
    }
  }

  Future<void> _applyUpdate() async {
    setState(() {
      _status = 'Applying update...';
      _lastResult = '';
    });

    try {
      final result = await FlutterPatcher.checkForUpdate();
      if (!mounted) return;

      if (!result.hasUpdate || result.patch == null) {
        setState(() {
          _status = 'No update';
          _lastResult = 'Nothing to apply.';
        });
        return;
      }

      final applied = await FlutterPatcher.applyUpdate(
        result,
        onProgress: (p) {
          if (mounted) {
            setState(() => _status = 'Progress: ${p.phase.name}');
          }
        },
      );
      if (!mounted) return;

      setState(() {
        _status = applied.ok ? 'Applied!' : 'Failed';
        _lastResult =
            'ok: ${applied.ok}\n'
            'error: ${applied.error?.name}\n'
            'message: ${applied.message}';
      });
    } catch (e) {
      if (!mounted) return;
      setState(() {
        _status = 'Error';
        _lastResult = e.toString();
      });
    }
  }

  Future<void> _rollback() async {
    setState(() {
      _status = 'Rolling back...';
      _lastResult = '';
    });

    try {
      await FlutterPatcher.rollback();
      if (!mounted) return;
      setState(() {
        _status = 'Rolled back';
        _lastResult = 'Patch deleted. Restart to use built-in version.';
      });
    } catch (e) {
      if (!mounted) return;
      setState(() {
        _status = 'Error';
        _lastResult = e.toString();
      });
    }
  }

  Future<void> _showDiagnostics() async {
    final diag = await FlutterPatcher.lastBootDiagnostic;
    if (!mounted) return;

    final buf = StringBuffer();
    if (diag == null) {
      buf.writeln('No diagnostic recorded yet.');
    } else {
      buf.writeln('Status: ${diag.status.name}');
      if (diag.patchVersion != null) buf.writeln('Patch v: ${diag.patchVersion}');
      if (diag.patchTargetVersionCode != null) {
        buf.writeln('Patch vc: ${diag.patchTargetVersionCode}');
      }
      if (diag.appVersionCode != null) buf.writeln('App vc: ${diag.appVersionCode}');
      if (diag.crashCount != null) buf.writeln('Crashes: ${diag.crashCount}');
      if (diag.message != null) buf.writeln('Message: ${diag.message}');
    }

    setState(() {
      _status = 'Diagnostics';
      _lastResult = buf.toString();
    });
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(
      title: const Text('Flutter OTA Kit'),
      backgroundColor: Theme.of(context).colorScheme.inversePrimary,
    ),
    body: SafeArea(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Container(
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: Colors.deepPurple.withValues(alpha: 0.1),
                borderRadius: BorderRadius.circular(12),
                border: Border.all(color: Colors.deepPurple.withValues(alpha: 0.3)),
              ),
              child: const Text(
                'OTA v1.0.1 — Purple theme active',
                style: TextStyle(
                  fontSize: 14,
                  fontWeight: FontWeight.bold,
                  color: Colors.deepPurple,
                ),
                textAlign: TextAlign.center,
              ),
            ),
            const SizedBox(height: 12),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      _status,
                      style: const TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    if (_lastResult.isNotEmpty) ...[
                      const SizedBox(height: 8),
                      Text(
                        _lastResult,
                        style: const TextStyle(
                          fontFamily: 'monospace',
                          fontSize: 13,
                        ),
                      ),
                    ],
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),
            FilledButton(
              onPressed: _checkForUpdate,
              child: const Text('Check for Update'),
            ),
            const SizedBox(height: 8),
            FilledButton(
              onPressed: _applyUpdate,
              child: const Text('Apply Update'),
            ),
            const SizedBox(height: 8),
            OutlinedButton(
              onPressed: _rollback,
              child: const Text('Rollback'),
            ),
            const SizedBox(height: 8),
            OutlinedButton(
              onPressed: _showDiagnostics,
              child: const Text('Show Diagnostics'),
            ),
            const SizedBox(height: 24),
            const Divider(),
            const SizedBox(height: 8),
            Row(
              children: [
                Expanded(
                  child: FilledButton.icon(
                    onPressed: () {
                      Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (_) => const ChatListScreen(),
                        ),
                      );
                    },
                    icon: const Icon(Icons.chat_bubble_outline),
                    label: const Text('Chat'),
                    style: FilledButton.styleFrom(
                      backgroundColor: Colors.blue,
                      padding: const EdgeInsets.symmetric(vertical: 14),
                    ),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: FilledButton.icon(
                    onPressed: () {
                      Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (_) => const GameScreen(),
                        ),
                      );
                    },
                    icon: const Icon(Icons.videogame_asset),
                    label: const Text('Game'),
                    style: FilledButton.styleFrom(
                      backgroundColor: Colors.green,
                      padding: const EdgeInsets.symmetric(vertical: 14),
                    ),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    ),
  );
}