patch_app 0.3.0
patch_app: ^0.3.0 copied to clipboard
A lightweight helper to patch your Flutter app at runtime using shorebird code push and terminate restart
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, with
PatchResult.failedfor caught errors - 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>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 #
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
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
}
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, 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, it runs beforePatchResult.failedis returned. - If omitted, the error is still converted into
PatchResult.failed.