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 dn command 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, open ios/Runner.xcworkspace in Xcode once and set your team and the bundle identifier under Signing & Capabilities (a free Apple ID is enough). On Android, set applicationId in android/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.dart holds the four mistakes, each a small function that returns a plain value. This is the only file you edit.
  • lib/checkout_screen.dart is the screen. It reads the four values while it draws and never changes.
  • lib/main.dart only registers the platform bindings and starts the app. Code push needs no line of its own: runApp applies 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 release also 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 priceWithTax return a double. 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 TextStyle or an Icon in the changed file is refused. That is why the screen, not fix_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=1 in 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 run command from step 3; a plain dn run --release ignores 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 install fails 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.

Open the finished code →