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), ), ), ), ), ), ); } } 

Put it above your app

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

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