Patch App
A lightweight helper to patch your Flutter app at runtime using shorebird_code_push and terminate_restart.
It automatically checks for Shorebird updates, applies patches, and restarts your app safely when accepted.
Features
- Check and apply Shorebird patches dynamically
- Show a customizable restart confirmation dialog
- Restart the app safely with one line of code
- 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 or
PatchResult - Only report
PatchResult.restartRequiredwhen the restart dialog is accepted (or cannot be shown), while rejected prompts returnPatchResult.cancelled
Setup
iOS
Add the following to your Info.plist to enable restarts with terminate_restart:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleURLSchemes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
Android
No configuration required.
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().
Patch Results
enum PatchResult {
noUpdate, // No updater or no patch available
upToDate, // Already on the latest version
cancelled, // Restart prompt dismissed or skipped
restartRequired, // Patch applied; restart needed
failed, // Error during the update
}
cancelled is returned when the confirmation dialog is dismissed, while restartRequired is only emitted after the user accepts a restart (or if the dialog cannot be shown because the context was unmounted), signaling that a restart is still needed.
Tips
-
Always provide an
onErrorcallback in production to capture unexpected failures.- If
onErroris provided, the method returnsPatchResult.failedon error. - If omitted, the error is rethrown.
- If