Skip to content

chore(ui)!: migrate to material_ui and cupertino_ui - #151

Draft
xsahil03x wants to merge 7 commits into
mainfrom
chore/migrate-material-cupertino-ui
Draft

chore(ui)!: migrate to material_ui and cupertino_ui#151
xsahil03x wants to merge 7 commits into
mainfrom
chore/migrate-material-cupertino-ui

Conversation

@xsahil03x

@xsahil03x xsahil03x commented Aug 17, 2026

Copy link
Copy Markdown
Member

Submit a pull request

Linear: FLU-700

CLA

  • I have signed the Stream CLA (required).
  • The code changes follow best practices
  • Code changes are tested (add some information if not applicable)

Description of the pull request

Flutter split Material and Cupertino out of the framework into the flutter.dev-published material_ui / cupertino_ui packages — both v1.0.0, published 2026-08-12, the same day as Flutter 3.47 stable. Both require flutter: >=3.44.0.

Stacked on #150, which raises the floor to exactly that version — that is what unblocks this, so review #150 first and merge it first.

What changed

  • 195 import directives across 194 files rewritten from package:flutter/material.dart to package:material_ui/material_ui.dart. Every occurrence was verified to be a plain import directive first — no export directives, no show/hide clauses. Reordering moved the directive relative to its neighbours, so directives_ordering was reapplied via dart fix.
  • material_ui registered in melos.yaml (the workspace's single version registry) and added to the three pubspecs that import it: stream_core_flutter, the gallery, and the stream_thumbnail example.
  • migrate_design_widgets enabled in all_lint_rules.yaml to hold the line.
  • STYLE_GUIDE.md and the gallery's AGENTS.md — the import-convention code samples showed the old URI, which is what the next contributor would copy.

cupertino_ui gets no direct dependency — deliberately

Nothing in this repo imports package:flutter/cupertino.dart; the only Cupertino hits are a doc-comment link and a private _CupertinoSwitchDefaults class name in StreamSwitch. So cupertino_ui arrives transitively through material_ui, and adding it directly would be an unused dependency. If a widget starts importing it, add it then.

Why this is breaking

material_ui ships its own implementations and Flutter 3.47's material.dart does not re-export them, so flutter/material's ThemeData / ThemeExtension / ColorScheme are unrelated types to material_ui's. Concretely, an app that does

MaterialApp(theme: ThemeData(extensions: [StreamTheme(...)]))   // Flutter's Material

no longer compiles against this package. It must import material_ui and use its MaterialApp — which is also what supplies the MaterialLocalizations several components read.

Two prefixed bridges remain

Each is for a dependency that has not migrated, and each carries an // ignore: migrate_design_widgets. Neither changes rendering:

Site Why
StreamMessageText flutter_markdown_plus (latest, 1.0.12) still takes Flutter's ThemeData in MarkdownStyleSheet.fromTheme. The bridge maps exactly the fields that factory reads — the five textTheme slots, cardTheme.color, cardColor, primaryColor, dividerColor, and the colorScheme — so the sheet it produces is unchanged.
ThemeConfiguration.buildWidgetbookTheme (gallery) Widgetbook still takes Flutter's ThemeData for its own chrome. The Stream branding is restated over the same colors, so the gallery looks the same.

None of the other Material-adjacent dependencies needed one — alchemist, shimmer, flutter_svg, cached_network_image_ce, svg_icon_widget, device_frame_plus and flutter_colorpicker either take shared types (Color, TextStyle, Widget) or none at all.

The lint is safe at the floor

migrate_design_widgets is recognised by the 3.47 analyzer and not by 3.44's. That does not break legacy_version_analyze: undefined_lint is reported against the options file itself, and nothing in CI analyses the repo root where all_lint_rules.yaml lives — melos run analyze runs per package, and the legacy job runs from packages/stream_core/lib. Enabled, the rule reports exactly the two bridges above and nothing else, which is the proof the migration is complete.

Test-only adjustments

Both are consequences of the split, not of anything this repo does unusually:

  • find.byTooltip is bound to Flutter's Tooltip. The bottom-nav tooltip test now matches material_ui's by predicate.
  • Alchemist scaffolds golden tests with Flutter's MaterialApp, whose MaterialLocalizations the migrated StreamBottomNavBar cannot read — it threw No MaterialLocalizations found and rendered an error box. Its two golden wrappers now provide material_ui's delegates. Worth knowing for any future component that reads localizations: widget tests are fine (they wrap in material_ui's MaterialApp), golden tests are not.

Testing

Check Result
dart analyze --fatal-infos, 4 packages @ 3.47.0 clean
melos run check:barrels pass
melos run format:verify (441 files) pass
melos run generate:all no diff — the generators see the same types
stream_core_flutter tests (GITHUB_ACTIONS=true) 362 passed, 42 failed — identical set to before the migration
stream_core / stream_thumbnail tests unaffected — neither imports Material

On the 42 golden failures: the pre-existing macOS-vs-Linux baseline (committed goldens are Linux-rendered), carried over unchanged from #150comm returns empty in both directions against that branch's run. Zero introduced. Every golden that can be compared on macOS is unchanged; the 42 are the ones whose committed baselines are Linux-rendered, so they are equally unverifiable here before and after. This PR's own CI run on Linux is the check for those. No golden was regenerated, update_goldens.yml was not dispatched, and no .png is touched by this diff.

Screenshots / Videos

Not applicable — the migration is pixel-neutral, which is what the golden comparison above establishes.

🤖 Generated with Claude Code

xsahil03x and others added 3 commits August 17, 2026 12:11
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Flutter split Material and Cupertino out of the framework into the
flutter.dev-published `material_ui` / `cupertino_ui` packages, both
v1.0.0 and published on 2026-08-12, the same day as Flutter 3.47 stable.
Both require `flutter: >=3.44.0`, which is exactly the floor raised in
the parent commit — that is what unblocks this, and why it is stacked.

Rewrites all 195 import directives across 194 tracked files:
`package:flutter/material.dart` -> `package:material_ui/material_ui.dart`.
Every occurrence was verified to be a plain import first — no export
directives, no `show`/`hide` clauses, and no `package:flutter/cupertino.dart`
import anywhere in the repo. Reordering moved the directive relative to
its neighbours, so `directives_ordering` was reapplied via `dart fix`.

Nothing imports Cupertino directly: the only `Cupertino` references are a
doc-comment link and a private class name in `StreamSwitch`. So
`cupertino_ui` arrives transitively through `material_ui` and gets no
direct pubspec entry, which would otherwise be an unused dependency.

This is breaking for consumers. `material_ui` ships its own
implementations and Flutter 3.47's `material.dart` does not re-export
them, so the two `ThemeData`/`ThemeExtension`/`ColorScheme` are unrelated
types. An app that puts `StreamTheme` into Flutter's
`ThemeData.extensions` no longer compiles; it must import `material_ui`
and use its `MaterialApp`, which is also what supplies the
`MaterialLocalizations` the components read.

Two prefixed bridges remain, each for a dependency that has not migrated
and each carrying an `// ignore: migrate_design_widgets`:

- `StreamMessageText` restates the theme for
  `MarkdownStyleSheet.fromTheme`, mapping exactly the fields that factory
  reads so the rendering is unchanged.
- The gallery builds a separate Flutter theme for Widgetbook's own
  chrome, over the same colors as before.

`migrate_design_widgets` is enabled to hold the line — without it a new
file can silently reintroduce the split. It is only recognised by the
3.47 analyzer, but that is safe at the 3.44 floor: `undefined_lint` is
reported against the options file itself, and nothing in CI analyses the
repo root, where `all_lint_rules.yaml` lives. Enabled, it reports exactly
the two bridges above and nothing else, which is the proof the migration
is complete.

Two test-only adjustments, both consequences of the split rather than of
anything this repo does differently:

- `find.byTooltip` is bound to Flutter's `Tooltip`, so the bottom-nav
  tooltip test now matches `material_ui`'s by predicate.
- Alchemist scaffolds golden tests with Flutter's `MaterialApp`, whose
  `MaterialLocalizations` the migrated `StreamBottomNavBar` cannot read.
  Its golden wrappers now provide `material_ui`'s delegates.

Verified: `dart analyze --fatal-infos` clean across all four packages,
barrels and formatting pass, `melos run generate:all` produces no diff,
and `stream_core_flutter` fails the same 42 macOS-vs-Linux goldens as
before the migration — an identical set, 362 passed either way. No
golden was regenerated and no `.png` is touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 473d13ad-f380-4c5b-86db-6cf0a9ddcd56

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

CI caught two golden tests the local run could not: the composer's reply
and link-preview matrices throw `No MaterialLocalizations found` for the
same reason `StreamBottomNavBar` did — alchemist scaffolds golden tests
with Flutter's own `MaterialApp`, whose localizations are an unrelated
type to `material_ui`'s.

The local check missed them because both files were already in the
42-golden macOS-vs-Linux baseline, so comparing failure *sets* showed no
change while the underlying failure had switched from a pixel mismatch
to a thrown exception rendering an error box. Comparing exception text
rather than failure sets finds them: the run now reports zero
`No MaterialLocalizations found`, and all 42 remaining failures are
plain `Pixel test failed` with no widget-library exception among them.

The wrapper introduced for the nav bar is extracted to
`test/utils/material_localizations.dart` and shared by all three files,
since any component reading localizations will need it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.94%. Comparing base (c1c7136) to head (97e115b).

Additional details and impacted files
@@                   Coverage Diff                    @@
##           chore/flutter-3.44.0     #151      +/-   ##
========================================================
+ Coverage                 59.47%   60.94%   +1.47%     
========================================================
  Files                       192      192              
  Lines                      7745     7755      +10     
========================================================
+ Hits                       4606     4726     +120     
+ Misses                     3139     3029     -110     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

xsahil03x and others added 3 commits August 17, 2026 12:46
`StreamMessageText` had no tests, so the hand-written bridge that adapts
`material_ui`'s `ThemeData` for `MarkdownStyleSheet.fromTheme` was the
one piece of new logic in the migration with no coverage at all —
codecov reported 0% of the diff hit.

Pins the fields the bridge has to carry: the heading and body text
styles, inline code's 85%-of-body sizing, `cardColor`, `dividerColor`,
and that an explicitly supplied style sheet still wins. Dropping any of
them from the bridge silently falls back to a Material default rather
than failing, which is exactly the kind of regression a golden diff on
one component would not localise.

Not asserted: `blockquoteDecoration`, which the pinned
flutter_markdown_plus 1.0.7 hardcodes to `Colors.blue.shade100` and only
derives from `colorScheme.surfaceContainerHighest` in 1.0.12. The bridge
maps the colour scheme anyway, so a dependency bump stays correct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each place the migration left something behind now names the upstream
package it is waiting on, so none of them survives on inertia after the
dependency catches up.

Two are bridges with a clear deletion trigger — the markdown theme
adapter (FLU-701, flutter_markdown_plus) and the gallery's Widgetbook
theme (FLU-703). One is a test helper every future component reading
localizations will need until alchemist migrates (FLU-702).

The other two are not workarounds but silent degradation, which is why
they are worth marking in the source rather than only in a ticket:
`DeviceFrame` and the colour picker call Flutter's `Theme.of`, and
Flutter's `Theme.of` falls back to a default rather than throwing when
the ancestor is `material_ui`'s `MaterialApp`. They compile and render —
just with stock Material colours (FLU-704).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…encies

`material_ui` ships a supported migration utility for exactly the case
this PR was hand-rolling: `MaterialUiCompatibilityBridge` reads the
modern theme from context and installs a legacy `Theme` plus legacy
`Localizations` below it, so packages still built on
`package:flutter/material.dart` resolve correctly inside a modern tree.
Step 3 of the package's own migration guide. It replaces both bridges
written here, and reaches cases neither of them covered.

`StreamMessageText` now wraps `MarkdownBody` rather than mapping a theme
by hand. Two gains beyond deleting ~30 lines: the mapping is upstream's
and covers the whole `ColorScheme` and `TextTheme` instead of the five
slots `fromTheme` happens to read today, and `MarkdownBody` itself —
a legacy Material widget — finally has a legacy theme and localizations
to resolve against, which it never did after the migration. A selectable
markdown body needs those for its context menu.

The bridge maps `platform`, `visualDensity`, `colorScheme` and
`textTheme` and nothing else, so the legacy-only scalars `fromTheme`
also reads — `cardColor`, `dividerColor`, `primaryColor`,
`cardTheme.color` — are copied on top. Without that, an app setting
`cardColor` explicitly would lose its code-block background. Pinned by
the existing tests, which caught it.

The gallery bridges at the app root via `MaterialApp.builder`, the
pattern the guide recommends, which covers every legacy dependency below
it at once: the colour-picker dialog that was silently rendering with
stock Material colours, and DeviceFrame. `buildWidgetbookTheme` is gone
— Widgetbook installs its own `MaterialApp.router` below the bridge, so
its chrome takes the bridged theme by value instead, which is a fuller
mapping than the 13 colours that method restated by hand.

The prefixed imports are `as legacy`, matching the modern/legacy naming
`material_ui` uses in `migration_utility.dart` itself.

Alchemist is unaffected and keeps its own wrapper: that failure runs the
other way — a modern widget needing modern localizations inside a legacy
test harness — which is not what the bridge does.

Locally: analyze, barrels and formatting clean; the golden failure set is
the same 42 macOS-vs-Linux baseline with zero widget-library exceptions
and zero `No MaterialLocalizations found`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@xsahil03x
xsahil03x marked this pull request as draft August 17, 2026 12:18
Base automatically changed from chore/flutter-3.44.0 to main August 17, 2026 13:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant