@_nazmiforreal/flutter-ota 0.1.27 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -18
- package/bin/flutter-ota-linux-x64 +0 -0
- package/dart-src/packages/cli-tools/README.md +146 -2
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +36 -8
- package/dart-src/packages/cli-tools/pubspec.yaml +1 -1
- package/dart-src/root/README.md +193 -253
- package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatchManager.kt +24 -5
- package/dart-src/root/lib/src/ota_progress_overlay.dart +176 -96
- package/dart-src/root/pubspec.yaml +1 -1
- package/package.json +1 -1
package/dart-src/root/README.md
CHANGED
|
@@ -1,48 +1,73 @@
|
|
|
1
1
|
# flutter_ota_kit
|
|
2
2
|
|
|
3
|
-
**English** | [简体中文](README-zh.md)
|
|
4
|
-
|
|
5
3
|
[](https://pub.dev/packages/flutter_ota_kit)
|
|
6
4
|
[](https://flutter.dev)
|
|
7
5
|
[](LICENSE)
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
### Ship Dart & asset hotfixes over the air. Your backend. No store review. No monthly bill.
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
**flutter_ota_kit** is open-source code push for **Flutter Android**. Push a bug
|
|
10
|
+
fix or a new screen, and your users get it on the next cold start — no Play
|
|
11
|
+
Store round-trip, no vendor lock-in, no per-seat pricing. The patch lives on
|
|
12
|
+
**your** infrastructure (Supabase, Postgres, Cloudflare, AWS S3, PocketBase, or
|
|
13
|
+
your own CDN), signed and integrity-checked, with automatic crash rollback if
|
|
14
|
+
anything goes wrong.
|
|
15
15
|
|
|
16
16
|
If you've used [Shorebird](https://shorebird.dev/),
|
|
17
17
|
[CodePush](https://learn.microsoft.com/en-us/appcenter/distribution/codepush/),
|
|
18
|
-
or [Expo EAS Update](https://docs.expo.dev/eas-update/introduction/)
|
|
19
|
-
|
|
20
|
-
with your choice of cloud backend.
|
|
18
|
+
or [Expo EAS Update](https://docs.expo.dev/eas-update/introduction/), this is the
|
|
19
|
+
same idea for Flutter Android — MIT-licensed, self-hosted, and yours to keep.
|
|
21
20
|
|
|
22
21
|

|
|
23
22
|
|
|
23
|
+
```yaml
|
|
24
|
+
dependencies:
|
|
25
|
+
flutter_ota_kit: ^0.2.0 # one package — every backend included
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
> **v0.2.0 is a single package.** Everything — the device SDK plus all five
|
|
29
|
+
> backends — now ships inside `flutter_ota_kit`. There are no more
|
|
30
|
+
> `flutter_ota_kit_supabase` / `_postgres` / `_cloudflare` / `_aws` /
|
|
31
|
+
> `_pocketbase` packages to add. One import, one dependency.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Why teams pick it
|
|
36
|
+
|
|
37
|
+
- **You own the pipeline.** Patches sit in your Supabase project, your S3
|
|
38
|
+
bucket, your Cloudflare account — not someone else's cloud. No third party
|
|
39
|
+
sees your code or your users.
|
|
40
|
+
- **Zero recurring cost.** MIT-licensed. The only bill is your own storage,
|
|
41
|
+
which for OTA payloads is pennies.
|
|
42
|
+
- **Ships in one dependency.** Add `flutter_ota_kit`, run `flutter-ota init`,
|
|
43
|
+
deploy. Five backends are built in; you configure the one you use.
|
|
44
|
+
- **Safe by default.** MD5 + Ed25519 verification, automatic crash rollback,
|
|
45
|
+
a bad-patch blacklist, and staged rollouts — all on by default.
|
|
46
|
+
- **Zero-code forced-update UI.** Opt in and the SDK renders a polished
|
|
47
|
+
full-screen install progress screen for you. Write no UI.
|
|
48
|
+
|
|
24
49
|
---
|
|
25
50
|
|
|
26
51
|
## How it compares
|
|
27
52
|
|
|
28
|
-
|
|
|
29
|
-
|
|
30
|
-
| **Framework**
|
|
31
|
-
| **Platforms**
|
|
32
|
-
| **Hosting**
|
|
33
|
-
| **Update scope**
|
|
34
|
-
| **Takes effect**
|
|
35
|
-
| **Cost**
|
|
36
|
-
| **Backend flexibility**
|
|
37
|
-
| **Forced updates**
|
|
38
|
-
| **Crash rollback**
|
|
39
|
-
| **Signing**
|
|
40
|
-
|
|
41
|
-
**Choose Shorebird** if you need iOS
|
|
42
|
-
|
|
43
|
-
**Choose flutter_ota_kit** if you
|
|
53
|
+
| | flutter_ota_kit | Shorebird | CodePush (React Native) |
|
|
54
|
+
|--------------------------|----------------------------------------------------------|-------------------------------|------------------------------|
|
|
55
|
+
| **Framework** | Flutter | Flutter | React Native |
|
|
56
|
+
| **Platforms** | Android | Android + iOS | Android + iOS (retired 2025) |
|
|
57
|
+
| **Hosting** | Your backend / your CDN — see [Backends](doc/backends.md) | Shorebird cloud (managed) | AppCenter cloud (deprecated) |
|
|
58
|
+
| **Update scope** | Dart AOT + Flutter assets | Dart code (engine-level diff) | JS bundle |
|
|
59
|
+
| **Takes effect** | Next cold start | Next restart | Next restart |
|
|
60
|
+
| **Cost** | Free (MIT) — you pay only your own storage | Free tier + paid plans | — |
|
|
61
|
+
| **Backend flexibility** | Supabase / Postgres / Cloudflare / AWS / PocketBase / BYO CDN | Cloud-managed only | — |
|
|
62
|
+
| **Forced updates** | Yes — built-in progress UI | Yes | Yes |
|
|
63
|
+
| **Crash rollback** | Automatic + bad-patch blacklist | Yes | Yes |
|
|
64
|
+
| **Signing** | Ed25519 (Android 13+) + MD5 | Yes | — |
|
|
65
|
+
|
|
66
|
+
**Choose Shorebird** if you need iOS today or want a fully managed cloud.
|
|
67
|
+
|
|
68
|
+
**Choose flutter_ota_kit** if you want OTA on infrastructure you control —
|
|
44
69
|
enterprise apps, regional distribution, air-gapped deployments, or non-Play
|
|
45
|
-
|
|
70
|
+
channels. Bring your own backend, CDN, and auth.
|
|
46
71
|
|
|
47
72
|
> Google Play and some stores restrict downloading executable code at runtime.
|
|
48
73
|
> flutter_ota_kit targets self-controlled, enterprise, or permissive
|
|
@@ -52,23 +77,24 @@ distribution channels. Bring your own backend, your own CDN, your own auth.
|
|
|
52
77
|
|
|
53
78
|
## Features
|
|
54
79
|
|
|
55
|
-
- **OTA code push** —
|
|
56
|
-
next cold start, with MD5 + Ed25519 integrity checks.
|
|
57
|
-
- **Five
|
|
58
|
-
|
|
59
|
-
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
`
|
|
63
|
-
|
|
64
|
-
- **
|
|
80
|
+
- **OTA code push** — replaces the Dart AOT `libapp.so` and Flutter assets on
|
|
81
|
+
the next cold start, with MD5 + Ed25519 integrity checks.
|
|
82
|
+
- **Five backends, built in** — Supabase (fully automated), Postgres,
|
|
83
|
+
Cloudflare (D1 + R2), AWS (S3 + optional CloudFront), and **PocketBase**
|
|
84
|
+
(single-binary, self-hosted). Or point at your own CDN.
|
|
85
|
+
- **Automatic crash rollback** — a patch that fails to boot is rolled back and
|
|
86
|
+
blacklisted so it's never retried. Tunable via `maxCrashCount` /
|
|
87
|
+
`verifyAfter`. A runtime crash *hours* after a healthy boot no longer reverts
|
|
88
|
+
a good patch (bounded crash-attribution window).
|
|
89
|
+
- **Forced updates with a built-in UI** — set `autoApplyUpdates: true` and wrap
|
|
90
|
+
your app in `FlutterOtaApp`; the SDK shows a full-screen terminal-style
|
|
91
|
+
install screen (steps, live progress bar, speed/ETA, server message) and
|
|
92
|
+
cold-restarts into the new build. You write no UI.
|
|
93
|
+
- **Staged rollout** — server-side cohort math (per-mille + target cohorts) lets
|
|
65
94
|
you ship 1% → 5% → 20% → 50% → 100% safely.
|
|
66
|
-
- **
|
|
67
|
-
`
|
|
68
|
-
runtime diagnostics
|
|
69
|
-
- **No native plugin churn** — the patch system uses the Android
|
|
70
|
-
`ContentProvider` to stage files; no platform channel gymnastics, no
|
|
71
|
-
per-vendor SDKs to upgrade.
|
|
95
|
+
- **Batteries-included tooling** — the `flutter-ota` CLI (`init`, `build`,
|
|
96
|
+
`deploy`, `bundle`, `channel`, `rollback`, `migrate`, `doctor`, and a bundled
|
|
97
|
+
PocketBase manager), plus runtime diagnostics and a sample app.
|
|
72
98
|
|
|
73
99
|
---
|
|
74
100
|
|
|
@@ -76,44 +102,40 @@ distribution channels. Bring your own backend, your own CDN, your own auth.
|
|
|
76
102
|
|
|
77
103
|
```bash
|
|
78
104
|
git clone https://github.com/HYPER12755/flutter-ota-kit.git
|
|
79
|
-
cd
|
|
105
|
+
cd flutter-ota-kit/example
|
|
80
106
|
flutter pub get
|
|
81
107
|
flutter build apk --release
|
|
82
108
|
flutter install
|
|
83
109
|
```
|
|
84
110
|
|
|
85
|
-
1. Launch the app — it shows the **original**
|
|
111
|
+
1. Launch the app — it shows the **original** demo screen.
|
|
86
112
|
2. Tap **Apply patch**.
|
|
87
113
|
3. Swipe the app away from recents and reopen.
|
|
88
|
-
4. The
|
|
89
|
-
5. Tap **Rollback** → restart → original
|
|
114
|
+
4. The screen has changed — the patch took effect.
|
|
115
|
+
5. Tap **Rollback** → restart → the original is back.
|
|
90
116
|
|
|
91
|
-
The example bundles a precompiled
|
|
92
|
-
|
|
93
|
-
[Getting Started](doc/getting-started.md)
|
|
94
|
-
[Beginner Guide](doc/beginner-guide.md) (the human-narrated walkthrough).
|
|
117
|
+
The example bundles a precompiled patch; everything runs offline on the device.
|
|
118
|
+
For a full backend walkthrough, see the [Beginner Guide](doc/beginner-guide.md)
|
|
119
|
+
or [Getting Started](doc/getting-started.md).
|
|
95
120
|
|
|
96
121
|
---
|
|
97
122
|
|
|
98
123
|
## Requirements
|
|
99
124
|
|
|
100
|
-
| Item
|
|
101
|
-
|
|
102
|
-
| **Platform**
|
|
103
|
-
| **Dart SDK**
|
|
104
|
-
| **Flutter**
|
|
105
|
-
| **Android `minSdk`**
|
|
106
|
-
| **Android `compileSdk`**
|
|
107
|
-
| **ABI**
|
|
108
|
-
| **NDK**
|
|
109
|
-
| **
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
**no-op** — the plugin logs a one-time warning and returns safe defaults.
|
|
115
|
-
This makes it safe to ship `flutter_ota_kit` in cross-platform code without
|
|
116
|
-
guarding every call site.
|
|
125
|
+
| Item | Requirement |
|
|
126
|
+
|--------------------------|----------------------------------------------------------|
|
|
127
|
+
| **Platform** | Android only (iOS / macOS / Windows / Linux / Web are no-ops) |
|
|
128
|
+
| **Dart SDK** | `>=3.13.2 <4.0.0` |
|
|
129
|
+
| **Flutter** | `>=3.47.2` |
|
|
130
|
+
| **Android `minSdk`** | 24 |
|
|
131
|
+
| **Android `compileSdk`** | 36 |
|
|
132
|
+
| **ABI** | `armeabi-v7a` / `arm64-v8a` / `x86_64` |
|
|
133
|
+
| **NDK** | 27.0.12077973+ |
|
|
134
|
+
| **Java / JVM** | 17 |
|
|
135
|
+
|
|
136
|
+
On non-Android platforms every API is safe to call but is a **no-op** — the
|
|
137
|
+
plugin logs a one-time warning and returns safe defaults, so you can ship
|
|
138
|
+
`flutter_ota_kit` in cross-platform code without guarding every call site.
|
|
117
139
|
|
|
118
140
|
---
|
|
119
141
|
|
|
@@ -122,130 +144,119 @@ guarding every call site.
|
|
|
122
144
|
### 1. Install
|
|
123
145
|
|
|
124
146
|
```yaml
|
|
125
|
-
# pubspec.yaml
|
|
126
147
|
dependencies:
|
|
127
|
-
flutter_ota_kit: ^0.
|
|
148
|
+
flutter_ota_kit: ^0.2.0
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 2. Scaffold your backend (CLI)
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# installs the flutter-ota CLI globally
|
|
155
|
+
npm i -g @_nazmiforreal/flutter-ota
|
|
156
|
+
|
|
157
|
+
# scaffold config + a generated setup file for your backend
|
|
158
|
+
flutter-ota init supabase # or: postgres | cloudflare | aws | pocketbase
|
|
128
159
|
```
|
|
129
160
|
|
|
130
|
-
|
|
161
|
+
`init` writes a `.flutter_ota_kit/config.json`, an `.env` template for secrets,
|
|
162
|
+
and a `lib/flutter_ota_kit_setup.dart` helper you call from `main()`.
|
|
131
163
|
|
|
132
|
-
|
|
164
|
+
### 3. Initialize in your app
|
|
165
|
+
|
|
166
|
+
Call the generated `setupFlutterOta()` (or wire it by hand) before `runApp`:
|
|
133
167
|
|
|
134
168
|
```dart
|
|
135
169
|
import 'package:flutter/material.dart';
|
|
136
170
|
import 'package:flutter_ota_kit/flutter_ota_kit.dart';
|
|
137
|
-
import 'flutter_ota_kit_setup.dart';
|
|
171
|
+
import 'flutter_ota_kit_setup.dart'; // generated by `flutter-ota init`
|
|
138
172
|
|
|
139
173
|
Future<void> main() async {
|
|
140
174
|
WidgetsFlutterBinding.ensureInitialized();
|
|
141
|
-
await setupFlutterOta();
|
|
175
|
+
await setupFlutterOta(); // configures the backend + forced updates
|
|
142
176
|
runApp(const FlutterOtaApp(child: MyApp()));
|
|
143
177
|
}
|
|
144
178
|
```
|
|
145
179
|
|
|
146
|
-
`FlutterOtaApp` is the zero-code wrapper that
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
Disable the overlay app-wide with `FlutterPatcher.showUpdateUi = false`
|
|
152
|
-
(or `FlutterOtaApp(showUpdateUi: false)`), or host it yourself by
|
|
153
|
-
assigning `MaterialApp.navigatorKey = FlutterPatcher.navigatorKey`.
|
|
180
|
+
`FlutterOtaApp` is the zero-code wrapper that lets the SDK render its built-in
|
|
181
|
+
forced-update screen. Disable it app-wide with
|
|
182
|
+
`FlutterPatcher.showUpdateUi = false` (or `FlutterOtaApp(showUpdateUi: false)`),
|
|
183
|
+
or host it yourself by setting `MaterialApp.navigatorKey =
|
|
184
|
+
FlutterPatcher.navigatorKey`.
|
|
154
185
|
|
|
155
|
-
|
|
186
|
+
Prefer wiring the backend directly? Every backend has a `configureX` helper:
|
|
156
187
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
188
|
+
```dart
|
|
189
|
+
FlutterPatcher.configureSupabase(SupabaseUpdateConfig(
|
|
190
|
+
supabaseUrl: 'https://<ref>.supabase.co',
|
|
191
|
+
anonKey: '<anon-key>', // public, RLS-protected reads
|
|
192
|
+
bucket: 'bundles',
|
|
193
|
+
channel: 'production',
|
|
194
|
+
platform: Platform.android,
|
|
195
|
+
updateStrategy: UpdateStrategy.appVersion,
|
|
196
|
+
appVersion: '1.0.0', // or omit — auto-detected from versionName
|
|
197
|
+
));
|
|
198
|
+
await FlutterPatcher.init(autoApplyUpdates: true);
|
|
165
199
|
```
|
|
166
200
|
|
|
167
|
-
|
|
201
|
+
### 4. Build a patch
|
|
202
|
+
|
|
203
|
+
Rebuild the release APK, then pack it into a device-ready `patch.zip`:
|
|
168
204
|
|
|
169
205
|
```bash
|
|
170
|
-
|
|
206
|
+
flutter build apk --release
|
|
207
|
+
flutter-ota build \
|
|
171
208
|
--apk build/app/outputs/flutter-apk/app-release.apk \
|
|
172
209
|
--version 1.0.1 \
|
|
173
|
-
--target-version-code 100
|
|
174
|
-
--assets assets/hero.png,assets/strings/zh.json
|
|
210
|
+
--target-version-code 100
|
|
175
211
|
```
|
|
176
212
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
a `PatchInfo` pointing at it.
|
|
213
|
+
Every ABI in the APK is included, so one bundle serves all devices. Add
|
|
214
|
+
`--assets assets/hero.png,assets/strings/zh.json` to overlay updated assets.
|
|
180
215
|
|
|
181
|
-
###
|
|
216
|
+
### 5. Deploy
|
|
182
217
|
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
targetVersionCode: 100,
|
|
190
|
-
),
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
if (result.ok) {
|
|
194
|
-
// Patch takes effect on the next cold start.
|
|
195
|
-
}
|
|
218
|
+
```bash
|
|
219
|
+
flutter-ota deploy \
|
|
220
|
+
--source dist \
|
|
221
|
+
--channel production \
|
|
222
|
+
--target-app-version 1.0.0 \
|
|
223
|
+
--force
|
|
196
224
|
```
|
|
197
225
|
|
|
198
|
-
|
|
226
|
+
That's it — devices pick up the patch on their next check.
|
|
199
227
|
|
|
200
|
-
|
|
201
|
-
final bytes = await loadPatchFromYourSource();
|
|
202
|
-
final result = await FlutterPatcher.applyPatchBytes(
|
|
203
|
-
bytes,
|
|
204
|
-
version: '1.0.0-h1',
|
|
205
|
-
targetVersionCode: 100,
|
|
206
|
-
);
|
|
207
|
-
```
|
|
228
|
+
### 6. Roll back (server side)
|
|
208
229
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
```dart
|
|
212
|
-
await FlutterPatcher.rollback();
|
|
230
|
+
```bash
|
|
231
|
+
flutter-ota rollback --channel production
|
|
213
232
|
```
|
|
214
233
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
this deletes the local patch, it doesn't change what's in your backend.
|
|
234
|
+
Or on the device, `FlutterPatcher.rollback()` deletes the local patch and
|
|
235
|
+
reverts to the APK's built-in version on the next cold start.
|
|
218
236
|
|
|
219
237
|
---
|
|
220
238
|
|
|
221
239
|
## How it works
|
|
222
240
|
|
|
223
241
|
```text
|
|
224
|
-
┌─────────────────┐
|
|
225
|
-
│ Your backend │
|
|
226
|
-
│ (or CDN) │ stores: patch.zip (the diff) + manifest.json (the metadata)
|
|
242
|
+
┌─────────────────┐ Supabase / Postgres / Cloudflare / AWS / PocketBase / CDN
|
|
243
|
+
│ Your backend │ stores: patch.zip (the diff) + manifest.json (metadata)
|
|
227
244
|
└────────▲────────┘
|
|
228
245
|
│ 1. check-for-update on launch
|
|
229
|
-
│ 2.
|
|
230
|
-
│
|
|
246
|
+
│ 2. backend returns PatchInfo (url, md5, signature, force flag)
|
|
231
247
|
┌────────┴────────┐
|
|
232
|
-
│ The app │
|
|
233
|
-
│ │ 3. download patch.zip
|
|
248
|
+
│ The app │ 3. download patch.zip
|
|
234
249
|
│ │ 4. verify MD5 + Ed25519 signature
|
|
235
250
|
│ │ 5. write to local patch dir (atomic rename)
|
|
236
251
|
│ │ 6. cold-restart (forced) OR next cold start (staged)
|
|
237
252
|
│ │ 7. loader hook reads patched libapp.so + asset overlays
|
|
238
|
-
│ │ 8. boot
|
|
239
|
-
│ │ boot fails → auto-rollback + blacklist
|
|
253
|
+
│ │ 8. boot ok → keep · boot fails → auto-rollback + blacklist
|
|
240
254
|
└─────────────────┘
|
|
241
255
|
```
|
|
242
256
|
|
|
243
|
-
A patch is a
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
process — it loads on the next cold start. The cold-restart for forced
|
|
247
|
-
updates goes through `restart_app` (with `RestartMode.process`, which
|
|
248
|
-
actually exits the process so the new code reloads).
|
|
257
|
+
A patch is a byte-for-byte diff of the new APK's `libapp.so` against the
|
|
258
|
+
version the user has installed; asset patches overlay `flutter_assets/` by path.
|
|
259
|
+
Nothing is swapped into the running process — it loads on the next cold start.
|
|
249
260
|
|
|
250
261
|
Full internals: [Architecture](doc/architecture.md).
|
|
251
262
|
|
|
@@ -253,139 +264,68 @@ Full internals: [Architecture](doc/architecture.md).
|
|
|
253
264
|
|
|
254
265
|
## What can and can't be patched
|
|
255
266
|
|
|
256
|
-
| ✅ Hot-patchable
|
|
257
|
-
|
|
258
|
-
| Anything in `lib/` — widgets, logic, routes, constants
|
|
259
|
-
| Pure-Dart package upgrades (same native side)
|
|
260
|
-
| Flutter asset files
|
|
261
|
-
| New `Image.asset()` / `rootBundle.load()` calls
|
|
262
|
-
|
|
|
263
|
-
|
|
|
264
|
-
| | `pubspec.yaml` font registration changes |
|
|
267
|
+
| ✅ Hot-patchable | ❌ Not hot-patchable |
|
|
268
|
+
|------------------------------------------------------|----------------------------------------------------------|
|
|
269
|
+
| Anything in `lib/` — widgets, logic, routes, constants | Native code (Kotlin / Java / C++ in `android/src/main/`) |
|
|
270
|
+
| Pure-Dart package upgrades (same native side) | `AndroidManifest.xml` changes |
|
|
271
|
+
| Flutter asset files registered in `pubspec.yaml` | APK `res/` (icons, layouts, strings.xml) |
|
|
272
|
+
| New `Image.asset()` / `rootBundle.load()` calls | Flutter Engine upgrades |
|
|
273
|
+
| | Adding or removing native plugins |
|
|
274
|
+
| | Removing assets that exist in the base APK |
|
|
265
275
|
|
|
266
|
-
|
|
267
|
-
|
|
276
|
+
See [API Reference → What can be patched](doc/api-reference.md#what-can-and-cannot-be-patched)
|
|
277
|
+
for edge cases (ProGuard/R8, multi-ABI/flavors, state migrations).
|
|
268
278
|
|
|
269
279
|
---
|
|
270
280
|
|
|
271
281
|
## Safety
|
|
272
282
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
- Patches are bound to the host APK's `versionCode` — old patches expire
|
|
288
|
-
after an APK upgrade and the server stops serving them.
|
|
289
|
-
- Always download over HTTPS. Keep private signing keys on the server only,
|
|
290
|
-
never in the app bundle.
|
|
291
|
-
|
|
292
|
-
### Production tips
|
|
293
|
-
|
|
294
|
-
- **Stage your rollout** (1% → 5% → 20% → 50% → 100%) and monitor crash
|
|
295
|
-
rate at each stage. The SDK records `lastBootDiagnostic` per device.
|
|
296
|
-
- **Report diagnostics** to your analytics pipeline so you can spot a bad
|
|
297
|
-
patch before users hit the app.
|
|
298
|
-
- **Prepare for emergency rollback** — stop returning the bad patch from
|
|
299
|
-
your endpoint; devices that already tripped crash protection have
|
|
300
|
-
rolled back locally and won't re-download.
|
|
301
|
-
|
|
302
|
-
Full release workflow + diagnostic-reporting code:
|
|
303
|
-
[Production Playbook](doc/production-playbook.md).
|
|
304
|
-
|
|
305
|
-
---
|
|
306
|
-
|
|
307
|
-
## FAQ
|
|
308
|
-
|
|
309
|
-
**Q: Must the patch and base APK use the same Flutter version?**
|
|
310
|
-
A: Yes. `libapp.so` is tightly coupled to the Flutter Engine ABI.
|
|
311
|
-
After upgrading the SDK, ship a new release.
|
|
312
|
-
|
|
313
|
-
**Q: Why doesn't a patch take effect immediately?**
|
|
314
|
-
A: Once `libapp.so` is loaded by the current process, it can't be swapped
|
|
315
|
-
at runtime. The patch is written to disk and loaded on the next cold
|
|
316
|
-
start. Forced updates trigger a process restart so the new code loads
|
|
317
|
-
immediately.
|
|
318
|
-
|
|
319
|
-
**Q: Why does each patch need a `targetVersionCode`?**
|
|
320
|
-
A: Two reasons: (1) old patches expire after an APK upgrade (so a user on
|
|
321
|
-
v2 doesn't accidentally load a v1-targeted patch), and (2) the server
|
|
322
|
-
won't ship a patch built for v1 to a user on v2 (the SDK filters them
|
|
323
|
-
out at update-check time).
|
|
324
|
-
|
|
325
|
-
**Q: How does the client report its app version, and why must it match `--target-app-version`?**
|
|
326
|
-
A: The client reports an `appVersion` that the backend matches against
|
|
327
|
-
each bundle's `target_app_version`. By default this is **auto-detected at
|
|
328
|
-
runtime** from the host app's `versionName` via `package_info_plus` — no
|
|
329
|
-
build flag needed. You can override it explicitly with
|
|
330
|
-
`--dart-define=APP_VERSION=1.2.3` or `SupabaseUpdateConfig.appVersion`.
|
|
331
|
-
The backend keeps only bundles whose `target_app_version` is
|
|
332
|
-
semver-compatible with the reported version. **If they don't match, the
|
|
333
|
-
backend returns no bundle and the app silently stays "up to date"** — so
|
|
334
|
-
always deploy with `--target-app-version` equal to the app's real
|
|
335
|
-
`versionName` (e.g. `1.0.1` for a `version: 1.0.1+2` pubspec). A mismatched
|
|
336
|
-
version is the most common cause of "the update never arrives".
|
|
337
|
-
|
|
338
|
-
More questions: [Full FAQ](doc/faq.md).
|
|
283
|
+
- **Crash protection (on by default).** If a patch fails to boot, the SDK
|
|
284
|
+
auto-rolls back and blacklists the offending version. Tune with
|
|
285
|
+
`maxCrashCount` (default 1) and `verifyAfter` (default 5s). Crash attribution
|
|
286
|
+
is bounded to a boot window, so an unrelated crash long after a healthy boot
|
|
287
|
+
won't revert a working patch. See [Crash Protection](doc/crash-protection.md).
|
|
288
|
+
- **Integrity & signing.** MD5 is strongly recommended; Ed25519 signature
|
|
289
|
+
verification is available on Android 13+ (falls back to MD5-only below, with a
|
|
290
|
+
warning). Patches are bound to the host APK's `versionCode`, so stale patches
|
|
291
|
+
expire after an app upgrade. Keep private signing keys on your server, never
|
|
292
|
+
in the app bundle.
|
|
293
|
+
- **Staged rollout.** Ship 1% → 5% → 20% → 50% → 100% and watch crash rate at
|
|
294
|
+
each stage. The SDK records `lastBootDiagnostic` per device.
|
|
295
|
+
|
|
296
|
+
Full release workflow: [Production Playbook](doc/production-playbook.md).
|
|
339
297
|
|
|
340
298
|
---
|
|
341
299
|
|
|
342
300
|
## Documentation
|
|
343
301
|
|
|
344
|
-
**Guides**
|
|
302
|
+
**Guides**
|
|
345
303
|
|
|
346
|
-
- [Beginner Guide](doc/beginner-guide.md) — zero
|
|
347
|
-
- [Getting Started](doc/getting-started.md) — scaffold → build → deploy
|
|
348
|
-
- [Developer Guide](doc/developer-guide.md) — full workflow reference
|
|
349
|
-
- [Configuration](doc/configuration.md) —
|
|
350
|
-
- [Backends](doc/backends.md) — Supabase / Postgres / Cloudflare / AWS / PocketBase setup
|
|
351
|
-
- [Production Playbook](doc/production-playbook.md) — staged rollout, diagnostics,
|
|
304
|
+
- [Beginner Guide](doc/beginner-guide.md) — zero to first OTA, narrated
|
|
305
|
+
- [Getting Started](doc/getting-started.md) — scaffold → build → deploy
|
|
306
|
+
- [Developer Guide](doc/developer-guide.md) — full workflow reference
|
|
307
|
+
- [Configuration](doc/configuration.md) — env vars, `.env`, resolution order
|
|
308
|
+
- [Backends](doc/backends.md) — Supabase / Postgres / Cloudflare / AWS / PocketBase setup
|
|
309
|
+
- [Production Playbook](doc/production-playbook.md) — staged rollout, diagnostics, rollback
|
|
352
310
|
|
|
353
311
|
**Reference**
|
|
354
312
|
|
|
355
|
-
- [API Reference](doc/api-reference.md) — `FlutterPatcher
|
|
356
|
-
- [Architecture](doc/architecture.md) — internals,
|
|
357
|
-
- [CLI Reference](doc/cli-reference.md) — every command
|
|
358
|
-
- [Crash Protection](doc/crash-protection.md) —
|
|
359
|
-
- [Golden Testing](doc/golden-testing.md) — how Flutter's pixel-perfect test system works (and how this project uses it)
|
|
360
|
-
|
|
361
|
-
**Other**
|
|
362
|
-
|
|
313
|
+
- [API Reference](doc/api-reference.md) — `FlutterPatcher`, configs, overlay, error codes
|
|
314
|
+
- [Architecture](doc/architecture.md) — internals, protocol, signing
|
|
315
|
+
- [CLI Reference](doc/cli-reference.md) — every command and flag
|
|
316
|
+
- [Crash Protection](doc/crash-protection.md) — rollback, blacklist, Android differences
|
|
363
317
|
- [FAQ](doc/faq.md) — versioning, cold start, store policy
|
|
364
|
-
- [CHANGELOG.md](CHANGELOG.md) — release notes
|
|
365
|
-
|
|
366
|
-
中文文档: [README-zh.md](README-zh.md) · [api-reference-zh](doc/api-reference-zh.md) ·
|
|
367
|
-
[architecture-zh](doc/architecture-zh.md) · [crash-protection-zh](doc/crash-protection-zh.md) ·
|
|
368
|
-
[getting-started-zh](doc/getting-started-zh.md) · [production-playbook-zh](doc/production-playbook-zh.md) ·
|
|
369
|
-
[faq-zh](doc/faq-zh.md)
|
|
370
|
-
|
|
371
|
-
---
|
|
372
|
-
|
|
373
|
-
## Who's using it?
|
|
374
|
-
|
|
375
|
-
If you run flutter_ota_kit in production, [open an issue](https://github.com/HYPER12755/flutter-ota-kit/issues) and tell us about your use case — we'd love to list you here.
|
|
376
318
|
|
|
377
319
|
---
|
|
378
320
|
|
|
379
321
|
## Contributing
|
|
380
322
|
|
|
381
|
-
Issues and PRs
|
|
382
|
-
|
|
383
|
-
Before submitting, please make sure:
|
|
323
|
+
Issues and PRs welcome. Before submitting:
|
|
384
324
|
|
|
385
|
-
- `flutter analyze` reports no
|
|
325
|
+
- `flutter analyze` reports no issues
|
|
386
326
|
- `flutter test` is fully green
|
|
387
|
-
- If you touched native code,
|
|
388
|
-
-
|
|
327
|
+
- If you touched native code, run a real-device patch / rollback end-to-end
|
|
328
|
+
- Document new public APIs (they surface on pub.dev)
|
|
389
329
|
|
|
390
330
|
---
|
|
391
331
|
|
package/dart-src/root/android/src/main/kotlin/com/flutter_patcher/flutter_patcher/PatchManager.kt
CHANGED
|
@@ -613,14 +613,22 @@ internal class PatchManager(
|
|
|
613
613
|
}
|
|
614
614
|
|
|
615
615
|
private fun extractBaseAssetsIfNeeded(): Int {
|
|
616
|
-
//
|
|
616
|
+
// Store the base APK's flutter_assets as a single archive for rollback.
|
|
617
|
+
// We keep ONLY the compressed archive (not an uncompressed mirror dir):
|
|
618
|
+
// the previous implementation left both `assets/0/` (uncompressed) AND
|
|
619
|
+
// `assets/0/flutter_assets.apk` (zip) on disk — two full copies of every
|
|
620
|
+
// bundled asset. The uncompressed copy is never read (rollback re-extracts
|
|
621
|
+
// from the archive), so it was pure waste.
|
|
617
622
|
if (!baseAssetsArchive.exists()) {
|
|
618
623
|
baseAssetsArchive.parentFile?.mkdirs()
|
|
624
|
+
val tmp = File(assetsHistoryDir, "0.tmp")
|
|
619
625
|
try {
|
|
620
|
-
copyInstalledFlutterAssets(
|
|
621
|
-
writeFlutterAssetsArchive(
|
|
626
|
+
copyInstalledFlutterAssets(tmp)
|
|
627
|
+
writeFlutterAssetsArchive(tmp, baseAssetsArchive)
|
|
622
628
|
} catch (e: Exception) {
|
|
623
629
|
Log.w(TAG, "failed to extract base assets", e)
|
|
630
|
+
} finally {
|
|
631
|
+
tmp.deleteRecursively()
|
|
624
632
|
}
|
|
625
633
|
}
|
|
626
634
|
return 0
|
|
@@ -998,8 +1006,19 @@ internal class PatchManager(
|
|
|
998
1006
|
meta.put("assetsRef", assetsRef)
|
|
999
1007
|
archiveCurrentPatchToHistory(meta)
|
|
1000
1008
|
} else {
|
|
1001
|
-
// First patch ever
|
|
1002
|
-
|
|
1009
|
+
// First patch ever.
|
|
1010
|
+
//
|
|
1011
|
+
// Only extract the base APK's flutter_assets (as a rollback
|
|
1012
|
+
// reference) when this patch actually ships assets. A Dart-only
|
|
1013
|
+
// (code-only) patch never overlays assets, so extracting the
|
|
1014
|
+
// whole base asset tree here is pure waste — it was the main
|
|
1015
|
+
// cause of a first code-patch ballooning app storage by 100s of
|
|
1016
|
+
// MB. Code-only patches load assets straight from the APK.
|
|
1017
|
+
assetsRef = if (finalAssets != null) {
|
|
1018
|
+
extractBaseAssetsIfNeeded()
|
|
1019
|
+
} else {
|
|
1020
|
+
0
|
|
1021
|
+
}
|
|
1003
1022
|
meta.put("assetsRef", assetsRef)
|
|
1004
1023
|
}
|
|
1005
1024
|
|