Skip to content

chore(example): fix example app builds on current toolchains (iOS 27, Apple Silicon, Flutter 3.47) - #1180

Merged
hiroshihorie merged 10 commits into
mainfrom
hiroshi/fix-example-builds
Aug 26, 2026
Merged

chore(example): fix example app builds on current toolchains (iOS 27, Apple Silicon, Flutter 3.47)#1180
hiroshihorie merged 10 commits into
mainfrom
hiroshi/fix-example-builds

Conversation

@hiroshihorie

Copy link
Copy Markdown
Member

The example app no longer launched on iOS 27 and no longer built for Android at all. This gets it running again on every platform, and clears out config that had drifted from what flutter create generates.

Scoped entirely to example/ — no changes to the published package, hence no changeset.

The actual fix

iOS 27 terminates apps that have not adopted the UIScene lifecycle, and flutter run tool-exits on the resulting device log line, so the app could not be launched on an iOS 27 device at all. Flutter's auto-migrator declined to help (AppDelegate does not match original template).

The old AppDelegate also had a latent bug: under UIScene the app delegate's window is nil, so guard let controller = window?.rootViewController returned early and skipped plugin registration entirely. The migration guide calls out that exact pattern.

Android was separately broken — Flutter 3.47 enforces AGP and Kotlin minimums the example was below, surfaced one at a time (AGP 8.9.1 → 8.11.1, Kotlin 2.1.0 → 2.2.20).

Cleanups

  • EXCLUDED_ARCHS[sdk=iphonesimulator*] = 'arm64' — a 2020 Intel-Mac workaround that, on Apple Silicon, made every build print The following target(s) do not support arm64 architecture and list every plugin.
  • use_modular_headers! — in both Podfiles, in no Flutter template. Verified unnecessary by building each platform with SwiftPM disabled so every plugin resolves through CocoaPods (including flutter_webrtc + WebRTC-SDK, and permission_handler_apple on iOS).
  • Stale project-level IPHONEOS_DEPLOYMENT_TARGET = 12.1 (every target already overrode to 15.0) and three empty EXCLUDED_ARCHS = "".
  • [CP] Embed Pods Frameworks / [CP] Copy Pods Resources — with SwiftPM on, Flutter is the only pod left and pod install removes them. Brings iOS in line with macOS.
  • ndkVersion was pinned to 27.0.12077973 while five plugins require 28.2.13676358; now uses flutter.ndkVersion. Java target VERSION_1_8VERSION_17 (the source/target value 8 is obsolete warning).

Verified

platform result
iOS device (iOS 27.0 beta) launches
iOS simulator (iOS 27.0) launches
macOS launches
Android emulator (clean rebuild) launches
web compiles and boots; UI not captured

Deliberately not included

  • AGP 9.1.0 / Kotlin 2.4.0 / Gradle 9.3.1 (what flutter create now generates). It builds and runs, but buys nothing functional today, and does not unlock built-in Kotlin — device_info_plus 12.3.0 blocks that regardless. Flutter still ships android.builtInKotlin=false in its own template. Left for when the ecosystem catches up. Note Flutter already warns that AGP 8.11.1, Kotlin 2.2.20 and Gradle 8.14 will "soon be dropped", so this is a when, not an if.
  • A RunnerTests target. Neither platform has one, which is why the Podfiles cannot match the template. Not worth adding to an example app.

Unrelated issue found

flutter build runs rsync -av --delete <repo>/ → example/build/<platform>/SourcePackages/client-sdk-flutter — the destination is inside the source, because livekit_client is a path: dependency on the repo root. From a clean build/ it is one bounded ~737 MB copy, but each build re-copies the previous copies. It reached 30 GB and later 43 GB during this work, nesting 9 levels deep. Workaround is flutter clean between platform switches; the real fix is upstream in Flutter, and it will hit any plugin repo whose example depends on its parent by path.

@hiroshihorie hiroshihorie changed the title Fix example app builds on current toolchains (iOS 27, Apple Silicon, Flutter 3.47) chore(example): fix example app builds on current toolchains (iOS 27, Apple Silicon, Flutter 3.47) Aug 26, 2026
@hiroshihorie
hiroshihorie marked this pull request as ready for review August 26, 2026 16:47

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@hiroshihorie
hiroshihorie force-pushed the hiroshi/fix-example-builds branch from a4567e2 to bb19a19 Compare August 26, 2026 16:59
hiroshihorie added a commit that referenced this pull request Aug 26, 2026
`Build` has been failing on `main` and therefore on every PR, regardless
of what the PR changes — visible on #1180,
`max/fix-ios-audio-session-before-recording` and `sync-upstream-2.11.0`,
all of which show `Build ✗` with `Changeset Check ✓`. Two jobs are
responsible.

