Show updates in your app

OverPush shows nothing on screen. If you want users to see an update arrive, your app listens to OverPush.events and shows it in its own design. This page has a complete banner you can copy.

The banner below sits at the bottom of every screen and appears only while something is happening:

  1. New update found…
  2. Downloading update · 42%, with a progress bar
  3. New update downloaded. Restart the app to apply it. It hides after a few seconds.

It works the same on Android and iOS. Prefer to show nothing? Skip this page; updates still download and apply on their own.

Before you start

1. Add the banner widget

Create a new file in your app:

import 'dart:async';

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

/// A small banner at the bottom of the screen that shows an OverPush update:
/// "New update found", the download percent, then "Restart the app to apply
/// it". Hidden the rest of the time.
///
/// Put it in a Stack above your app (see main.dart). Restyle it freely: it is
/// your app's code, OverPush only sends the events.
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;
  double? _progress; // 0..1, or null for an indeterminate bar

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

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

  void _show(String? message, double? progress) {
    if (!mounted) return;
    setState(() {
      _message = message;
      _progress = progress;
    });
  }

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

  @override
  Widget build(BuildContext context) {
    final String? message = _message;
    return Positioned(
      left: 16,
      right: 16,
      // Above the keyboard when it is open, so it is never hidden behind it.
      bottom:
          MediaQuery.viewInsetsOf(context).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<String>('update-banner'),
                  color: const Color(0xFF1F2430),
                  elevation: 6,
                  borderRadius: BorderRadius.circular(12),
                  child: Padding(
                    padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      children: [
                        Text(
                          message,
                          style: const TextStyle(
                            color: Colors.white,
                            fontSize: 13,
                            fontWeight: FontWeight.w600,
                          ),
                        ),
                        const SizedBox(height: 8),
                        ClipRRect(
                          borderRadius: BorderRadius.circular(4),
                          child: LinearProgressIndicator(
                            value: _progress,
                            minHeight: 4,
                            backgroundColor: Colors.white24,
                            color: _progress == 1
                                ? const Color(0xFF3CCF7A)
                                : Theme.of(context).colorScheme.primary,
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
        ),
      ),
    );
  }
}

2. Put it above your app

Add the banner to your app widget's builder, so it shows over every screen. Pick the app widget you use:

import 'update_banner.dart';

// ...inside your app widget's build():
return MaterialApp(
  builder: (context, child) => Stack(
    children: [
      if (child != null) child,
      const UpdateBanner(),
    ],
  ),
  home: const HomeScreen(),
);
import 'update_banner.dart';

// ...inside your app widget's build():
return MaterialApp.router(
  routerConfig: router,
  builder: (context, child) => Stack(
    children: [
      if (child != null) child,
      const UpdateBanner(),
    ],
  ),
);
import 'update_banner.dart';

// ...inside your app widget's build():
return GetMaterialApp(
  builder: (context, child) => Stack(
    children: [
      if (child != null) child,
      const UpdateBanner(),
    ],
  ),
  home: const HomeScreen(),
);

Already using builder, for example for a theme or text scale? Keep what you have and put it inside the Stack in place of child.

You don't call OverPush.init or change main(). OverPush starts by itself in a release build; the banner only listens.

3. Ship it and try it

  1. Build a release with the banner in it and install it on your phone: overpush release androidoverpush release ios --artifact app, then preview it.
  2. Change some Dart code and publish a patch: overpush patch androidoverpush patch ios.
  3. Close the app fully and open it. A few seconds in, the banner shows the download, then the restart message.
  4. Close and open the app again. The patch is running.

Make it yours

All events

EventWhenFields
OverPushCheckingA check with the server started–
OverPushPatchFoundA new patch exists and is about to downloadpatchNumber
OverPushDownloadingWhile downloading, once per percentpercent (0–100 or null), progress (0–1 or null), receivedBytes, totalBytes, patchNumber
OverPushPatchReadyDownloaded and verified; runs from the next app startpatchNumber
OverPushUpToDateNothing new–
OverPushRevokedThe running patch was pulled; the next start runs the store build–
OverPushFailedNo network, or the patch didn't verify. Nothing changed on the deviceerror

percent is null only when the download size isn't known; the banner above then shows a moving bar instead of a number.

What's next

Update strategiesWhen the app checks, and checking on your own schedule. Automatic rollbackWhat happens if a patch breaks startup.