DEV Community

Overpush
Overpush

Posted on

Show an "update ready" banner in your Flutter app (OTA updates)

When you ship over-the-air (OTA) updates to a Flutter app, the update downloads in the background and runs at the next launch. By default users see nothing, which is usually what you want.

But sometimes you do want to tell them, for example: "New update downloaded. Restart the app to apply it."

OverPush doesn't draw any UI itself. It sends events, and your app shows them in its own design. Here's a small banner you can drop into any app.

The events

OverPush.events is a stream. The ones you'll care about:

  • OverPushPatchFound: a new update exists
  • OverPushDownloading: download progress (percent, progress)
  • OverPushPatchReady: downloaded, will run at next launch
  • OverPushFailed: something went wrong; nothing changed on the device

The banner

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:overpush/overpush.dart';

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

  @override
  State<UpdateBanner> createState() => _UpdateBannerState();
}

class _UpdateBannerState extends State<UpdateBanner> {
  StreamSubscription<OverPushEvent>? _subscription;
  Timer? _hideTimer;
  String? _message;

  @override
  void initState() {
    super.initState();
    _subscription = OverPush.events.listen(_onEvent);
  }

  void _onEvent(OverPushEvent event) {
    _hideTimer?.cancel();
    switch (event) {
      case OverPushPatchFound():
        _show('New update found…');
      case OverPushDownloading(:final percent):
        _show(percent == null ? 'Downloading update…' : 'Downloading update · $percent%');
      case OverPushPatchReady():
        _show('Update downloaded. Restart the app to apply it.');
        _hideTimer = Timer(const Duration(seconds: 6), () => _show(null));
      case OverPushFailed():
        _show(null); // nothing changed; stay quiet
      default:
        break;
    }
  }

  void _show(String? message) {
    if (mounted) setState(() => _message = message);
  }

  @override
  void dispose() {
    _subscription?.cancel();
    _hideTimer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final message = _message;
    return Positioned(
      left: 16,
      right: 16,
      bottom: MediaQuery.paddingOf(context).bottom + 24,
      child: IgnorePointer(
        child: AnimatedSwitcher(
          duration: const Duration(milliseconds: 250),
          child: message == null
              ? const SizedBox.shrink()
              : Material(
                  key: const ValueKey('update-banner'),
                  color: const Color(0xFF1F2430),
                  borderRadius: BorderRadius.circular(12),
                  elevation: 6,
                  child: Padding(
                    padding: const EdgeInsets.all(14),
                    child: Text(
                      message,
                      style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600),
                    ),
                  ),
                ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Put it above your app

MaterialApp(
  builder: (context, child) => Stack(
    children: [child!, const UpdateBanner()],
  ),
  home: const HomePage(),
);
Enter fullscreen mode Exit fullscreen mode

That's it. It's your own widget, so restyle it however you like: a snackbar, a dialog with a "Restart now" button, or a small dot in the settings screen.

Try it

Top comments (0)