## Dart Format

Three files under `scripts/` are not formatted, so `dart format .
--set-exit-if-changed` exits 1. Straight from the CI log:

```
Formatted scripts/check_version.dart
Formatted scripts/create_change.dart
Formatted scripts/create_version.dart
Formatted 227 files (3 changed) in 0.72 seconds.
##[error]Process completed with exit code 1
```

Formatting only, no behaviour change.

## Dart Analyze

```
lib/src/token_source/caching.dart:138:9 • unawaited_return_in_try_block
lib/src/token_source/caching.dart:144:7 • unawaited_return_in_try_block
```

`CachingTokenSource.fetch` returned `resultFuture` from inside its
`try`, so the surrounding `catch` could not observe a failure from that
future and `finally` removed the in-flight entry before it settled. Both
returns now `await`.

Behaviour is unchanged — the completer is completed on the line
immediately before each return, so the awaited future is already
resolved — but errors now route through the existing `catch`, and the
in-flight map is cleared only once the request settles. `AGENTS.md`
calls out un-awaited async state updates as a recurring bug class here,
so this is a real latent issue rather than a lint silence. Includes a
`patch type="fixed"` changeset.

## The analysis_options churn, and a coverage hole it was hiding

`flutter analyze` kept leaving `analysis_options.yaml` modified. The
cause is `AnalysisOptionsMigration`
(`flutter_tools/lib/src/migrations/analysis_options_migration.dart`,
invoked from `project.dart:429`), which appends seven patterns —
`build/**`, `android/**`, `ios/**`, `web/**`, `windows/**`, `macos/**`,
`linux/**` — on every `flutter analyze` / `pub get` / `run`. It is
unconditional; there is no feature flag or config to disable it, unlike
the UIScene migrator's `enable-uiscene-migration`.

Committing the patterns makes it a no-op. But one of them matters:
**this package keeps real Dart source in `web/`** (the E2EE worker), not
just assets, so `web/**` silently drops it from analysis. That was
already happening in CI, since `flutter analyze` runs the migrator
before it analyzes.

Passing the path explicitly does **not** work — `dart analyze web/`
still honours the root exclude. I confirmed this by appending a
deliberate type error to `web/e2ee.logger.dart`: it reported `No issues
found!`. Giving `web/` its own `analysis_options.yaml` creates a
separate context that is genuinely analyzed; the same canary then
correctly reports `return_of_invalid_type`. That file mirrors the root's
error overrides and its formatter settings (`page_width: 120`,
`trailing_commas: preserve`) so `dart format` output is unaffected, and
`build.yaml` gains a `dart analyze web/` step.

## Verified locally

```
dart format . --set-exit-if-changed   PASS  (227 files, 0 changed)
flutter analyze                        PASS  (No issues found)
dart analyze web/                      PASS  (and fails on an injected error)
flutter test                           PASS  (+398)
dart run scripts/check_version.dart    PASS
import_sorter --exit-if-changed        PASS
```

## Not included

Pinning the Flutter version in
`.github/actions/setup-flutter/action.yml`, which currently passes only
`channel: stable` and so tracks whatever the newest stable release is.
That is a genuine fragility — `unawaited_return_in_try_block` arrived
this way — but it is a separate decision from unbreaking CI, and worth
its own discussion.

Draft because #1180 should confirm this actually turns `Build` green
once rebased on it.
Bumps the iOS floor for the example app and its broadcast extension:

- Podfile `platform :ios` 13.0 -> 15.0, and the post_install override
  that forces IPHONEOS_DEPLOYMENT_TARGET on every pod 12.1 -> 15.0
- Runner and LiveKit Broadcast Extension targets 14.0 -> 15.0

Also sorts Info.plist keys into alphabetical order, which Xcode does on
save (no semantic change).
`flutter analyze` in example/ was walking build output and the native
platform folders. Adds the standard excludes (build, android, ios, web,
windows, macos, linux) alongside the existing generated-code patterns.
iOS 27 terminates apps that have not adopted the UIScene lifecycle, and
`flutter run` tool-exits on the resulting device log line, so the example
app could no longer be launched on an iOS 27 device.

