From 5599e2cdb3947fed7c640e5513efbec557617d61 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 17 Aug 2026 11:36:19 +0200 Subject: [PATCH 01/10] docs(repo): add the flutter-version-bump skill Adopting a new Flutter stable and raising the published minimum are two different jobs that share the phrase "bump Flutter", and the second one is due on a fixed policy (minimum supported = latest stable - 1) rather than when something breaks. The skill separates them and records what this repo's CI actually checks, since most of the traps come from gaps between the jobs: - A floor raise moves two knobs, not three: `melos.yaml` (source of truth, propagated by `melos bs`) and `legacy_version_analyze.yml`. There is no `.fvmrc` here, so nothing pins a developer's toolchain to the floor. - Alchemist keys on `GITHUB_ACTIONS`, not the more common `CI`, so the obvious local invocation runs the untracked platform goldens and fails every golden test for reasons unrelated to the toolchain. - `package_analysis` analyzes `stream_core/lib` only, with `--fatal-warnings`. The beta canary and the N-1 job therefore say nothing about the Flutter packages, and nothing at all about the new infos that `--fatal-infos` turns into failures. - `all_lint_rules.yaml` is an explicit list, so a new SDK's rules never self-activate, and a removed rule surfaces as `undefined_lint` in that file rather than in `analysis_options.yaml`. - Raising the Dart constraint raises each package's language version, which wakes lints whose fix was not previously expressible. `dart fix` applies them mechanically and rewrites or deletes doc comments while doing so, so the diff needs reading in both directions. - Nothing in CI builds for Android or iOS, so platform build floors are invisible here and must not be reported as verified. Co-Authored-By: Claude Opus 5 (1M context) --- .claude/skills/flutter-version-bump/SKILL.md | 482 +++++++++++++++++++ 1 file changed, 482 insertions(+) create mode 100644 .claude/skills/flutter-version-bump/SKILL.md diff --git a/.claude/skills/flutter-version-bump/SKILL.md b/.claude/skills/flutter-version-bump/SKILL.md new file mode 100644 index 00000000..b7c4701e --- /dev/null +++ b/.claude/skills/flutter-version-bump/SKILL.md @@ -0,0 +1,482 @@ +--- +name: flutter-version-bump +description: > + Adopt a new Flutter stable release in this monorepo — diagnose and fix the analyze, format, golden, and barrel + failures a new toolchain introduces, then raise the published minimum Flutter/Dart floor to the SDK's + "latest stable − 1" policy. Use when CI suddenly goes red after a Flutter release, when + `dart analyze --fatal-infos` reports diagnostics that did not exist before, when goldens drift after upgrading, + or when asked to "support Flutter X" or "bump the min Flutter version". +allowed-tools: + - Bash + - Read + - Edit + - Write + - Grep + - Glob +--- + +# flutter-version-bump + +Two different jobs share the phrase "bump Flutter". Decide which one you are doing **before** touching a file — +they produce different diffs, different review burdens, and only one of them changes what consumers can resolve. + +| Track | Goal | Scope | Consumer impact | +|---|---|---|---| +| **A — Compat** (first) | Make CI green on the new stable | Source fixes, lint config, goldens | None | +| **B — Floor raise** (policy-driven) | Move the minimum to latest − 1 | 6 pubspecs + `melos.yaml` + `legacy_version_analyze.yml` + CHANGELOGs + newly-activated lints | Apps below the floor stop resolving | + +A floor raise is **not** a breaking change: no `!` in the commit/PR title, and the CHANGELOG bullet does not go +under `### 🛑 Breaking / Removals`. Existing code keeps compiling; older SDKs simply stop resolving the new +version. Do not invent a `!` for it. + +**Do Track A first, always.** "CI broke after the new Flutter came out" is Track A, and it must land green before +Track B starts — otherwise you cannot tell a floor-raise failure from a new-stable failure. + +**Then check whether Track B is due.** The SDK's policy is **minimum supported = latest stable − 1**, so a new +stable makes the floor raise *routine, not exceptional*: when 3.47 shipped, the floor moved 3.38 → **3.44**. Pair +each Flutter minor with its Dart SDK (3.44 → Dart 3.12, 3.47 → Dart 3.13); `fvm releases` lists both, and the +authoritative list is +`https://storage.googleapis.com/flutter_infra_release/releases/releases_macos.json` (`fvm`'s cache goes stale). + +Track B belongs in a minor/major release rather than a hotfix, and must be its own commit — its diff has nothing +to do with the compat fixes. + +Only **two** version knobs move on a floor raise here, and they are both the *floor*, never the new stable: + +| Knob | Value | +|---|---| +| `melos.yaml` `command.bootstrap.environment` | the new floor — source of truth, `melos bs` propagates to every pubspec | +| `.github/workflows/legacy_version_analyze.yml` `env.flutter_version` | the new floor — this job *is* the floor's regression test | + +> The repo has **no `.fvmrc`** — local Flutter selection is not pinned here. Do not create one as a side effect of +> a version bump; that is its own decision. It does mean nothing forces a developer's local toolchain to be the +> floor, so newer-than-floor API usage is caught only by `legacy_version_analyze`, which analyzes +> **`stream_core/lib` only** (see below). Analyse against the floor by hand. + +## Why CI breaks the day a Flutter stable ships + +`.github/workflows/stream_core_flutter_workflow.yml` installs Flutter with + +```yaml +channel: ${{ env.FLUTTER_CHANNEL }} # stable — no version pin +``` + +so both jobs (`analyze` and `test`) **auto-adopt the new stable within hours of release**. +`legacy_version_analyze.yml` (the N-1 canary) pins a version and does *not* follow. So the first symptom is always +"CI went red and nobody changed anything", while a local machine on an older SDK still passes. + +`melos run analyze` runs `dart analyze --fatal-infos` (examples excluded). New SDKs ship new diagnostics as +**infos and warnings**, which `--fatal-infos` turns into hard failures. That is why an SDK bump hurts here more +than in a typical repo. + +### Check the canary first — it usually already told you + +`beta_version_analyze.yml` runs the `package_analysis` action against the **beta** channel every Monday and Slacks +on failure. Beta becomes stable roughly a quarter later, so this workflow reports the next release's analyzer +failures *months* early. Before investigating anything, read its history: + +```bash +gh run list --workflow=beta_version_analyze.yml --limit 10 +gh run view --log-failed | grep -E "warning -|info -|error" +``` + +Two traps when reading it, both because `.github/actions/package_analysis/action.yml` is a **one-package action**: + +```bash +cd packages/stream_core/lib && dart analyze --fatal-warnings . && cd .. && flutter test --exclude-tags golden +``` + +- **It only covers `stream_core`.** `stream_core_flutter`, `stream_thumbnail` and the gallery are never analysed + by the canary or by `legacy_version_analyze`. A green canary says nothing about the Flutter packages. +- **It is `--fatal-warnings`, not `--fatal-infos`,** and `lib/` only. New *infos* — the bulk of what a new SDK + ships — pass the canary and fail `melos run analyze`. + +If the canary has been red and unactioned for weeks, that is the most valuable finding in the exercise — report it +separately from the code fixes. + +## Step 1 — Branch off main + +Never branch a toolchain bump off a feature branch. Bootstrap rewrites lockfiles repo-wide. + +```bash +git fetch origin +git checkout -b chore/flutter- origin/main +``` + +## Step 2 — Install the new SDK side by side + +Keep the old one. Every claim below is an A/B comparison, and you cannot make one with a single toolchain. + +```bash +fvm install # e.g. 3.47.0 +fvm list # confirm old + new are both cached +NEW=~/fvm/versions/ +OLD=~/fvm/versions/ +``` + +On a floor raise you want **three**: the old floor, the new floor (must analyse clean — it is what +`legacy_version_analyze` will run) and the latest stable (what CI's `analyze` job actually resolves to). + +## Step 3 — Measure before you fix + +The single most important habit: **never attribute a failure to the new SDK without seeing the old SDK pass it.** +Repos accumulate drift; a feature branch may already be dirty; a local `build/` directory can inject hundreds of +phantom issues. Run each check under both toolchains and diff. + +### Format + +CI runs `melos run format:verify` → `dart format --set-exit-if-changed .` from the root, which walks untracked +trees too. Scope to tracked files so `build/` noise cannot pollute the comparison: + +```bash +git ls-files '*.dart' > /tmp/dartfiles.txt +for V in $OLD $NEW; do + echo "== $V" + $V/bin/cache/dart-sdk/bin/dart format --output=none --set-exit-if-changed $(cat /tmp/dartfiles.txt) 2>&1 | tail -3 +done +``` + +The root `analysis_options.yaml` sets `page_width: 120` and `trailing_commas: preserve`; `dart format` reads it +from the file nearest the target, so always format from the repo root, never from inside a package with its own +options file. + +Interpretation: + +- **Both 0 changed** → the formatter did not change. Do not touch formatting in this PR. +- **New > 0, old = 0** → `dart_style` changed. Apply it as an **isolated commit** touching nothing else, so the + real fixes stay reviewable. +- **Old > 0, new = 0** → the repo is already formatted for a newer formatter than the floor. Pre-existing drift, + harmless. Not yours to fix here — but mention it. + +### Analyze + +> **Run `melos bootstrap` first, and re-run it after every pubspec edit.** `dart analyze` reads the *language +> version* from `.dart_tool/package_config.json`, written by `pub get` — **not** from `pubspec.yaml`. A stale +> `.dart_tool` reports a confidently clean result that CI will not reproduce, and editing an SDK constraint +> without re-bootstrapping changes nothing at all. Verify with: +> +> ```bash +> python3 -c "import json;d=json.load(open('packages/stream_core_flutter/.dart_tool/package_config.json'));\ +> print([p.get('languageVersion') for p in d['packages'] if p['name']=='stream_core_flutter'])" +> ``` +> +> Despite the root `pubspec.yaml` being named `stream_core_flutter_workspace`, this is **not** a pub workspace — +> no package declares `resolution: workspace`. Melos bootstraps each package separately, so every package has its +> own `.dart_tool/package_config.json` and you must check the one you care about. + +Mirror `melos run analyze` (`--fatal-infos`, examples excluded) and **filter local build artifacts**, which are +not in CI and will otherwise bury the real signal: + +```bash +set -o pipefail # otherwise a matching grep masks an analyzer that crashed +for V in $OLD $NEW; do + echo "##### $V" + for p in packages/stream_core packages/stream_core_flutter packages/stream_thumbnail apps/design_system_gallery; do + echo "### $p" + # `|| true` so a package with no diagnostics is not reported as a failure + (cd "$p" && $V/bin/cache/dart-sdk/bin/dart analyze --fatal-infos . 2>&1 \ + | { grep -E "^\s+(info|warning|error)" | grep -v " build/" || true; }) + done +done +``` + +Everything present under `$NEW` and absent under `$OLD` is your work list. Everything in both is pre-existing — +leave it alone and say so. + +### Tests and goldens — `GITHUB_ACTIONS=true` is mandatory + +`packages/stream_core_flutter/test/flutter_test_config.dart` switches alchemist on **`GITHUB_ACTIONS` only** — +not the more common `CI`. Setting `CI=true` looks right and silently runs the *platform* variant instead: + +```dart +final isRunningInCi = Platform.environment.containsKey('GITHUB_ACTIONS'); +ciGoldensConfig: CiGoldensConfig(enabled: isRunningInCi), +platformGoldensConfig: PlatformGoldensConfig(enabled: !isRunningInCi), +``` + +**Only `goldens/ci/` is committed** (47 files). A bare `flutter test` on your machine runs the platform variant, +whose goldens do not exist in the repo, and fails every golden test for reasons that have nothing to do with the +new SDK. Always: + +```bash +for V in $OLD $NEW; do + (cd packages/stream_core_flutter && GITHUB_ACTIONS=true $V/bin/flutter test --reporter=compact > /tmp/t-$(basename $V).log 2>&1) +done +# compare the failure sets, not the counts — `\r` matters, the compact reporter uses it +for V in $OLD $NEW; do + tr '\r' '\n' < /tmp/t-$(basename $V).log | grep -E '\[E\]$' | sed 's|.*/test/|test/|' | sort -u \ + > /tmp/fail-$(basename $V).txt +done +comm -13 /tmp/fail-$(basename $OLD).txt /tmp/fail-$(basename $NEW).txt # caused by the new SDK +comm -12 /tmp/fail-$(basename $OLD).txt /tmp/fail-$(basename $NEW).txt # pre-existing, out of scope +``` + +Repeat for `packages/stream_core` (pure Dart, no goldens). + +Committed goldens are Linux-rendered, so some fail on macOS even on the old SDK. That baseline noise is exactly +what `comm` separates out. **Only the lines the new SDK adds are yours.** + +> **Never run `git checkout -- .` between runs.** Alchemist only writes untracked `failures/*.png` directories, +> so there is nothing to revert — and a blanket checkout silently destroys the source fixes you just made. Clean +> up with `git clean -fd -- '*/failures'` instead, and keep `git status --short` in view. + +### Barrels + +`melos run check:barrels` gates the PR alongside analyze. It is not toolchain-sensitive on its own, but a fix +that moves or adds a file under `packages/stream_core_flutter/lib/src/` breaks it. Run it in the same breath as +analyze. + +## Step 4 — Fix, by failure class + +Work the diff from Step 3. Known classes and this repo's chosen remedy: + +### New analyzer diagnostics (the usual bulk) + +New SDKs add diagnostics that `--fatal-infos` promotes to failures. Treat each as a real finding first — most of +them point at a genuine latent bug — and only suppress when the diagnostic is wrong about this code. + +| Remedy | When | +|---|---| +| Fix the code | Default. The diagnostic is usually right. | +| `// ignore: ` with a one-line reason above it | The diagnostic is correct in general but wrong here, or the fix belongs to an upstream package. Never a bare ignore — see `STYLE_GUIDE.md`. | +| Flip the rule to `false` in `analysis_options.yaml`, or delete it from `all_lint_rules.yaml` | The lint was **removed or renamed** by the SDK. An unrecognized rule name is itself a warning. | + +The lint config here is inverted relative to most repos: `all_lint_rules.yaml` is an **explicit list of every +rule**, included wholesale, and `analysis_options.yaml` then switches individual rules back to `false` with a +comment saying why. Two consequences: + +- **A new SDK's new rules do not activate on their own.** The list is static, so adopting them means adding the + names to `all_lint_rules.yaml` — a code-style decision with its own before/after numbers, and **always its own + PR**, never this one. +- **A removed or renamed rule fires `undefined_lint` in `all_lint_rules.yaml`, not in `analysis_options.yaml`.** + Delete it there; if `analysis_options.yaml` also disables it, delete that line too or it becomes the next + `undefined_lint`. + +To find removed/renamed/deprecated lints mechanically rather than by guessing, analyse the options files +themselves — `melos run analyze` never does, because both sit at the repo **root**, outside every melos package: + +```bash +for V in $OLD $NEW; do + echo "== $V"; $V/bin/cache/dart-sdk/bin/dart analyze --fatal-infos analysis_options.yaml all_lint_rules.yaml +done +``` + +`undefined_lint` means the rule was removed or renamed — a hard failure once the root is analysed. +`deprecated_lint` means it still parses but is on its way out — cheaper to drop now than to discover as +`undefined_lint` two releases later. Run it under both toolchains: a `deprecated_lint` that also fires on the old +SDK is pre-existing debt, not something this release introduced. + +### Framework deprecations + +New `deprecated_member_use` infos are fatal here. Prefer migrating to the replacement API. If the replacement does +not exist on the floor in `melos.yaml`, you cannot use it — suppress with a scoped ignore naming the reason, and +leave the migration for the release that raises the floor. + +### New runtime assertions + +Flutter adds asserts that only fire in tests, so they surface as widget-test failures, not analyzer output. Read +the assertion and fix the widget tree; do not silence the test. + +### Golden pixel drift + +Small diffs (well under 1%) across unrelated widgets mean the engine's rasterisation changed — legitimate, and the +goldens must be regenerated. Larger diffs confined to one widget family usually mean a real layout change; +investigate before regenerating. **On a Track B floor raise, goldens should not move at all** — CI still runs the +latest stable either way. If they do, that is a signal to investigate, not to regenerate. + +**Goldens are always regenerated by the CI workflow — never locally. No exceptions.** + +`melos run update:goldens` writes the *platform* variant on your machine; the committed `goldens/ci/*.png` are +Linux-rendered. Regenerating locally therefore produces macOS pixels the repo does not even track. Locally you may +**compare** (`GITHUB_ACTIONS=true flutter test`) to see which goldens moved — never write them. + +`update_goldens.yml` checks out with `secrets.BOT_SSH_PRIVATE_KEY` and commits `**/test/**/goldens/*.png` back to +whatever branch you dispatch against, so **push the branch first**. It has no inputs — it always regenerates +everything, on `ubuntu-latest`, with `flutter-version: "3.x"` (the new stable, automatically). + +> **Confirm with the user before running this.** It pushes a branch and dispatches a workflow that writes a commit +> to the remote. It is the one outward-facing action in this skill — never dispatch it unprompted, and expect the +> branch to stay red on goldens until it has run. + +```bash +git push -u origin chore/flutter- +gh workflow run update_goldens.yml --ref chore/flutter- +gh run watch $(gh run list --workflow=update_goldens.yml --limit 1 --json databaseId --jq '.[0].databaseId') +git pull # pick up the bot's "chore: Update Goldens" commit +``` + +Two consequences to state plainly when you report: + +- **The branch is not verifiable-green on macOS.** Even after regeneration, a local `GITHUB_ACTIONS=true` run + still shows the pre-existing Linux-vs-macOS baseline diffs from Step 3. Give the reviewer that number so a + non-zero local failure count is not read as "the fix did not work". +- `legacy_version_analyze.yml` never runs golden tests, so regenerating against the new stable cannot break the + N-1 canary. + +### Files the toolchain rewrites underneath you + +`melos bootstrap` and `flutter pub get` both edit tracked files. Anything they rewrite that you do not commit +fails the **format** job, since `melos run format:verify` runs after bootstrap in CI. + +- **`pubspec.lock`** — the workspace has a single root lock. Commit it. +- **`analyzer.exclude` blocks injected into an app-type `analysis_options.yaml`** — Flutter 3.47's `pub get` + writes `build/`, `android/`, `ios/`, … exclusions into packages that have a `flutter:` SDK dep *and* platform + directories (here: `apps/design_system_gallery`, `packages/stream_thumbnail/example`). It is tool-authored + config, not ours. **Surface it, do not decide it yourself** — and note that `pub get` *merges* its full list + into any existing `exclude:`, so a hand-trimmed version is not a stable fixed point. +- **`test_api: any` / `flutter_test: any` appended to `dev_dependencies`** — melos injects these around bootstrap + and normally strips them again. **Never commit them**: `flutter_test` has no pub.dev version, so committing it + makes the next `melos bootstrap` fail version solving outright. + +There are **no Android/iOS build jobs** in this repo's CI — nothing compiles the gallery or the thumbnail example +for a device. So Gradle/AGP/Kotlin/Xcode floors never gate a PR here, and a new Flutter's raised build-tool floors +are invisible until someone builds locally. Say that rather than implying the platform projects are verified. + +## Step 5 — Verify like CI does + +```bash +melos bootstrap +melos run analyze +melos run check:barrels +melos run format:verify +GITHUB_ACTIONS=true melos run test:all +git status --short # expect only your intended edits +git clean -fdn -- '*/failures' # review, then drop -n to remove alchemist's diff images +``` + +Two things will still look wrong locally and are not: + +- `melos run analyze` surfaces `build/` noise if you have ever built the gallery. Compare against Step 3's + baseline instead of expecting a clean zero. +- `GITHUB_ACTIONS=true melos run test:all` still fails the pre-existing Linux-vs-macOS goldens. Compare failure + **sets**, not counts. + +Also re-check the floor, since `legacy_version_analyze.yml` gates the PR — and remember it covers `stream_core` +only, so extend it by hand to the packages it misses: + +```bash +for p in packages/*/; do (cd "$p/lib" && $FLOOR/bin/cache/dart-sdk/bin/dart analyze --fatal-infos .); done +``` + +A fix that relies on syntax newer than the floor passes on `$NEW` and fails that job. + +## Step 6 — Track B: raise the floor to latest − 1 + +Keep this out of the Track A commit — it changes what consumers can resolve and needs to be reviewable on its own. + +Version-carrying files, all of which must move together. **Derive the list by grep, do not trust this one** — it +is accurate as of the 3.44 raise: + +```bash +git ls-files '*pubspec.yaml' | xargs grep -ln 'sdk: \^3\.' +``` + +- `melos.yaml` — `command.bootstrap.environment.{sdk,flutter}` (the source of truth; `melos bs` propagates) +- `pubspec.yaml` (root workspace) — `environment.sdk` only; the root carries no `flutter` constraint +- `packages/stream_core/pubspec.yaml` — `sdk` only. It is **pure Dart and carries no `flutter` constraint — do + not add one.** +- `packages/stream_core_flutter/pubspec.yaml` — `sdk` + `flutter` +- `packages/stream_thumbnail/pubspec.yaml` and `packages/stream_thumbnail/example/pubspec.yaml` +- `apps/design_system_gallery/pubspec.yaml` +- `.github/workflows/legacy_version_analyze.yml` — `env.flutter_version`. Set it to the **new floor** (never the + new stable): this job exists to prove the floor still analyses clean. Note the existing value may be a *patch* + of the floor (`3.38.10` for a `>=3.38.1` floor) — pick one convention, say which in the commit message. + +Confirm melos actually propagated rather than assuming it: + +```bash +melos bootstrap && git diff --stat -- '*pubspec.yaml' +``` + +Melos only rewrites keys that already exist, so a pubspec missing a `flutter:` key stays missing one — which is +what you want for `stream_core`, and what you must fix by hand anywhere it is wrong. + +### The floor raise activates dormant lints — budget for it + +This is the step that surprises people. Raising the Dart constraint raises each package's **language version**, +and lints stay silent while their suggested fix is not yet expressible. Raise the floor and they all fire at once, +in code nobody touched. + +Concretely, the Dart 3.10 → 3.12 raise activated `prefer_initializing_formals` on 24 sites across +`stream_core`, `stream_core_flutter` and the gallery, because Dart 3.12 legalised `this._privateField` as a named +parameter. Zero issues before, 24 after — none of it caused by the new *stable*, all of it caused by the *floor*. + +So: **`melos bootstrap` and re-analyse immediately after editing the constraints**, before you write the +CHANGELOG. Then let the tooling do the mechanical work: + +```bash +for p in packages/*/ apps/design_system_gallery; do (cd $p && dart fix --dry-run); done +# then, per lint, once you have decided the fix is right: +(cd && dart fix --apply --code=prefer_initializing_formals) +``` + +Two things to check by hand afterwards — `dart fix` is mechanical, not thoughtful: + +- **Doc comments get mangled *and silently deleted*.** It rewrites `[logger]` to `[_logger]` in the doc above the + constructor — leaking a private name into public API docs, when callers still pass the *public* name + (`logger:`, underscore stripped). It also drops any `///` comment attached to the parameter it rewrites. Review + every comment line the refactor touched, in both directions: + + ```bash + git diff -- '*.dart' | grep -E "^[-+]\s*(///|//)" + ``` + + Checking only added lines misses the deletions — that is how a lost doc comment survives review. +- **Confirm no public parameter was renamed.** For every `this._foo` it introduced, the parameter it replaced must + have been named exactly `foo`. A mismatch is a silent breaking change for callers — and in this repo a public + widget or theme constructor is API that downstream SDKs (`stream-chat-flutter`, `stream-video-flutter`) depend + on. + +This repo generates heavily (`json_serializable`, `theme_extensions_builder`). Re-run `melos run generate:all` +afterwards and confirm the generated call sites are unchanged — the generators read constructor parameters, so a +renamed parameter would silently change `.g.dart` / `.g.theme.dart`. + +**Always `dart format` after build_runner.** Generated files carry a `// dart format width=80` marker and are +emitted at 80 columns while the repo's `analysis_options.yaml` sets `page_width: 120`, so a regen can dirty +generated files with pure reflow. Format before concluding codegen "changed" anything. Note the root analyzer +`exclude` covers `packages/*/lib/**/*.*.dart`, so generated files are not analysed — but they *are* formatted. + +Then, per `STYLE_GUIDE.md`, one short bullet under `## Upcoming` in each published package's CHANGELOG: + +```md +- Raised minimum Flutter to `>=X.Y.Z` and Dart SDK to `^A.B.C`. +``` + +`stream_core` is Dart-only — its bullet mentions the Dart SDK only. `apps/design_system_gallery` is not published +and has no CHANGELOG. The style guide's heading list (`### ✨ Features`, `### 🐛 Bug Fixes`, +`### 🛑 Breaking / Removals`, `### ⚠️ Deprecations`) has no "changed" bucket; a floor raise is **not** breaking, so +use `### 🔄 Changed` and update `STYLE_GUIDE.md` if you introduce it. + +Two CI gates read the CHANGELOG and will bite: + +- `semantic_changelog_update` in `pr_title.yml` maps scopes `llc`/`ui`/`thumb` to packages. A `chore(repo):` PR is + outside that map, so it is not *required* to touch a changelog — but if you do touch one, `changelog_placement` + requires the entries to be under `## Upcoming`, not under an already-published version heading. +- A package that was just released may have **no `## Upcoming` section at all**. Add one at the top rather than + filing the bullet under the released version — that is precisely what `changelog_placement` fails on. + +Finish with `melos bootstrap` and commit the resulting root `pubspec.lock`. + +## Step 7 — Changelog and PR + +Track A changes that are user-visible (a widget swapped, a deprecation migrated) get a CHANGELOG bullet in the +affected package. Pure CI/tooling/golden churn does not. + +PR title follows Conventional Commits with a **required scope** from `llc` / `ui` / `repo` / `thumb` +(`pr_title.yml` enforces it): + +- Track A → `chore(repo): support Flutter ` +- Track B → `chore(repo): bump min Flutter to and Dart SDK to ` + +## Report back with attribution + +When summarising, always separate the three buckets — it is the difference between a reviewable PR and a mystery: + +1. **Caused by the new SDK** (present on new, absent on old) — what this PR fixes. +2. **Pre-existing** (present on both) — explicitly out of scope, named so nobody re-investigates. +3. **Local-only noise** (`build/` artifacts, platform goldens without `GITHUB_ACTIONS=true`) — never appears in + CI, never fix. + +Say plainly what you could not verify. Here that is a short list — there are no build jobs — but +`legacy_version_analyze` covering only `stream_core` means "the canary is green" is a much weaker claim than it +sounds. From c1c71362a2221a3783e9e59c69e68bf1a7fa7f32 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 17 Aug 2026 12:11:41 +0200 Subject: [PATCH 02/10] chore(repo): bump min Flutter to 3.44.0 and Dart SDK to 3.12.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flutter 3.47.0 went stable on 2026-08-12, so the "minimum supported = latest stable - 1" policy makes 3.44.0 (Dart 3.12.0) the new floor, up from 3.38.1 / 3.10.0. This is Track B only: CI already runs the new stable and is green on it, so no compat work was needed first. A floor raise is not breaking — existing code keeps compiling, older SDKs simply stop resolving the new version — so there is no `!` and the bullets go under a `🔄 Changed` heading. That heading had no entry in STYLE_GUIDE's list, which covered only features, fixes and removals; it is added there with the rule that a floor raise belongs under it rather than under `🛑 Breaking / Removals`. Two knobs move in lockstep: `melos.yaml`, which is the source of truth and propagates to all five package pubspecs on bootstrap, and `legacy_version_analyze.yml`, whose `flutter_version` is the floor's own regression test. That job was pinned to `3.38.10`, a patch of the old floor rather than the floor itself; it now names the floor exactly, so what CI proves is what the pubspecs claim. Raising the Dart constraint raises each package's language version, which activated `prefer_initializing_formals` on 24 previously-silent sites: Dart 3.12 legalised `this._privateField` as a named parameter, so the lint's suggested fix only became expressible now. Zero issues before the constraint moved, 24 after — caused by the floor, not by the new stable. Applied with `dart fix --apply --code=prefer_initializing_formals` and then audited by hand, since the fix is mechanical: - It rewrote five doc-comment references to the private field name (`[cdn]` -> `[_cdn]`), which would have leaked private names into public API docs while callers still pass the public name. Reverted; the diff now touches no comment line in either direction. - Every introduced `this._foo` replaced a parameter named exactly `foo`, so no public parameter was renamed. `melos run generate:all` produces no diff, confirming the generators still see the same names. Verified at both ends: `dart analyze --fatal-infos` is clean across all four packages on 3.47.0 (what CI resolves) and reports only one pre-existing info on 3.44.0 (the floor). `stream_core` 317 tests and `stream_thumbnail` 10 tests pass; `stream_core_flutter` fails the same 42 macOS-vs-Linux goldens as before the change, an identical set, so no golden was regenerated. Barrels and formatting pass. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/legacy_version_analyze.yml | 2 +- STYLE_GUIDE.md | 9 +++++++ .../lib/config/theme_configuration.dart | 4 +-- apps/design_system_gallery/pubspec.yaml | 4 +-- melos.yaml | 4 +-- packages/stream_core/CHANGELOG.md | 4 +++ .../src/api/system_environment_manager.dart | 4 +-- .../uploader/attachment_uploader.dart | 4 +-- .../lib/src/user/token_manager.dart | 4 +-- .../engine/stream_web_socket_engine.dart | 8 +++--- .../connection_recovery_handler.dart | 3 +-- .../ws/client/web_socket_health_monitor.dart | 4 +-- packages/stream_core/pubspec.yaml | 2 +- packages/stream_core_flutter/CHANGELOG.md | 6 +++++ .../common/stream_intrinsic_flex.dart | 26 +++++++------------ .../components/common/stream_safe_area.dart | 7 +++-- .../common/stream_tap_target_padding.dart | 11 +++----- .../src/components/sheet/stream_sheet.dart | 7 +++-- .../src/factory/stream_component_factory.dart | 4 +-- packages/stream_core_flutter/pubspec.yaml | 4 +-- packages/stream_thumbnail/CHANGELOG.md | 6 +++++ .../stream_thumbnail/example/pubspec.yaml | 4 +-- packages/stream_thumbnail/pubspec.yaml | 4 +-- pubspec.lock | 2 +- pubspec.yaml | 2 +- 25 files changed, 74 insertions(+), 65 deletions(-) diff --git a/.github/workflows/legacy_version_analyze.yml b/.github/workflows/legacy_version_analyze.yml index 32374d25..76940ddf 100644 --- a/.github/workflows/legacy_version_analyze.yml +++ b/.github/workflows/legacy_version_analyze.yml @@ -3,7 +3,7 @@ name: legacy_version_analyze env: # Note: The versions below should be manually updated after a new stable # version comes out. - flutter_version: "3.38.10" + flutter_version: "3.44.0" on: push: diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index d124e397..4efd5389 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -1352,12 +1352,21 @@ sub-headings: - Fixed a crash when opening the media viewer with an empty attachments list. +### 🔄 Changed + +- Raised the minimum Flutter version to `>=3.44.0` and the Dart SDK to `^3.12.0`. + ### 🛑 Breaking / Removals - Removed `StreamCoreMessageComposer`. Use `StreamMessageComposer` from `stream_chat_flutter` instead. ``` +`### 🔄 Changed` covers what is neither new API nor a fix and does not break +existing code — a raised minimum Flutter/Dart version, a tightened dependency +constraint, a changed default. A raised floor is **not** breaking: code keeps +compiling, older SDKs simply stop resolving the new version. + Prefer **one short bullet** per entry, describing the functional change. Longer entries are acceptable for user-visible multi-facet features where the extra context matters to someone deciding whether to upgrade — but avoid sub-bullets, diff --git a/apps/design_system_gallery/lib/config/theme_configuration.dart b/apps/design_system_gallery/lib/config/theme_configuration.dart index 733359e7..3e162514 100644 --- a/apps/design_system_gallery/lib/config/theme_configuration.dart +++ b/apps/design_system_gallery/lib/config/theme_configuration.dart @@ -7,8 +7,8 @@ import 'package:stream_core_flutter/core.dart'; /// exact naming conventions from [StreamColorScheme]. class ThemeConfiguration extends ChangeNotifier { ThemeConfiguration({ - Brightness brightness = Brightness.light, - }) : _brightness = brightness { + this._brightness = Brightness.light, + }) { _rebuildTheme(); } diff --git a/apps/design_system_gallery/pubspec.yaml b/apps/design_system_gallery/pubspec.yaml index c3fb8d38..d9af0fac 100644 --- a/apps/design_system_gallery/pubspec.yaml +++ b/apps/design_system_gallery/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ^3.10.0 - flutter: ">=3.38.1" + sdk: ^3.12.0 + flutter: ">=3.44.0" dependencies: device_frame_plus: ^1.0.0 diff --git a/melos.yaml b/melos.yaml index 17b97ecb..adda26e6 100644 --- a/melos.yaml +++ b/melos.yaml @@ -12,9 +12,9 @@ command: bootstrap: # Dart and Flutter environment used in the project. environment: - sdk: ^3.10.0 + sdk: ^3.12.0 # We are not using carat '^' syntax here because flutter don't follow semantic versioning. - flutter: ">=3.38.1" + flutter: ">=3.44.0" # List of all the dependencies used in the project. dependencies: diff --git a/packages/stream_core/CHANGELOG.md b/packages/stream_core/CHANGELOG.md index 7e039175..3a90f784 100644 --- a/packages/stream_core/CHANGELOG.md +++ b/packages/stream_core/CHANGELOG.md @@ -4,6 +4,10 @@ - Added `teams` field to `User` class. +### 🔄 Changed + +- Raised the minimum Dart SDK to `^3.12.0`. + ## 0.4.0 ### 💥 BREAKING CHANGES diff --git a/packages/stream_core/lib/src/api/system_environment_manager.dart b/packages/stream_core/lib/src/api/system_environment_manager.dart index 086e497c..96986bdc 100644 --- a/packages/stream_core/lib/src/api/system_environment_manager.dart +++ b/packages/stream_core/lib/src/api/system_environment_manager.dart @@ -8,8 +8,8 @@ import 'system_environment.dart'; class SystemEnvironmentManager { /// {@macro systemEnvironmentManager} SystemEnvironmentManager({ - required SystemEnvironment environment, - }) : _environment = environment; + required this._environment, + }); /// Returns the Stream client user agent string based on the current /// [environment] value. diff --git a/packages/stream_core/lib/src/attachment/uploader/attachment_uploader.dart b/packages/stream_core/lib/src/attachment/uploader/attachment_uploader.dart index a6603068..a02b1182 100644 --- a/packages/stream_core/lib/src/attachment/uploader/attachment_uploader.dart +++ b/packages/stream_core/lib/src/attachment/uploader/attachment_uploader.dart @@ -53,8 +53,8 @@ class AttachmentUploadException implements Exception { class StreamAttachmentUploader { /// Creates a [StreamAttachmentUploader] with the specified [cdn] client. const StreamAttachmentUploader({ - required CdnClient cdn, - }) : _cdn = cdn; + required this._cdn, + }); // The CDN client used for upload operations. final CdnClient _cdn; diff --git a/packages/stream_core/lib/src/user/token_manager.dart b/packages/stream_core/lib/src/user/token_manager.dart index 288f0d4f..aa25ac07 100644 --- a/packages/stream_core/lib/src/user/token_manager.dart +++ b/packages/stream_core/lib/src/user/token_manager.dart @@ -32,8 +32,8 @@ class TokenManager { /// The [tokenProvider] is used to load tokens when needed. TokenManager({ required this.userId, - required TokenProvider tokenProvider, - }) : _tokenProvider = tokenProvider; + required this._tokenProvider, + }); /// The unique identifier of the user whose tokens are managed. final String userId; diff --git a/packages/stream_core/lib/src/ws/client/engine/stream_web_socket_engine.dart b/packages/stream_core/lib/src/ws/client/engine/stream_web_socket_engine.dart index 7a705969..f702ff6f 100644 --- a/packages/stream_core/lib/src/ws/client/engine/stream_web_socket_engine.dart +++ b/packages/stream_core/lib/src/ws/client/engine/stream_web_socket_engine.dart @@ -34,11 +34,9 @@ class StreamWebSocketEngine implements WebSocketEngine { /// Creates a new instance of [StreamWebSocketEngine]. StreamWebSocketEngine({ WebSocketProvider? wsProvider, - WebSocketEngineListener? listener, - required WebSocketMessageCodec messageCodec, - }) : _wsProvider = wsProvider ?? _createWebSocket, - _messageCodec = messageCodec, - _listener = listener; + this._listener, + required this._messageCodec, + }) : _wsProvider = wsProvider ?? _createWebSocket; final WebSocketProvider _wsProvider; final WebSocketMessageCodec _messageCodec; diff --git a/packages/stream_core/lib/src/ws/client/reconnect/connection_recovery_handler.dart b/packages/stream_core/lib/src/ws/client/reconnect/connection_recovery_handler.dart index eb89c0dd..55c4a0af 100644 --- a/packages/stream_core/lib/src/ws/client/reconnect/connection_recovery_handler.dart +++ b/packages/stream_core/lib/src/ws/client/reconnect/connection_recovery_handler.dart @@ -39,12 +39,11 @@ class ConnectionRecoveryHandler extends Disposable { required StreamWebSocketClient client, NetworkStateProvider? networkStateProvider, LifecycleStateProvider? lifecycleStateProvider, - bool keepConnectionAliveInBackground = false, + this._keepConnectionAliveInBackground = false, List? policies, RetryStrategy? retryStrategy, }) : _client = client, _reconnectStrategy = retryStrategy ?? RetryStrategy(), - _keepConnectionAliveInBackground = keepConnectionAliveInBackground, _policies = [ ...?policies, WebSocketAutomaticReconnectionPolicy( diff --git a/packages/stream_core/lib/src/ws/client/web_socket_health_monitor.dart b/packages/stream_core/lib/src/ws/client/web_socket_health_monitor.dart index 940eb7b8..f4299c32 100644 --- a/packages/stream_core/lib/src/ws/client/web_socket_health_monitor.dart +++ b/packages/stream_core/lib/src/ws/client/web_socket_health_monitor.dart @@ -41,10 +41,10 @@ abstract interface class WebSocketHealthListener { class WebSocketHealthMonitor { /// Creates a new instance of [WebSocketHealthMonitor]. WebSocketHealthMonitor({ - required WebSocketHealthListener listener, + required this._listener, this.pingInterval = const Duration(seconds: 25), this.timeoutThreshold = const Duration(seconds: 3), - }) : _listener = listener; + }); /// The interval between ping requests for health checking. final Duration pingInterval; diff --git a/packages/stream_core/pubspec.yaml b/packages/stream_core/pubspec.yaml index aaf62940..c28d1a61 100644 --- a/packages/stream_core/pubspec.yaml +++ b/packages/stream_core/pubspec.yaml @@ -16,7 +16,7 @@ repository: https://github.com/GetStream/stream-core-flutter # 2. Add it to the melos.yaml file for future updates. environment: - sdk: ^3.10.0 + sdk: ^3.12.0 dependencies: collection: ^1.19.0 diff --git a/packages/stream_core_flutter/CHANGELOG.md b/packages/stream_core_flutter/CHANGELOG.md index 88ef9c01..a5cd3c8a 100644 --- a/packages/stream_core_flutter/CHANGELOG.md +++ b/packages/stream_core_flutter/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +### 🔄 Changed + +- Raised the minimum Flutter version to `>=3.44.0` and the Dart SDK to `^3.12.0`. + ## 0.5.0 ### ✨ Features diff --git a/packages/stream_core_flutter/lib/src/components/common/stream_intrinsic_flex.dart b/packages/stream_core_flutter/lib/src/components/common/stream_intrinsic_flex.dart index 95f30402..1b9e9905 100644 --- a/packages/stream_core_flutter/lib/src/components/common/stream_intrinsic_flex.dart +++ b/packages/stream_core_flutter/lib/src/components/common/stream_intrinsic_flex.dart @@ -494,28 +494,20 @@ class _RenderStreamIntrinsicFlex extends RenderBox ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { _RenderStreamIntrinsicFlex({ - required Axis direction, - required MainAxisAlignment mainAxisAlignment, - required MainAxisSize mainAxisSize, - required double spacing, + required this._direction, + required this._mainAxisAlignment, + required this._mainAxisSize, + required this._spacing, required CrossAxisAlignment crossAxisAlignment, - required TextBaseline? textBaseline, - required TextDirection? textDirection, - required VerticalDirection verticalDirection, - required Clip clipBehavior, + required this._textBaseline, + required this._textDirection, + required this._verticalDirection, + required this._clipBehavior, }) : assert( crossAxisAlignment != CrossAxisAlignment.stretch, 'StreamIntrinsicFlex does not support $crossAxisAlignment.', ), - _direction = direction, - _mainAxisAlignment = mainAxisAlignment, - _mainAxisSize = mainAxisSize, - _spacing = spacing, - _crossAxisAlignment = crossAxisAlignment, - _textBaseline = textBaseline, - _textDirection = textDirection, - _verticalDirection = verticalDirection, - _clipBehavior = clipBehavior; + _crossAxisAlignment = crossAxisAlignment; Axis get direction => _direction; Axis _direction; diff --git a/packages/stream_core_flutter/lib/src/components/common/stream_safe_area.dart b/packages/stream_core_flutter/lib/src/components/common/stream_safe_area.dart index 59ec42bf..b19b0485 100644 --- a/packages/stream_core_flutter/lib/src/components/common/stream_safe_area.dart +++ b/packages/stream_core_flutter/lib/src/components/common/stream_safe_area.dart @@ -73,8 +73,8 @@ class StreamSafeArea extends StatelessWidget { /// clamped to `[0, 1]`. const StreamSafeArea.driven({ super.key, - required ValueListenable listenable, - EdgeInsets to = EdgeInsets.zero, + required ValueListenable this._listenable, + this._to = EdgeInsets.zero, this.left = true, this.top = true, this.right = true, @@ -83,8 +83,7 @@ class StreamSafeArea extends StatelessWidget { this.margin = EdgeInsets.zero, this.maintainBottomViewPadding = false, required this.child, - }) : _listenable = listenable, - _to = to; + }); /// Whether to avoid system intrusions on the left ([minimum] and [margin] apply either way). final bool left; diff --git a/packages/stream_core_flutter/lib/src/components/common/stream_tap_target_padding.dart b/packages/stream_core_flutter/lib/src/components/common/stream_tap_target_padding.dart index 29d65790..993552d6 100644 --- a/packages/stream_core_flutter/lib/src/components/common/stream_tap_target_padding.dart +++ b/packages/stream_core_flutter/lib/src/components/common/stream_tap_target_padding.dart @@ -84,14 +84,11 @@ class StreamTapTargetPadding extends SingleChildRenderObjectWidget { class _RenderTapTargetPadding extends RenderShiftedBox { _RenderTapTargetPadding({ - required Size minSize, - required AlignmentGeometry alignment, - required TextDirection? textDirection, + required this._minSize, + required this._alignment, + required this._textDirection, RenderBox? child, - }) : _minSize = minSize, - _alignment = alignment, - _textDirection = textDirection, - super(child); + }) : super(child); Size get minSize => _minSize; Size _minSize; diff --git a/packages/stream_core_flutter/lib/src/components/sheet/stream_sheet.dart b/packages/stream_core_flutter/lib/src/components/sheet/stream_sheet.dart index c972d6bd..2bf2717f 100644 --- a/packages/stream_core_flutter/lib/src/components/sheet/stream_sheet.dart +++ b/packages/stream_core_flutter/lib/src/components/sheet/stream_sheet.dart @@ -656,9 +656,9 @@ class StreamSheetRoute extends PageRoute { super.requestFocus, required this.builder, this.backgroundColor, - Color? barrierColor, + this._barrierColor, this.barrierLabel, - String? barrierOnTapHint, + this._barrierOnTapHint, this.shape, this.borderRadius, this.constraints, @@ -672,8 +672,7 @@ class StreamSheetRoute extends PageRoute { this.onDragEnd, this.parentSheet, this.capturedThemes, - }) : _barrierColor = barrierColor, - _barrierOnTapHint = barrierOnTapHint; + }); /// Builds the primary contents of the sheet. The provided [ScrollController] /// should be attached to the topmost scrollable widget inside the sheet. diff --git a/packages/stream_core_flutter/lib/src/factory/stream_component_factory.dart b/packages/stream_core_flutter/lib/src/factory/stream_component_factory.dart index b332a687..6c215d19 100644 --- a/packages/stream_core_flutter/lib/src/factory/stream_component_factory.dart +++ b/packages/stream_core_flutter/lib/src/factory/stream_component_factory.dart @@ -668,8 +668,8 @@ class StreamComponentBuilders with _$StreamComponentBuilders { final class StreamComponentBuilderExtension { /// Creates a builder extension for a component with Props type [T]. const StreamComponentBuilderExtension({ - required StreamComponentBuilder builder, - }) : _builder = builder; + required this._builder, + }); // The internal builder function that creates the widget from the context and props. final StreamComponentBuilder _builder; diff --git a/packages/stream_core_flutter/pubspec.yaml b/packages/stream_core_flutter/pubspec.yaml index 7a0be962..968e8990 100644 --- a/packages/stream_core_flutter/pubspec.yaml +++ b/packages/stream_core_flutter/pubspec.yaml @@ -4,8 +4,8 @@ version: 0.5.0 homepage: https://github.com/GetStream/stream-core-flutter environment: - sdk: ^3.10.0 - flutter: ">=3.38.1" + sdk: ^3.12.0 + flutter: ">=3.44.0" dependencies: cached_network_image_ce: ^4.9.0 diff --git a/packages/stream_thumbnail/CHANGELOG.md b/packages/stream_thumbnail/CHANGELOG.md index 60732342..6f01f714 100644 --- a/packages/stream_thumbnail/CHANGELOG.md +++ b/packages/stream_thumbnail/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +### 🔄 Changed + +- Raised the minimum Flutter version to `>=3.44.0` and the Dart SDK to `^3.12.0`. + ## 0.1.0 * Initial release. diff --git a/packages/stream_thumbnail/example/pubspec.yaml b/packages/stream_thumbnail/example/pubspec.yaml index e0e6f9cf..8175fce8 100644 --- a/packages/stream_thumbnail/example/pubspec.yaml +++ b/packages/stream_thumbnail/example/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ^3.10.0 - flutter: ">=3.38.1" + sdk: ^3.12.0 + flutter: ">=3.44.0" dependencies: flutter: diff --git a/packages/stream_thumbnail/pubspec.yaml b/packages/stream_thumbnail/pubspec.yaml index 2cd583de..b2183f9c 100644 --- a/packages/stream_thumbnail/pubspec.yaml +++ b/packages/stream_thumbnail/pubspec.yaml @@ -5,8 +5,8 @@ homepage: https://github.com/GetStream/stream-core-flutter repository: https://github.com/GetStream/stream-core-flutter environment: - sdk: ^3.10.0 - flutter: ">=3.38.1" + sdk: ^3.12.0 + flutter: ">=3.44.0" dependencies: cross_file: ^0.3.4+2 diff --git a/pubspec.lock b/pubspec.lock index 1843812b..00115924 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -458,4 +458,4 @@ packages: source: hosted version: "2.2.3" sdks: - dart: ">=3.10.0 <4.0.0" + dart: ">=3.12.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 6e9eabda..098769b0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_core_flutter_workspace environment: - sdk: ^3.10.0 + sdk: ^3.12.0 dev_dependencies: code_builder: ^4.10.1 From 35c6b04a2c954e369f7d4c16e82a6b654001abf6 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 17 Aug 2026 14:23:26 +0200 Subject: [PATCH 03/10] chore(repo): adopt the lint rules Dart 3.13 added MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `all_lint_rules.yaml` is a full enumeration of the linter's rules, so it goes stale on every SDK bump rather than growing on its own. Dart 3.13 added 15 and renamed one (`no_runtimeType_toString` -> `no_runtimetype_tostring`); all are recognised by the 3.47 analyzer, and none reports `undefined_lint` under the 3.44 floor. Three of them fire on existing code, 15 sites in total: - `async_return_with_no_await` (11) — each returned a future from an `async` body without awaiting it. Awaited rather than dropping `async`, which keeps the frame in the stack trace and leaves error timing alone. - `no_dynamic_casts` (3) — a `jsonDecode` result and two platform-channel replies were implicitly cast from `dynamic`. Made explicit; the casts already happened and threw the same way, they were just invisible. - `simple_directive_paths` (1) — `stream_color_scheme.dart` reached its sibling through `../../theme/primitives/`, unlike the two imports beside it. Two rules are disabled rather than adopted: - `unnecessary_await_in_return` directly contradicts `async_return_with_no_await` — it wants the `await` the other one demands, and 6 of the 11 sites above reported both at once. The newer rule wins. - `migrate_design_widgets` guards the `material_ui` migration, which lands separately. On this branch it fires on all 195 `package:flutter/material.dart` imports, so it stays listed and off until that work merges. Verified: analyze, barrels and formatting clean; `stream_core` 317 tests and `stream_thumbnail` 10 pass; `stream_core_flutter` fails the same 42 macOS-vs-Linux goldens as before, an identical set. Co-Authored-By: Claude Opus 5 (1M context) --- all_lint_rules.yaml | 16 +++++++++++++++- analysis_options.yaml | 9 +++++++++ .../common/stream_loading_spinner.dart | 2 +- .../lib/src/api/stream_core_dio_error.dart | 2 +- .../lib/src/attachment/attachment_file.dart | 2 +- .../attachment/uploader/attachment_uploader.dart | 2 +- .../client/engine/stream_web_socket_engine.dart | 2 +- .../reconnect/connection_recovery_handler.dart | 2 +- .../src/theme/semantics/stream_color_scheme.dart | 2 +- .../test/flutter_test_config.dart | 2 +- .../lib/src/stream_thumbnail.dart | 2 +- .../lib/src/stream_thumbnail_method_channel.dart | 10 +++++----- .../lib/stream_thumbnail_web.dart | 2 +- 13 files changed, 39 insertions(+), 16 deletions(-) diff --git a/all_lint_rules.yaml b/all_lint_rules.yaml index 0fb9e4b5..02cbce02 100644 --- a/all_lint_rules.yaml +++ b/all_lint_rules.yaml @@ -7,6 +7,7 @@ linter: - always_use_package_imports - annotate_overrides - annotate_redeclares + - async_return_with_no_await - avoid_annotating_with_dynamic - avoid_bool_literals_in_conditional_expressions - avoid_catches_without_on_clauses @@ -71,15 +72,18 @@ linter: - document_ignores - empty_catches - empty_constructor_bodies + - empty_container_bodies - empty_statements - eol_at_end_of_file - exhaustive_cases - file_names - flutter_style_todos + - future_sync_value - hash_and_equals - implementation_imports - implicit_call_tearoffs - implicit_reopen + - initialize_in_field_declaration - invalid_case_patterns - invalid_runtime_check_with_js_interop_types - join_return_with_assignment @@ -91,16 +95,19 @@ linter: - lines_longer_than_80_chars - literal_only_boolean_expressions - matching_super_parameters + - migrate_design_widgets - missing_code_block_language_in_doc_comment - missing_whitespace_between_adjacent_strings - no_adjacent_strings_in_list - no_default_cases - no_duplicate_case_values + - no_dynamic_casts - no_leading_underscores_for_library_prefixes - no_leading_underscores_for_local_identifiers - no_literal_bool_comparisons - no_logic_in_create_state - - no_runtimeType_toString + - no_raw_types + - no_runtimetype_tostring - no_self_assignments - no_wildcard_variable_uses - non_constant_identifier_names @@ -161,6 +168,7 @@ linter: - remove_deprecations_in_breaking_versions - require_trailing_commas - secure_pubspec_urls + - simple_directive_paths - simplify_variable_pattern - sized_box_for_whitespace - sized_box_shrink_expand @@ -186,6 +194,7 @@ linter: - unnecessary_brace_in_string_interps - unnecessary_breaks - unnecessary_const + - unnecessary_const_in_enum_constructor - unnecessary_constructor_name - unnecessary_final - unnecessary_getters_setters @@ -202,12 +211,15 @@ linter: - unnecessary_nullable_for_final_variable_declarations - unnecessary_overrides - unnecessary_parenthesis + - unnecessary_primary_constructor_body - unnecessary_raw_strings - unnecessary_statements - unnecessary_string_escapes - unnecessary_string_interpolations - unnecessary_this + - unnecessary_this_alias - unnecessary_to_list_in_spreads + - unnecessary_type_name_in_constructor - unnecessary_unawaited - unnecessary_underscores - unreachable_from_main @@ -215,6 +227,7 @@ linter: - unsafe_variance - use_build_context_synchronously - use_colored_box + - use_declaring_parameters - use_decorated_box - use_enums - use_full_hex_values_for_flutter_colors @@ -235,4 +248,5 @@ linter: - use_to_and_as_if_applicable - use_truncating_division - valid_regexps + - var_with_no_type_annotation - void_checks diff --git a/analysis_options.yaml b/analysis_options.yaml index 0ba28266..f1cb4ceb 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -39,6 +39,15 @@ linter: # Single quotes are easier to type and don't compromise on readability. prefer_double_quotes: false + # Conflicts with `async_return_with_no_await`, which wants the `await` this + # rule wants removed. Awaiting keeps the frame in the stack trace, so the + # newer rule wins and this one is off. + unnecessary_await_in_return: false + + # Enabled once the packages move to `material_ui`; until then it fires on + # every `package:flutter/material.dart` import in the repo. + migrate_design_widgets: false + # Conflicts with `omit_local_variable_types` and other rules. # As per Dart guidelines, we want to avoid unnecessary types to make the code # more readable. diff --git a/apps/design_system_gallery/lib/components/common/stream_loading_spinner.dart b/apps/design_system_gallery/lib/components/common/stream_loading_spinner.dart index f9574997..1fc849b9 100644 --- a/apps/design_system_gallery/lib/components/common/stream_loading_spinner.dart +++ b/apps/design_system_gallery/lib/components/common/stream_loading_spinner.dart @@ -83,7 +83,7 @@ class _AnimatedSpinnerState extends State<_AnimatedSpinner> with SingleTickerPro if (!mounted) return; _controller.reset(); - return _controller.forward(); + return await _controller.forward(); } @override diff --git a/packages/stream_core/lib/src/api/stream_core_dio_error.dart b/packages/stream_core/lib/src/api/stream_core_dio_error.dart index fb0d3455..1767ea83 100644 --- a/packages/stream_core/lib/src/api/stream_core_dio_error.dart +++ b/packages/stream_core/lib/src/api/stream_core_dio_error.dart @@ -25,7 +25,7 @@ extension StreamDioExceptionExtension on DioException { final apiErrorResult = runSafelySync( () => switch (response?.data) { final Map data => StreamApiError.fromJson(data), - final String data => StreamApiError.fromJson(jsonDecode(data)), + final String data => StreamApiError.fromJson(jsonDecode(data) as Map), _ => null, }, ); diff --git a/packages/stream_core/lib/src/attachment/attachment_file.dart b/packages/stream_core/lib/src/attachment/attachment_file.dart index 35b4d8aa..63ee4f50 100644 --- a/packages/stream_core/lib/src/attachment/attachment_file.dart +++ b/packages/stream_core/lib/src/attachment/attachment_file.dart @@ -182,7 +182,7 @@ extension AttachmentFileMultipartExtension on AttachmentFile { ); final multipartFile = result.getOrNull(); - if (multipartFile != null) return multipartFile; + if (multipartFile != null) return await multipartFile; // Fallback to byte-based creation (web platforms, inaccessible paths) final bytes = await readAsBytes(); diff --git a/packages/stream_core/lib/src/attachment/uploader/attachment_uploader.dart b/packages/stream_core/lib/src/attachment/uploader/attachment_uploader.dart index a02b1182..dd1cf0db 100644 --- a/packages/stream_core/lib/src/attachment/uploader/attachment_uploader.dart +++ b/packages/stream_core/lib/src/attachment/uploader/attachment_uploader.dart @@ -84,7 +84,7 @@ class StreamAttachmentUploader { ), ); - return result.fold( + return await result.fold( onSuccess: (data) { final uploaded = UploadedAttachment( id: attachment.id, diff --git a/packages/stream_core/lib/src/ws/client/engine/stream_web_socket_engine.dart b/packages/stream_core/lib/src/ws/client/engine/stream_web_socket_engine.dart index f702ff6f..3a3b272a 100644 --- a/packages/stream_core/lib/src/ws/client/engine/stream_web_socket_engine.dart +++ b/packages/stream_core/lib/src/ws/client/engine/stream_web_socket_engine.dart @@ -65,7 +65,7 @@ class StreamWebSocketEngine implements WebSocketEngine { onError: _listener?.onError, ); - return _ws?.ready.then((_) => _listener?.onOpen()); + return await _ws?.ready.then((_) => _listener?.onOpen()); }); } diff --git a/packages/stream_core/lib/src/ws/client/reconnect/connection_recovery_handler.dart b/packages/stream_core/lib/src/ws/client/reconnect/connection_recovery_handler.dart index 55c4a0af..9b534887 100644 --- a/packages/stream_core/lib/src/ws/client/reconnect/connection_recovery_handler.dart +++ b/packages/stream_core/lib/src/ws/client/reconnect/connection_recovery_handler.dart @@ -158,6 +158,6 @@ class ConnectionRecoveryHandler extends Disposable { Future dispose() async { _cancelReconnection(); await _subscriptions.dispose(); - return super.dispose(); + return await super.dispose(); } } diff --git a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart index 3f8ad7d3..1308b05e 100644 --- a/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart +++ b/packages/stream_core_flutter/lib/src/theme/semantics/stream_color_scheme.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:stream_core/stream_core.dart' show Standard; import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart'; -import '../../theme/primitives/stream_colors.dart'; import '../primitives/internal/tokens/dark/stream_tokens.dart' as dark_tokens; import '../primitives/internal/tokens/light/stream_tokens.dart' as light_tokens; +import '../primitives/stream_colors.dart'; part 'stream_color_scheme.g.theme.dart'; diff --git a/packages/stream_core_flutter/test/flutter_test_config.dart b/packages/stream_core_flutter/test/flutter_test_config.dart index b689d05b..fe5e06e7 100644 --- a/packages/stream_core_flutter/test/flutter_test_config.dart +++ b/packages/stream_core_flutter/test/flutter_test_config.dart @@ -6,7 +6,7 @@ import 'package:alchemist/alchemist.dart'; Future testExecutable(FutureOr Function() testMain) async { final isRunningInCi = Platform.environment.containsKey('GITHUB_ACTIONS'); - return AlchemistConfig.runWithConfig( + return await AlchemistConfig.runWithConfig( config: AlchemistConfig( // Enable golden tests for CI environments and disable them for local environments. ciGoldensConfig: CiGoldensConfig(enabled: isRunningInCi), diff --git a/packages/stream_thumbnail/lib/src/stream_thumbnail.dart b/packages/stream_thumbnail/lib/src/stream_thumbnail.dart index eed9dcb3..dbbdd8ce 100644 --- a/packages/stream_thumbnail/lib/src/stream_thumbnail.dart +++ b/packages/stream_thumbnail/lib/src/stream_thumbnail.dart @@ -29,7 +29,7 @@ abstract final class StreamThumbnail { }) async { if (videos.isEmpty) return []; - return StreamThumbnailPlatform.instance.thumbnailFiles( + return await StreamThumbnailPlatform.instance.thumbnailFiles( videos: videos, headers: headers, thumbnailPath: thumbnailPath, diff --git a/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart b/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart index ed0f8f39..115e7491 100644 --- a/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart +++ b/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart @@ -154,7 +154,7 @@ class MethodChannelStreamThumbnail extends StreamThumbnailPlatform { try { final result = await methodChannel.invokeMethod('files', reqMap); if (result != true) { - _resolveFuture(callId, result); + _resolveFuture(callId, result as Object); } } catch (_) { // Drop the pending completer so it doesn't linger in `_futures`. @@ -162,7 +162,7 @@ class MethodChannelStreamThumbnail extends StreamThumbnailPlatform { rethrow; } - return completer.future; + return await completer.future; } @override @@ -202,7 +202,7 @@ class MethodChannelStreamThumbnail extends StreamThumbnailPlatform { rethrow; } - return completer.future; + return await completer.future; } @override @@ -231,13 +231,13 @@ class MethodChannelStreamThumbnail extends StreamThumbnailPlatform { try { final result = await methodChannel.invokeMethod('data', reqMap); if (result != true) { - _resolveFuture(callId, result); + _resolveFuture(callId, result as Object); } } catch (_) { _futures.remove(callId); rethrow; } - return completer.future; + return await completer.future; } } diff --git a/packages/stream_thumbnail/lib/stream_thumbnail_web.dart b/packages/stream_thumbnail/lib/stream_thumbnail_web.dart index f08c0abe..bee98515 100644 --- a/packages/stream_thumbnail/lib/stream_thumbnail_web.dart +++ b/packages/stream_thumbnail/lib/stream_thumbnail_web.dart @@ -262,7 +262,7 @@ class StreamThumbnailWeb extends StreamThumbnailPlatform { // Bound a source that never fires `seeked`/`error` so the future can't hang // (and retain the video element) forever, and release the media element once // the result settles. - return completer.future + return await completer.future .timeout( const Duration(seconds: 30), onTimeout: () => throw PlatformException( From e1861a5d522a32cf784b809c6c59d99ae0d26d34 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 17 Aug 2026 14:31:12 +0200 Subject: [PATCH 04/10] chore(repo): keep unnecessary_await_in_return and drop the rule that fought it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit resolved the clash between `async_return_with_no_await` and `unnecessary_await_in_return` the wrong way round: it disabled the rule the repo already had and rewrote 11 `return future` statements into `return await future` to satisfy the new one. Reverses that. `unnecessary_await_in_return` goes back to enabled and the 11 rewrites are undone, so the code is exactly as it was; the new rule is listed but off. Awaiting only pays for itself inside a `try`, where it decides whether the function catches its own errors — none of the 11 sites is in one, so all the rewrite bought was a stack frame. It also dragged the change through platform-channel and web code that no test covers, which is what dropped patch coverage to 16% on the previous push. What the new rules genuinely caught — three implicit `dynamic` casts and one needlessly indirect import path — is unaffected and stays. Co-Authored-By: Claude Opus 5 (1M context) --- .../lib/components/common/stream_loading_spinner.dart | 2 +- packages/stream_core/lib/src/attachment/attachment_file.dart | 2 +- .../lib/src/attachment/uploader/attachment_uploader.dart | 2 +- .../lib/src/ws/client/engine/stream_web_socket_engine.dart | 2 +- .../src/ws/client/reconnect/connection_recovery_handler.dart | 2 +- packages/stream_core_flutter/test/flutter_test_config.dart | 2 +- packages/stream_thumbnail/lib/src/stream_thumbnail.dart | 2 +- packages/stream_thumbnail/lib/stream_thumbnail_web.dart | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/design_system_gallery/lib/components/common/stream_loading_spinner.dart b/apps/design_system_gallery/lib/components/common/stream_loading_spinner.dart index 1fc849b9..f9574997 100644 --- a/apps/design_system_gallery/lib/components/common/stream_loading_spinner.dart +++ b/apps/design_system_gallery/lib/components/common/stream_loading_spinner.dart @@ -83,7 +83,7 @@ class _AnimatedSpinnerState extends State<_AnimatedSpinner> with SingleTickerPro if (!mounted) return; _controller.reset(); - return await _controller.forward(); + return _controller.forward(); } @override diff --git a/packages/stream_core/lib/src/attachment/attachment_file.dart b/packages/stream_core/lib/src/attachment/attachment_file.dart index 63ee4f50..35b4d8aa 100644 --- a/packages/stream_core/lib/src/attachment/attachment_file.dart +++ b/packages/stream_core/lib/src/attachment/attachment_file.dart @@ -182,7 +182,7 @@ extension AttachmentFileMultipartExtension on AttachmentFile { ); final multipartFile = result.getOrNull(); - if (multipartFile != null) return await multipartFile; + if (multipartFile != null) return multipartFile; // Fallback to byte-based creation (web platforms, inaccessible paths) final bytes = await readAsBytes(); diff --git a/packages/stream_core/lib/src/attachment/uploader/attachment_uploader.dart b/packages/stream_core/lib/src/attachment/uploader/attachment_uploader.dart index dd1cf0db..a02b1182 100644 --- a/packages/stream_core/lib/src/attachment/uploader/attachment_uploader.dart +++ b/packages/stream_core/lib/src/attachment/uploader/attachment_uploader.dart @@ -84,7 +84,7 @@ class StreamAttachmentUploader { ), ); - return await result.fold( + return result.fold( onSuccess: (data) { final uploaded = UploadedAttachment( id: attachment.id, diff --git a/packages/stream_core/lib/src/ws/client/engine/stream_web_socket_engine.dart b/packages/stream_core/lib/src/ws/client/engine/stream_web_socket_engine.dart index 3a3b272a..f702ff6f 100644 --- a/packages/stream_core/lib/src/ws/client/engine/stream_web_socket_engine.dart +++ b/packages/stream_core/lib/src/ws/client/engine/stream_web_socket_engine.dart @@ -65,7 +65,7 @@ class StreamWebSocketEngine implements WebSocketEngine { onError: _listener?.onError, ); - return await _ws?.ready.then((_) => _listener?.onOpen()); + return _ws?.ready.then((_) => _listener?.onOpen()); }); } diff --git a/packages/stream_core/lib/src/ws/client/reconnect/connection_recovery_handler.dart b/packages/stream_core/lib/src/ws/client/reconnect/connection_recovery_handler.dart index 9b534887..55c4a0af 100644 --- a/packages/stream_core/lib/src/ws/client/reconnect/connection_recovery_handler.dart +++ b/packages/stream_core/lib/src/ws/client/reconnect/connection_recovery_handler.dart @@ -158,6 +158,6 @@ class ConnectionRecoveryHandler extends Disposable { Future dispose() async { _cancelReconnection(); await _subscriptions.dispose(); - return await super.dispose(); + return super.dispose(); } } diff --git a/packages/stream_core_flutter/test/flutter_test_config.dart b/packages/stream_core_flutter/test/flutter_test_config.dart index fe5e06e7..b689d05b 100644 --- a/packages/stream_core_flutter/test/flutter_test_config.dart +++ b/packages/stream_core_flutter/test/flutter_test_config.dart @@ -6,7 +6,7 @@ import 'package:alchemist/alchemist.dart'; Future testExecutable(FutureOr Function() testMain) async { final isRunningInCi = Platform.environment.containsKey('GITHUB_ACTIONS'); - return await AlchemistConfig.runWithConfig( + return AlchemistConfig.runWithConfig( config: AlchemistConfig( // Enable golden tests for CI environments and disable them for local environments. ciGoldensConfig: CiGoldensConfig(enabled: isRunningInCi), diff --git a/packages/stream_thumbnail/lib/src/stream_thumbnail.dart b/packages/stream_thumbnail/lib/src/stream_thumbnail.dart index dbbdd8ce..eed9dcb3 100644 --- a/packages/stream_thumbnail/lib/src/stream_thumbnail.dart +++ b/packages/stream_thumbnail/lib/src/stream_thumbnail.dart @@ -29,7 +29,7 @@ abstract final class StreamThumbnail { }) async { if (videos.isEmpty) return []; - return await StreamThumbnailPlatform.instance.thumbnailFiles( + return StreamThumbnailPlatform.instance.thumbnailFiles( videos: videos, headers: headers, thumbnailPath: thumbnailPath, diff --git a/packages/stream_thumbnail/lib/stream_thumbnail_web.dart b/packages/stream_thumbnail/lib/stream_thumbnail_web.dart index bee98515..f08c0abe 100644 --- a/packages/stream_thumbnail/lib/stream_thumbnail_web.dart +++ b/packages/stream_thumbnail/lib/stream_thumbnail_web.dart @@ -262,7 +262,7 @@ class StreamThumbnailWeb extends StreamThumbnailPlatform { // Bound a source that never fires `seeked`/`error` so the future can't hang // (and retain the video element) forever, and release the media element once // the result settles. - return await completer.future + return completer.future .timeout( const Duration(seconds: 30), onTimeout: () => throw PlatformException( From e4414fd624bd4b7d000bfd3d15a3e1881e8b71ec Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 17 Aug 2026 14:34:41 +0200 Subject: [PATCH 05/10] chore(repo): complete the await-rule revert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit landed only part of it. `git checkout -- ` stages as it writes, so the commit picked up exactly those eight files and the `git add` that was meant to catch the rest ran from a package directory, where its `.` pathspec matched nothing. Left behind, and included here: - `analysis_options.yaml` — `unnecessary_await_in_return` back to enabled and `async_return_with_no_await` off, which is the whole point of the revert. Without it CI kept the rules the wrong way round and reported all eight reverted sites. - The three `return await` in `stream_thumbnail_method_channel.dart`, which the checkout did not cover because that file also carries the `no_dynamic_casts` fixes and had to be edited rather than restored. Co-Authored-By: Claude Opus 5 (1M context) --- analysis_options.yaml | 10 ++++++---- .../lib/src/stream_thumbnail_method_channel.dart | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index f1cb4ceb..ac5e8475 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -39,10 +39,12 @@ linter: # Single quotes are easier to type and don't compromise on readability. prefer_double_quotes: false - # Conflicts with `async_return_with_no_await`, which wants the `await` this - # rule wants removed. Awaiting keeps the frame in the stack trace, so the - # newer rule wins and this one is off. - unnecessary_await_in_return: false + # Conflicts with `unnecessary_await_in_return`, which stays enabled: a bare + # `return future` from an async body is what we want, and awaiting it only + # adds a hop. Inside a `try` the choice does matter — awaiting is what makes + # the function catch its own errors — but that is a per-site call, not one + # for a repo-wide lint. + async_return_with_no_await: false # Enabled once the packages move to `material_ui`; until then it fires on # every `package:flutter/material.dart` import in the repo. diff --git a/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart b/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart index 115e7491..6e42fb56 100644 --- a/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart +++ b/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart @@ -162,7 +162,7 @@ class MethodChannelStreamThumbnail extends StreamThumbnailPlatform { rethrow; } - return await completer.future; + return completer.future; } @override @@ -202,7 +202,7 @@ class MethodChannelStreamThumbnail extends StreamThumbnailPlatform { rethrow; } - return await completer.future; + return completer.future; } @override @@ -238,6 +238,6 @@ class MethodChannelStreamThumbnail extends StreamThumbnailPlatform { rethrow; } - return await completer.future; + return completer.future; } } From 12c20b503ee6eb248a14f134fa62c96e4850bfc0 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 17 Aug 2026 14:38:01 +0200 Subject: [PATCH 06/10] chore(repo): trim the async_return_with_no_await note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checked the premise behind it first: of the 11 sites the rule flags, none returns from inside a `try`, and the 11 returns that are inside one all return synchronous values. So the case where the `await` changes behaviour — letting the function catch its own errors — does not occur here, and the note can just say so. Co-Authored-By: Claude Opus 5 (1M context) --- analysis_options.yaml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index ac5e8475..197691d6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -39,11 +39,8 @@ linter: # Single quotes are easier to type and don't compromise on readability. prefer_double_quotes: false - # Conflicts with `unnecessary_await_in_return`, which stays enabled: a bare - # `return future` from an async body is what we want, and awaiting it only - # adds a hop. Inside a `try` the choice does matter — awaiting is what makes - # the function catch its own errors — but that is a per-site call, not one - # for a repo-wide lint. + # Conflicts with `unnecessary_await_in_return`, which stays enabled. + # The `await` only matters inside a `try`, and no site returns one there. async_return_with_no_await: false # Enabled once the packages move to `material_ui`; until then it fires on From c631210330a938f7339cd7efd2d756b1fb347614 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 17 Aug 2026 14:50:18 +0200 Subject: [PATCH 07/10] fix(thumb): fail the request when the platform returns no thumbnail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `no_dynamic_casts` made an existing implicit cast visible, and the cast turned out to be reachable: `StreamThumbnailPlugin.m` returns `nil` when generation fails and passes it straight to `result(data)`, so Flutter receives `null`, `result != true` holds, and the value was cast to a non-nullable `Object`. The caller got a `TypeError` about the cast rather than anything describing the failure. Android never reaches it — it replies `true` and delivers through the `result#data` callback. Widening `_resolveFuture` to `Object?` would only move the problem: the completers are `Completer` behind a `T extends Object` bound, and `thumbnailData`, `thumbnailFile` and `thumbnailFiles` all promise a value, so a null would fail at the caller's `await` instead — one frame later and less legibly. A missing thumbnail is a failure, not a value, so it now completes as a `PlatformException`, the path `_resolveFuture` already had for errors. Reported by CodeRabbit on #150. Behaviour before this commit was the same cast, written implicitly, so this is a latent bug the new lint surfaced rather than a regression it introduced. Co-Authored-By: Claude Opus 5 (1M context) --- .../flutter/generated_plugin_registrant.cc | 11 ++++++++ .../flutter/generated_plugin_registrant.h | 15 +++++++++++ .../linux/flutter/generated_plugins.cmake | 23 +++++++++++++++++ .../src/stream_thumbnail_method_channel.dart | 12 +++++++-- .../test/stream_thumbnail_test.dart | 25 +++++++++++++++++++ 5 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 packages/stream_thumbnail/example/linux/flutter/generated_plugin_registrant.cc create mode 100644 packages/stream_thumbnail/example/linux/flutter/generated_plugin_registrant.h create mode 100644 packages/stream_thumbnail/example/linux/flutter/generated_plugins.cmake diff --git a/packages/stream_thumbnail/example/linux/flutter/generated_plugin_registrant.cc b/packages/stream_thumbnail/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..e71a16d2 --- /dev/null +++ b/packages/stream_thumbnail/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/packages/stream_thumbnail/example/linux/flutter/generated_plugin_registrant.h b/packages/stream_thumbnail/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/packages/stream_thumbnail/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/stream_thumbnail/example/linux/flutter/generated_plugins.cmake b/packages/stream_thumbnail/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..2e1de87a --- /dev/null +++ b/packages/stream_thumbnail/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart b/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart index 6e42fb56..69d619ec 100644 --- a/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart +++ b/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart @@ -85,6 +85,14 @@ class MethodChannelStreamThumbnail extends StreamThumbnailPlatform { _resolveFuture(callId, error is Exception ? error : Exception(error)); } + // iOS reports a failed generation by returning a null payload, which has to + // become an error: the completers are non-nullable and the public futures + // promise a value. + PlatformException _thumbnailFailed(int callId) => PlatformException( + code: 'thumbnail_generation_failed', + message: 'The platform returned no thumbnail for request $callId.', + ); + void _resolveFuture(int callId, Object value) { if (value is Exception) { _futures[callId]?.completeError(value); @@ -154,7 +162,7 @@ class MethodChannelStreamThumbnail extends StreamThumbnailPlatform { try { final result = await methodChannel.invokeMethod('files', reqMap); if (result != true) { - _resolveFuture(callId, result as Object); + _resolveFuture(callId, (result as Object?) ?? _thumbnailFailed(callId)); } } catch (_) { // Drop the pending completer so it doesn't linger in `_futures`. @@ -231,7 +239,7 @@ class MethodChannelStreamThumbnail extends StreamThumbnailPlatform { try { final result = await methodChannel.invokeMethod('data', reqMap); if (result != true) { - _resolveFuture(callId, result as Object); + _resolveFuture(callId, (result as Object?) ?? _thumbnailFailed(callId)); } } catch (_) { _futures.remove(callId); diff --git a/packages/stream_thumbnail/test/stream_thumbnail_test.dart b/packages/stream_thumbnail/test/stream_thumbnail_test.dart index 7af3bb7d..ab989ea8 100644 --- a/packages/stream_thumbnail/test/stream_thumbnail_test.dart +++ b/packages/stream_thumbnail/test/stream_thumbnail_test.dart @@ -209,6 +209,31 @@ void main() { expect(args['quality'], 80); }); + test('a null reply fails the request instead of throwing a cast error', () async { + // iOS replies with nil when generation fails; the futures are + // non-nullable, so it has to surface as an error. + mockChannel(null); + + await expectLater( + MethodChannelStreamThumbnail().thumbnailData( + video: 'a.mp4', + headers: null, + imageFormat: StreamThumbnailFormat.png, + maxHeight: 10, + maxWidth: 20, + timeMs: 0, + quality: 80, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + 'thumbnail_generation_failed', + ), + ), + ); + }); + test('thumbnailFile wraps a directly-returned path in an XFile', () async { // iOS replies with the written file path directly (Android uses the // 'result#file' reverse callback instead). From 7feb7577cbd117ae276ff29bbdecb2d7768211dd Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 17 Aug 2026 14:50:37 +0200 Subject: [PATCH 08/10] docs(thumb): changelog the thumbnail failure fix The entry the previous commit owed: it changes published behaviour, so per STYLE_GUIDE it belongs under Upcoming in the package changelog. Co-Authored-By: Claude Opus 5 (1M context) --- packages/stream_thumbnail/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/stream_thumbnail/CHANGELOG.md b/packages/stream_thumbnail/CHANGELOG.md index 6f01f714..5f7d7910 100644 --- a/packages/stream_thumbnail/CHANGELOG.md +++ b/packages/stream_thumbnail/CHANGELOG.md @@ -1,5 +1,10 @@ ## Upcoming +### 🐛 Bug Fixes + +- A failed thumbnail generation now completes with a `PlatformException` instead of a cast error. iOS reports failure by + returning no data, which previously surfaced as a `TypeError` about an internal cast. + ### 🔄 Changed - Raised the minimum Flutter version to `>=3.44.0` and the Dart SDK to `^3.12.0`. From cda2dbef29a4a5f28c3c34f93e46ba9dabbb060e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 17 Aug 2026 15:01:32 +0200 Subject: [PATCH 09/10] refactor(thumb): type the method-channel replies instead of casting them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `invokeMethod` returns `Future`, but with no type argument `T` infers as `dynamic` — which is where `no_dynamic_casts` was pointing. The previous commit answered it by casting the result; naming the type is the better answer, since the reply then arrives as `Object?` and there is nothing to cast. `` rather than anything narrower: all three methods answer `true` on Android before delivering out-of-band through the reverse callbacks, so `` or `` would throw there. Typing the replies made the `file` branch's silent assumption visible — `result as String` on an `Object?` trips `cast_nullable_to_non_nullable`, where the same cast from `dynamic` had been invisible. It now completes as a failure for anything that is not a path, matching the other two branches, so the reverse-callback protocol is handled the same way in all three places. Co-Authored-By: Claude Opus 5 (1M context) --- .../lib/src/stream_thumbnail_method_channel.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart b/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart index 69d619ec..55ba7ca1 100644 --- a/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart +++ b/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart @@ -160,9 +160,9 @@ class MethodChannelStreamThumbnail extends StreamThumbnailPlatform { }; try { - final result = await methodChannel.invokeMethod('files', reqMap); + final result = await methodChannel.invokeMethod('files', reqMap); if (result != true) { - _resolveFuture(callId, (result as Object?) ?? _thumbnailFailed(callId)); + _resolveFuture(callId, result ?? _thumbnailFailed(callId)); } } catch (_) { // Drop the pending completer so it doesn't linger in `_futures`. @@ -199,11 +199,11 @@ class MethodChannelStreamThumbnail extends StreamThumbnailPlatform { }; try { - final result = await methodChannel.invokeMethod('file', reqMap); + final result = await methodChannel.invokeMethod('file', reqMap); if (result != true) { // iOS returns the written file path directly; wrap it as an [XFile] to // satisfy the Future contract (Android replies via 'result#file'). - _resolveFuture(callId, XFile(result as String)); + _resolveFuture(callId, result is String ? XFile(result) : _thumbnailFailed(callId)); } } catch (_) { _futures.remove(callId); @@ -237,9 +237,9 @@ class MethodChannelStreamThumbnail extends StreamThumbnailPlatform { }; try { - final result = await methodChannel.invokeMethod('data', reqMap); + final result = await methodChannel.invokeMethod('data', reqMap); if (result != true) { - _resolveFuture(callId, (result as Object?) ?? _thumbnailFailed(callId)); + _resolveFuture(callId, result ?? _thumbnailFailed(callId)); } } catch (_) { _futures.remove(callId); From 0d6e088aafe983c45b9327140eb31f3d4deb6499 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 17 Aug 2026 15:09:42 +0200 Subject: [PATCH 10/10] revert(thumb): drop the thumbnail failure handling from this PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the null-result work — the `PlatformException` on a missing thumbnail, its regression test, the typed `invokeMethod` replies and the CHANGELOG entry. All of it was chased down from a CodeRabbit comment on the two explicit casts, and none of it belongs in a PR whose subject is the SDK floor and the lint list. What stays is the smallest thing that satisfies `no_dynamic_casts`: the two implicit `dynamic` -> `Object` casts, written explicitly. Behaviour is byte-for-byte what it was before this PR, a null reply included — the cast was always there, just invisible. Also untracks `example/linux/**`, which a `git add packages/stream_thumbnail` swept in while committing that work. Those files are generated by a Linux build and were untracked before. The underlying bug is real and still unfixed: iOS answers a failed generation with `result(nil)` while the `file` branch beside it answers with a `FlutterError`, so a null reaches Dart and dies on the cast. Fixing that belongs in the plugin, in its own PR, with an iOS build to verify — nothing in CI compiles the native sources. Co-Authored-By: Claude Opus 5 (1M context) --- packages/stream_thumbnail/CHANGELOG.md | 5 ---- .../flutter/generated_plugin_registrant.cc | 11 -------- .../flutter/generated_plugin_registrant.h | 15 ----------- .../linux/flutter/generated_plugins.cmake | 23 ----------------- .../src/stream_thumbnail_method_channel.dart | 20 +++++---------- .../test/stream_thumbnail_test.dart | 25 ------------------- 6 files changed, 6 insertions(+), 93 deletions(-) delete mode 100644 packages/stream_thumbnail/example/linux/flutter/generated_plugin_registrant.cc delete mode 100644 packages/stream_thumbnail/example/linux/flutter/generated_plugin_registrant.h delete mode 100644 packages/stream_thumbnail/example/linux/flutter/generated_plugins.cmake diff --git a/packages/stream_thumbnail/CHANGELOG.md b/packages/stream_thumbnail/CHANGELOG.md index 5f7d7910..6f01f714 100644 --- a/packages/stream_thumbnail/CHANGELOG.md +++ b/packages/stream_thumbnail/CHANGELOG.md @@ -1,10 +1,5 @@ ## Upcoming -### 🐛 Bug Fixes - -- A failed thumbnail generation now completes with a `PlatformException` instead of a cast error. iOS reports failure by - returning no data, which previously surfaced as a `TypeError` about an internal cast. - ### 🔄 Changed - Raised the minimum Flutter version to `>=3.44.0` and the Dart SDK to `^3.12.0`. diff --git a/packages/stream_thumbnail/example/linux/flutter/generated_plugin_registrant.cc b/packages/stream_thumbnail/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index e71a16d2..00000000 --- a/packages/stream_thumbnail/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/packages/stream_thumbnail/example/linux/flutter/generated_plugin_registrant.h b/packages/stream_thumbnail/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47b..00000000 --- a/packages/stream_thumbnail/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/stream_thumbnail/example/linux/flutter/generated_plugins.cmake b/packages/stream_thumbnail/example/linux/flutter/generated_plugins.cmake deleted file mode 100644 index 2e1de87a..00000000 --- a/packages/stream_thumbnail/example/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart b/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart index 55ba7ca1..6e42fb56 100644 --- a/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart +++ b/packages/stream_thumbnail/lib/src/stream_thumbnail_method_channel.dart @@ -85,14 +85,6 @@ class MethodChannelStreamThumbnail extends StreamThumbnailPlatform { _resolveFuture(callId, error is Exception ? error : Exception(error)); } - // iOS reports a failed generation by returning a null payload, which has to - // become an error: the completers are non-nullable and the public futures - // promise a value. - PlatformException _thumbnailFailed(int callId) => PlatformException( - code: 'thumbnail_generation_failed', - message: 'The platform returned no thumbnail for request $callId.', - ); - void _resolveFuture(int callId, Object value) { if (value is Exception) { _futures[callId]?.completeError(value); @@ -160,9 +152,9 @@ class MethodChannelStreamThumbnail extends StreamThumbnailPlatform { }; try { - final result = await methodChannel.invokeMethod('files', reqMap); + final result = await methodChannel.invokeMethod('files', reqMap); if (result != true) { - _resolveFuture(callId, result ?? _thumbnailFailed(callId)); + _resolveFuture(callId, result as Object); } } catch (_) { // Drop the pending completer so it doesn't linger in `_futures`. @@ -199,11 +191,11 @@ class MethodChannelStreamThumbnail extends StreamThumbnailPlatform { }; try { - final result = await methodChannel.invokeMethod('file', reqMap); + final result = await methodChannel.invokeMethod('file', reqMap); if (result != true) { // iOS returns the written file path directly; wrap it as an [XFile] to // satisfy the Future contract (Android replies via 'result#file'). - _resolveFuture(callId, result is String ? XFile(result) : _thumbnailFailed(callId)); + _resolveFuture(callId, XFile(result as String)); } } catch (_) { _futures.remove(callId); @@ -237,9 +229,9 @@ class MethodChannelStreamThumbnail extends StreamThumbnailPlatform { }; try { - final result = await methodChannel.invokeMethod('data', reqMap); + final result = await methodChannel.invokeMethod('data', reqMap); if (result != true) { - _resolveFuture(callId, result ?? _thumbnailFailed(callId)); + _resolveFuture(callId, result as Object); } } catch (_) { _futures.remove(callId); diff --git a/packages/stream_thumbnail/test/stream_thumbnail_test.dart b/packages/stream_thumbnail/test/stream_thumbnail_test.dart index ab989ea8..7af3bb7d 100644 --- a/packages/stream_thumbnail/test/stream_thumbnail_test.dart +++ b/packages/stream_thumbnail/test/stream_thumbnail_test.dart @@ -209,31 +209,6 @@ void main() { expect(args['quality'], 80); }); - test('a null reply fails the request instead of throwing a cast error', () async { - // iOS replies with nil when generation fails; the futures are - // non-nullable, so it has to surface as an error. - mockChannel(null); - - await expectLater( - MethodChannelStreamThumbnail().thumbnailData( - video: 'a.mp4', - headers: null, - imageFormat: StreamThumbnailFormat.png, - maxHeight: 10, - maxWidth: 20, - timeMs: 0, - quality: 80, - ), - throwsA( - isA().having( - (e) => e.code, - 'code', - 'thumbnail_generation_failed', - ), - ), - ); - }); - test('thumbnailFile wraps a directly-returned path in an XFile', () async { // iOS replies with the written file path directly (Android uses the // 'result#file' reverse callback instead).