patch_app 0.4.1
patch_app: ^0.4.1 copied to clipboard
Lightweight helper for Shorebird Code Push: detects and applies updates, then restarts the app with confirmation.
Patch App #
A lightweight helper to patch your Flutter app at runtime using shorebird_code_push.
It automatically checks for Shorebird updates, applies patches, and restarts your app safely when accepted. Restart behavior is provided by an internal Restart abstraction — by default the package uses terminate_restart on mobile and web, and restart_app on other desktop platforms, but you can inject a custom restart implementation (useful for tests).
Features #
- Check and apply Shorebird patches dynamically
- Show a customizable restart confirmation dialog
- Restart the app safely with one line of code
- Inject a custom
restartimplementation (handy for testing) - Register with
context:,navigatorKey:, orbinding:so startup checks can wait for the navigator to be ready - Optional
timeoutto stop deferred navigator/context retries after a max wait duration - Built-in
minIntervalto limit check frequency and prevent redundant checks - Optional error handling via callback, with
PatchResult.failedfor caught errors - Return
PatchResult.successwhen a patch is applied and the restart completed successfully (if a restart was performed) - Only report
PatchResult.restartRequiredwhen the restart dialog is accepted (or cannot be shown), while rejected prompts returnPatchResult.cancelled - Optional
onResultcallback invoked after eachcheckAndUpdatewith the activeBuildContextand the resultingPatchResult(useful to show follow-up UI like a manual-restart dialog).
Setup #
iOS #
Add the following to your Info.plist to enable restarts on iOS (used by the package's default restart implementation which relies on terminate_restart):
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
<key>CFBundleURLName</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</dict>
</array>
Android, Windows, MacOS, Linux and Web #
No configuration required.
More information about the setup #
- terminate_restart: https://pub.dev/packages/terminate_restart
- restart_app: https://pub.dev/packages/restart_app
When this package was first written, restart_app only supported restarting on iOS by reopening the app from a notification. At the time Shorebird supported only Android and iOS, so I chose terminate_restart because it worked reliably for my use cases. For patch_app v0.4.0 I aimed to support restarts across all platforms (Shorebird now supports them), and I re-evaluated restart_app. Its iOS implementation has improved but requires an additional native configuration step. To avoid forcing native changes on users, I kept both packages: terminate_restart for mobile and web, and restart_app for desktop.
Usage #
class App extends StatefulWidget {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
final patchApp = PatchApp(
confirmDialog: (context) => patchAppConfirmationDialog(context),
onError: (error, stack) => debugPrint('Update failed: $error'),
);
@override
Widget build(BuildContext context) {
return PatchAppScope(
patchApp: patchApp,
child: FilledButton(
onPressed: () => patchApp.checkAndUpdate(context), // Check and update manually
child: const Text('Check and Update'),
),
);
}
}
PatchAppScope #
- Wrap any subtree in
PatchAppScopeto register automatically for the wrapped widget. It callsregister()ininitState()andunregister()indispose()for you. - If you are wrapping an app root that needs to wait for a navigator, pass a
navigatorKeyto the scope. - If you need to control deferred registration scheduling in tests or custom bindings, pass a
bindingto the scope. - If you want deferred registration to stop after a maximum wait, pass
timeout:
PatchAppScope(
patchApp: patchApp,
navigatorKey: navigatorKey,
timeout: const Duration(seconds: 5),
child: const MyApp(),
)
register(context:) and unregister() #
-
register(context)Automatically checks for updates when the app starts or resumes. Should be called once ininitState(). If you need to wait for the navigator to become available, pass anavigatorKeyinstead:patchApp.register(navigatorKey: navigatorKey). To stop retrying deferred registration after a max duration, passtimeout:patchApp.register(navigatorKey: navigatorKey, timeout: const Duration(seconds: 5)). Thecontext:andnavigatorKey:arguments are named. -
unregister()Cleans up the lifecycle listener created byregister(). Always call this indispose().
Testing / Custom restart implementations #
The package exposes an internal Restart abstraction (with Future<void> initialize() and Future<bool> restart()), and the PatchApp constructor accepts an optional restart: argument. In normal apps you don't need to pass anything — the default implementation handles platform specifics. In tests you can pass a fake implementation to avoid performing real restarts:
final patchApp = PatchApp(
confirmDialog: (_) async => true,
// Optional: inject a test-friendly restart implementation
restart: MyFakeRestart(),
);
Your fake should implement two async methods: initialize() and restart().
The restart() method should return true when the restart succeeded, or false when it did not.
onResult callback #
PatchApp accepts an optional onResult callback with the signature Future<void> Function(BuildContext context, PatchResult result)? onResult. This is useful to present follow-up UI (for example, when the updater reports that a manual restart is required):
final patchApp = PatchApp(
confirmDialog: (context) => patchAppConfirmationDialog(context),
onResult: (context, result) async {
// The `context.mounted` should be checked before use
if (context.mounted && result == PatchResult.restartRequired) {
await patchAppConfirmationDialog(
context: context,
title: 'Manual Restart Required',
content: 'The app cannot restart automatically to apply the update.\n\n'
'Please restart the app manually to apply the latest updates.',
restartLabel: 'OK',
cancelLabel: 'CANCEL',
);
}
},
);
The callback runs only when context.mounted is true; otherwise it is
skipped to avoid showing dialogs on unmounted contexts.
Patch Results #
enum PatchResult {
noUpdate, // No updater or no patch available
throttled, // Check skipped because `minInterval` not reached
upToDate, // Already on the latest version
cancelled, // Restart prompt dismissed or skipped
restartRequired, // Patch applied; restart needed
failed, // Error during the update
success, // Patch applied and restart completed successfully (may not be returned if the process exits before the restart API returns)
}
throttled is returned when a call to checkAndUpdate is skipped because the
configured minInterval between checks has not yet elapsed. cancelled is
returned when the confirmation dialog is dismissed. success is returned when
an update was applied and the app was successfully restarted — note this may
not be returned if the process exits before the restart API can return. restartRequired is emitted when a restart is needed but could not be completed automatically (for example when the restart API returns false, or when the dialog cannot be shown because the context was unmounted), signaling that a manual restart is still needed.