- Add `UIApplicationSceneManifest` to Info.plist using the stock
  `FlutterSceneDelegate`, so no custom scene delegate is needed.
- Conform AppDelegate to `FlutterImplicitEngineDelegate` and move
  `GeneratedPluginRegistrant.register` into
  `didInitializeImplicitFlutterEngine`. Under UIScene the app delegate's
  `window` is nil, so the old `guard let controller = window?...` returned
  early and skipped plugin registration entirely.

See https://flutter.dev/to/uiscene-migration
Flutter 3.47 requires a macOS 12.0 floor; the example was still at 10.15.
Updates the Podfile `platform :osx` and MACOSX_DEPLOYMENT_TARGET in all
three build configurations.
No Flutter template uses `use_modular_headers!` on any platform; it was a
local addition to both example Podfiles.

Verified unnecessary by building each platform with Swift Package Manager
disabled, so every plugin resolves through CocoaPods (including
flutter_webrtc + WebRTC-SDK, and permission_handler_apple on iOS):
both `pod install` and the Xcode build succeed without it.
The Podfile's post_install forced
`EXCLUDED_ARCHS[sdk=iphonesimulator*] = 'arm64'` on every pod. That is a
2020 workaround for Intel Macs, and on Apple Silicon it is backwards: it
made `flutter run`/`flutter build` print

  The following target(s) do not support arm64 architecture, which is a
  requirement for Apple Silicon iOS 26+ simulators

on every invocation, listing every plugin.

After removing it, `flutter build ios --simulator --debug` succeeds, the
warning is gone, and the built binary reports `x86_64 arm64`.
- Project-level IPHONEOS_DEPLOYMENT_TARGET 12.1 -> 15.0. Every target
  already overrode this to 15.0, so it was dead weight, and any new
  target would have silently inherited 12.1.
- Drop the three empty `EXCLUDED_ARCHS = ""` entries, left over from the
  same era as the simulator arm64 workaround.
With Swift Package Manager on (the default since Flutter 3.41), every
plugin resolves as a Swift Package and `Flutter` is the only remaining
pod, so `pod install` removes the `[CP] Embed Pods Frameworks` and
`[CP] Copy Pods Resources` build phases: there is nothing left to embed
or copy.

These phases are generated and owned by CocoaPods, so `pod install`
re-adds them for anyone building with SPM disabled. Committing the
generated state keeps iOS consistent with macOS, which already carried
only `[CP] Check Pods Manifest.lock`.
`flutter run -d <android>` failed outright: Flutter 3.47 enforces
minimum AGP and Kotlin Gradle Plugin versions that the example was below,
and they surface one at a time.

  Android Gradle Plugin  8.9.1 -> 8.11.1  (Flutter minimum)
  Kotlin Gradle Plugin   2.1.0 -> 2.2.20  (Flutter minimum)

Also picks up the two AGP 9 opt-outs that Flutter's own migrator writes
into gradle.properties (`android.builtInKotlin`, `android.newDsl`).

These are the enforced minimums, not the versions `flutter create` now
generates (AGP 9.1.0 / Kotlin 2.4.0 / Gradle 9.3.1). Moving to those is a
larger toolchain jump and is left as separate work. Gradle stays at 8.14,
which only warns.

Verified by running the example on a Pixel 10 Pro emulator.
Two divergences from what `flutter create` generates, both surfaced as
build warnings:

- ndkVersion was pinned to 27.0.12077973 while connectivity_plus,
  device_info_plus, flutter_background, flutter_webrtc and jni all
  require 28.2.13676358. Use `flutter.ndkVersion` (the template's own
  reference, currently 28.2.13676358) so this tracks the SDK instead of
  drifting again.
- Java source/target compatibility was VERSION_1_8, which now warns
  "source/target value 8 is obsolete and will be removed in a future
  release". The template uses VERSION_17.

kotlinOptions.jvmTarget moves to 17 alongside it; leaving it at 1_8 would
fail with inconsistent JVM-target compatibility.

Both warnings are gone from the build output and the APK still builds.
@hiroshihorie
hiroshihorie force-pushed the hiroshi/fix-example-builds branch from bb19a19 to e982293 Compare August 26, 2026 18:07
@hiroshihorie
hiroshihorie merged commit dd2c804 into main Aug 26, 2026
15 checks passed
@hiroshihorie
hiroshihorie deleted the hiroshi/fix-example-builds branch August 26, 2026 18:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant