canopy 0.1.0
canopy: ^0.1.0 copied to clipboard
Open-source code push for Flutter. Push Dart code updates to your apps instantly — without app store reviews. Self-hosted, no engine fork.
Canopy #
Open-source code push for Flutter. Push Dart code and asset updates to your apps instantly — without app store reviews.
No engine fork required. Works with the standard Flutter engine.
Features #
- OTA Updates — Push code changes to your Flutter apps in seconds
- Android Support — Patches applied via snapshot replacement, no engine fork needed
- Crash Protection — Automatic rollback if a patch causes boot failures
- Ed25519 Signing — Verify patch integrity with cryptographic signatures
- Staged Rollouts — Roll out patches to a percentage of users
- Self-Hosted — Run the Canopy server on your own infrastructure
Getting Started #
1. Install the SDK #
dependencies:
canopy: ^0.1.0
2. Android Setup #
a) Create a custom Application class in android/app/src/main/kotlin/.../CanopyApplication.kt:
package com.example.yourapp
import android.app.Application
import android.content.Context
import android.util.Log
import dev.canopy.sdk.CanopyFlutterLoader
import dev.canopy.sdk.SnapshotManager
import io.flutter.FlutterInjector
class CanopyApplication : Application() {
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
try {
val snapshotManager = SnapshotManager.getInstance(base)
val snapshotPath = snapshotManager.getActiveSnapshotPath()
FlutterInjector.setInstance(
FlutterInjector.Builder()
.setFlutterLoader(CanopyFlutterLoader(snapshotPath))
.build()
)
} catch (e: Exception) {
Log.e("CanopyApp", "Init failed: ${e.message}")
}
}
}
b) Register it in android/app/src/main/AndroidManifest.xml:
<application
android:name=".CanopyApplication"
android:usesCleartextTraffic="true"
...>
c) Update MainActivity.kt:
import dev.canopy.sdk.CanopyFlutterActivity
class MainActivity : CanopyFlutterActivity()
3. Initialize Canopy #
Add canopy.yaml to your Flutter assets in pubspec.yaml:
flutter:
assets:
- canopy.yaml
Then initialize in main.dart — all config is read from canopy.yaml automatically:
import 'package:canopy/canopy.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Reads app_id, server_url, public_key from canopy.yaml
await CanopyUpdater.instance.initFromAsset();
// Check and apply updates
final status = await CanopyUpdater.instance.checkForUpdate();
if (status == UpdateStatus.updateAvailable) {
await CanopyUpdater.instance.update();
// Patch will be applied on next cold restart
}
// Report successful boot (enables crash protection)
CanopyUpdater.instance.reportBootSuccess();
runApp(const MyApp());
}
You can override the server URL for local development:
await CanopyUpdater.instance.initFromAsset(
serverUrl: 'http://192.168.1.4:8080',
);
4. Push Updates #
# Create your first release
canopy release --platform android
# Make code changes, then push a patch
canopy patch --platform android --release 1.0.0
5. Listen to Events (optional) #
CanopyUpdater.instance.events.listen((event) {
switch (event) {
case UpdateAvailableEvent(:final patchNumber, :final sizeBytes):
print('Patch #$patchNumber available ($sizeBytes bytes)');
case UpdateAppliedEvent(:final patchNumber):
print('Patch #$patchNumber staged — restart to activate');
case UpdateFailedEvent(:final error):
print('Update failed: $error');
default:
break;
}
});
How It Works #
- You build and upload a release with the Canopy CLI (
canopy release) - You make code changes and push a patch (
canopy patch) - The SDK checks for updates on app launch
- If a patch is available, it downloads and verifies it (SHA-256 + Ed25519)
- On next cold restart, the patched
libapp.sois loaded instead of the bundled one - If the patch causes crashes, automatic rollback kicks in after 2 failed boots
Crash Protection #
Canopy tracks boot success. If the app crashes twice in a row after applying a patch, the SDK automatically rolls back to the original bundled version. Call reportBootSuccess() after your app initializes successfully to reset the crash counter.
API Reference #
CanopyUpdater #
| Method | Description |
|---|---|
initFromAsset({serverUrl?, channel?}) |
Initialize from bundled canopy.yaml |
init(serverUrl, appId, ...) |
Initialize with explicit parameters |
checkForUpdate({channel?}) |
Check if a patch is available |
update({channel?, onProgress?}) |
Download and stage a patch |
reportBootSuccess() |
Mark current boot as successful |
currentPatch |
Current patch number (null = original) |
currentChannel |
Current update channel |
events |
Stream of update events |
dispose() |
Clean up resources |
UpdateStatus #
| Value | Description |
|---|---|
upToDate |
No update available |
updateAvailable |
A new patch is ready to download |
error |
Failed to check for updates |
Requirements #
- Flutter >= 3.19.0
- Dart >= 3.0.0
- Android: minSdk 24 (Android 7.0+)
Links #
License #
Apache License 2.0