Tutorial · 20 min · advanced
Code push
Send a fix straight to phones, without a release to App Store and Google Play: release once, fix one file, and watch the installed app correct itself.
Code push sends a fix directly to the phones that already have your app. You make a release once, the way you always do. Later you change some Dart, run one command, and the installed apps pick up the change by themselves: no new store release, no review, no reinstall.
The app in this tutorial ships four deliberate mistakes: a total that
forgets the tax, a typo in the screen title, a total drawn far too small,
and the wrong icon next to “Paid”. You release it, install it on a phone,
fix the mistakes in one file, and send the fixes with dn patch. The
phone downloads them on its own, and the next time the app starts it is
correct. The screen labels each mistake as it goes from AS SHIPPED to
FIXED, so you can see exactly what happened.
It takes about 20 minutes, most of it build time.
What you need
- The
dncommand installed, and a phone: a real iPhone (release builds do not run on the iOS Simulator), or an Android phone or emulator. - The finished code from
tutorials/code_push. You work in that app throughout. - Your licence key, configured once:
dn config --license-key dnk_...
Code push works under your own account. dn release and dn patch
register what they build with the update service under your key, and a
phone downloads a fix only after the daily licence check your key allows.
Every build you make carries the key you configured.
- The opt-in. Code push is still experimental, so the two commands run only when you ask for it, in the shell you use for every command below:
export DN_CODE_PUSH_EXPERIMENTAL=1
- An app id of your own, on both platforms, before the first release. An
app id belongs to the first account that ships it, so the id this app
comes with cannot work for everyone who tries it. Put your own name or
domain in it, for example
com.yourname.codepushtutorial. On iPhone, openios/Runner.xcworkspacein Xcode once and set your team and the bundle identifier under Signing & Capabilities (a free Apple ID is enough). On Android, setapplicationIdinandroid/app/build.gradle.kts. While the app still has the id it came with, a yellow card at the top of its screen reminds you.
1. The app, in three files
lib/fix_me.dartholds the four mistakes, each a small function that returns a plain value. This is the only file you edit.lib/checkout_screen.dartis the screen. It reads the four values while it draws and never changes.lib/main.dartonly registers the platform bindings and starts the app. Code push needs no line of its own:runAppapplies a fix the previous run downloaded before the first screen, and checks for a newer one once the screen is up.
A fix reaches every caller of a fixed function, so a screen that never changed still shows the corrected values.
The mistakes are plain values, not widgets, for a reason. Today a fix may
build only a handful of framework widgets (Text, Column, Row,
SizedBox, Padding, Center, Container, EdgeInsets.all and
Color), and it may not create an instance of a class declared in the
file it changes. Numbers, text and true/false keep every fix inside those
rules, and the screen turns them into widgets.
2. Make the release
dn release ios # or: dn release android
This builds the app in release mode and remembers everything a later fix needs. It ends with:
✓ Released 1.0.0+1
...
Ship this build. When you need to fix something in it:
dn patch ios
Two things appear in the project:
dn_code_push.yaml: the app’s own id, the release version and the address where this release asks about fixes.dn releasealso adds it to the app’s assets, so the installed app carries it. Keep it in version control..dn_code_push/: the recorded release. Every fix is the difference from exactly this, so only this machine can send fixes for this release. It is already in.gitignore.
3. Put the release on the phone
On Android, dn release android keeps the release app with the recorded
release, and that is the one to install:
adb install -r .dn_code_push/apps/android/1.0.0+1/app-release.apk
On iPhone, dn release ios builds without signing, so install with
dn run, and hand it the same list of fixable functions the release used:
dn run -d <your iPhone> --release \
--extra-front-end-options=--dynamic-interface=$PWD/.dn_code_push/blocks/ios/1.0.0+1/build_iface.yaml
The long option matters: it marks every function as fixable, exactly as
the release build did. A plain dn run --release installs an app that
looks the same but ignores fixes.
Open the app. You see “Running as shipped” at the top, a small red total
of 100, a cart next to Paid, and all four rows marked AS SHIPPED. From
here on, do not run dn run again for this app: it would install a build
with your later edits compiled in, and there would be nothing left to fix.
4. Fix the number and send it
In lib/fix_me.dart, make priceWithTax add the tax:
int priceWithTax(int cents) {
return cents + (cents * 8) ~/ 100;
}
Then:
dn patch ios # or: dn patch android
In well under a minute it prints ✓ Fix 1 is live for 1.0.0+1. On iPhone
the fix carries every function of the file you changed, the three you did
not touch included; they behave exactly as before. On Android the fix is a
small binary difference, one per processor type.
5. Let the phone pick it up
Nobody has to tap anything for this. Every time the app starts, right after its screen comes up, it asks whether a fix is waiting and downloads it in the background, ready for the next start.
Close the app completely, by swiping it away in the app switcher. Putting it in the background is not enough: reopening it then resumes the same run. Open it again and give it a few seconds. At the bottom of the screen, under “What the app’s own check found”, the line reads:
fix 1 was downloaded over https and verified; restart the app to apply it
Nothing else changes yet. A fix is never applied to an app that is already running.
The Check for a fix button asks again, for when you want to watch a check happen. If a line says the phone has not completed its daily licence check yet, the app is still exchanging your key for the day’s pass: wait a few seconds and try again.
6. Restart, and watch it correct itself
Swipe the app away once more and open it. The top now reads “Running fix 1”, the total is 108 in green with a FIXED tag, and the other three rows are still AS SHIPPED. Under “What the app reported when it started”, the iPhone counts the functions attached (4 attached, 0 failed) and confirms the first screen came up with the fix; Android reports “fix 1 applied at launch”.
7. Fix the other three
In lib/fix_me.dart, make the remaining functions return 'Checkout',
40 and 'check' (each has the line in its comment), and run dn patch
again. The service numbers it fix 2. On the phone, close and reopen the
app, give it a few seconds to download the fix, then close and reopen it
once more. The title reads “Checkout”, the total is large, the icon is a
check mark, all four rows say FIXED, and the top says “Running fix 2”.
Fix 2 replaced fix 1. A phone holds one fix per release, and that fix carries everything that differs from the release, so you never send fixes in order or worry about one stacking on another.
What a fix cannot do
dn patch checks each fix before anything is sent, and refuses:
- A change to a function’s type, such as making
priceWithTaxreturn adouble. Change what a function returns, not its name or its type. - A new class in the file you changed, used from there, or a new method on a class the app was released with. Put a new class in its own, unchanged file, or ship a release.
- Widgets outside the small allowed set. A
TextStyleor anIconin the changed file is refused. That is why the screen, notfix_me.dart, turns the icon’s name into an icon. - A new picture, font or sound file. A fix carries Dart code only. Files are packed into the app by the store build, so a fix can choose among the files the app already has, but cannot add one. The same goes for a plugin’s native code and for the framework itself.
A fix may add a new function: a new top-level function or static method in the file you change travels inside the fix, and a fixed function in that same file can call it.
On Android a fix runs at full speed, as ordinary compiled code. On iPhone only the functions a fix changes run in an interpreter, the way a React Native app runs all of its code, all the time. So a fixed function runs like React Native code, and the rest of your app keeps running compiled, at full speed. A fix usually changes a few small functions, so the app as a whole stays fast. Your next store release compiles the fixed code in, and it runs at full speed too.
If something does not look right
- “Code push is not available yet”: export
DN_CODE_PUSH_EXPERIMENTAL=1in this shell. - A line says the app’s id belongs to another DartNative account: the id this app came with is still in place (the yellow card is then at the top of the screen), or you used an id someone else already released. The app keeps running, but gets no fixes. Set an id of your own on both platforms, make a new release and install it again.
- The phone never changes after a restart: make sure the restart is a
real one, with the app swiped away first. On iPhone, also check you
installed with the long
dn runcommand from step 3; a plaindn run --releaseignores fixes. - “fix N was built for another framework revision”: the app on the phone and the tool that built the fix come from different DartNative builds. Update the SDK, make a new release, and start again from step 2.
adb installfails with a signature message: a debug build is installed. Uninstall the app first.
What you built
An app that corrects itself on phones that already have it. It rests on
three things: dn release records exactly what shipped, dn patch sends
only what differs from it, and runApp downloads a waiting fix by itself
and applies it at the next start, with no code of your own.
The full code is in
tutorials/code_push.