Skip to content

Multiple Dart entrypoints and engines

A host app can start a Flutter module through more than one Dart entrypoint (functions marked @pragma('vm:entry-point')), and run several Flutter engines at once, for example with FlutterEngineGroup. Shorebird supports this with no extra setup. This page explains how patches behave in that case.

A patch replaces the module’s compiled Dart code as a whole, not one entrypoint at a time. Every entrypoint is part of the same release and the same patch, so a patch can change the code that any of them runs.

Shorebird picks the patch to run when the app process starts. With auto_update on (the default), it also checks for a new one once per process, not once per engine (see Checking for updates). A patch downloaded while the app is running is meant for the next time the app starts. To run it, fully quit the app and launch it again.

Don’t count on engines you create later in the same process running the same patch as the first one. On Android they do: Shorebird picks the patch once per process. On iOS, an engine created after a new patch has downloaded can start that new patch while older engines keep running the previous one. If your entrypoints share state that a patch changes the shape of, start every engine when the app starts, or make the code tolerate both versions.

The host app’s native code names the entrypoints it starts, and Shorebird doesn’t patch native code. So, in a patch:

  • Don’t remove or rename an entrypoint that the host app starts. The host app shipped in the store still asks for it by name. Shorebird doesn’t check this when you create the patch.
  • Adding an entrypoint is allowed, but nothing calls it until you ship a host app that does. Add it in a release instead.
  • Keep @pragma('vm:entry-point') on every entrypoint. Without it, the Dart compiler can remove a function that only native code calls.

Changing what an entrypoint does, or what its arguments mean, is fine as long as the host app you shipped still passes arguments the new code accepts.

With auto_update on (the default), the updater checks for a patch once per process, when the first engine starts. You don’t need to do anything in each entrypoint.

With auto_update: false, your code decides when to check, using package:shorebird_code_push. Check from the entrypoint that runs first, or from each entrypoint if you don’t know which one will. If you check from each one, pass every check the same track, and expect a network request each time an engine starts. Only one check runs at a time: if a second engine calls update while another check is running, that call returns without starting another check. (In a release built with a Flutter version older than 3.41.7, or with shorebird_code_push older than 2.0.6, it throws an UpdateException instead.) update also throws when a check fails, for example when the device is offline, so catch errors from update in every entrypoint that calls it. The testing patches page has an example.

@pragma('vm:entry-point')
void main(List<String> args) {
  checkForPatch(args);
  runApp(const MyApp());
}

@pragma('vm:entry-point')
void settingsMain(List<String> args) {
  checkForPatch(args);
  runApp(const SettingsApp());
}