flutter_ota_kit 0.1.7 copy "flutter_ota_kit: ^0.1.7" to clipboard
flutter_ota_kit: ^0.1.7 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]),
                  ),
                ),
              ),
            ],
          ),
        ),
      );
}
0
likes
0
points
310
downloads

Documentation

Documentation

Publisher

unverified uploader

Weekly Downloads

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.

Repository (GitHub)
View/report issues

Topics

#flutter #android #code-push #hot-update #ota-update

License

unknown (license)

Dependencies

archive, args, crypto, flutter, flutter_ota_kit_aws, flutter_ota_kit_client, flutter_ota_kit_cloudflare, flutter_ota_kit_core, flutter_ota_kit_postgres, flutter_ota_kit_supabase, plugin_platform_interface

More

Packages that depend on flutter_ota_kit

Packages that implement flutter_ota_kit