Flutter 3.50 Splits Material/Cupertino into Packages, Cuts iOS Touch Latency by a Full Frame, and Stabilizes Direct Native Calls
Flutter 3.50 & Dart 3.14 release, come see what's new
theme: smartblue
This Flutter 3.50 & Dart 3.14 release feels like it's shedding the historical baggage; it's a pretty solid version. Stop saying I only post Flutter problems—today I'm energized:
- The SDK's Material and Cupertino are officially deprecated, fully split into independent packages
material_uiandcupertino_ui. - Lots of iOS performance optimizations, e.g., touch response optimization is a full frame faster (60Hz faster by
16ms, 120Hz by8ms), Android release blocks the high-risk vulnerability of malicious Intent extras injection, and Hybrid Composition++ (HCPP) is optimized. - New
dart migratecommand, andFFIgenandJNIgenfor direct native calls are officially Stable.
Material and Cupertino
Starting with Flutter 3.50, the built-in style libraries in the SDK are officially deprecated. package:flutter/material.dart and package:flutter/cupertino.dart must be replaced with the standalone packages package:material_ui and package:cupertino_ui on pub.dev.
New projects default to package:material_ui. Existing projects can migrate automatically with dart fix --apply --code=migrate_design_widgets. After running it:
- Adds the standalone
material_ui/cupertino_uidependencies topubspec.yaml. - Rewrites all
package:flutter/material.dartold import paths to the new package paths. - Automatically syncs Localization-related code and configuration.
Framework
This one is truly an epic update. Scroll views (ScrollView / Slivers) have always been a pain point in complex business UIs. This time, Flutter 3.50 merged handling for the most requested issues, such as clipping, dynamic heights, and responsive physics.
SliverClipRect and SliverClipRRect
Previously, when you wrote a Sliver block with rounded corners inside a CustomScrollView, or had a semi-transparent pinned SliverPersistentHeader at the top, when list content scrolled under the pinned header, a plain ClipRRect couldn't wrap the Sliver because it's a RenderBox not a RenderSliver—or the UI would mess up the overlap region with the pinned header.
This time PR #179003 officially introduces SliverClipRect and SliverClipRRect in rendering/sliver_clip.dart, with the built-in ClipOverlapBehavior enum, specifically handling clipping behavior when overlapping with other Slivers (especially pinned headers) (SliverConstraints.overlap):
enum ClipOverlapBehavior {
/// Ignore the overlap region. If the pinned header is a translucent frosted glass, content below can show through.
none,
/// (Default) Clip the clip rectangle along the edge of the overlap region, ensuring content never intrudes under the pinned header's bottom.
followEdge,
/// Keep the shape and shrink inward! When a Sliver with rounded corners scrolls under a pinned header,
/// it doesn't crush the top corners flat, but smoothly pushes the rounded corners down along the header's bottom edge!
preserveShape,
}
And in the source constructor, Flutter also added Dart's concise enum dot syntax (.followEdge):
SliverClipRRect(
borderRadius: BorderRadius.circular(16),
// Preserve the rounded shape when scrolling under a pinned header, visually refined
clipOverlap: ClipOverlapBehavior.preserveShape,
sliver: SliverList.builder(
itemCount: 20,
itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
),
)
Besides clipping, the main branch also merged
SliverTransform(PR #192866), supporting 2D/3D matrix transforms at the Sliver level, e.g., scroll parallax, elastic stretching, 3D folding. It uses dual listeners (repaintandsemanticsUpdateseparated) to ensure high-refresh-rate 120Hz animations don't drop frames from frequently refreshing the accessibility tree.
ListView.separated supports itemExtentBuilder
As is well known, when a list has tens of thousands of items and each has a fixed height, passing itemExtent gives much better performance. But if each item's height varies—like chat logs or different card types—Flutter previously provided itemExtentBuilder for ListView.builder, allowing you to compute each item's height by index, avoiding instantiating off-screen widgets just to measure them.
But the awkward part was that the most commonly used separated list ListView.separated surprisingly never supported itemExtentBuilder. This time PR #191157 finally fills that gap. Now ListView.separated can also dynamically compute the size of each item and each separator:
ListView.separated(
itemCount: 10000,
// even index is list item, odd index is separator
itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
if (index.isOdd) return 1.0; // separator height 1px
final itemIndex = index ~/ 2;
return myItemHeightCache[itemIndex]; // dynamically return the corresponding card height
},
itemBuilder: (context, index) => MyCard(index: index),
separatorBuilder: (context, index) => const Divider(height: 1),
)
ScrollPhysics.shouldUpdate
This one also supports dynamically switching physics properties without rebuilding the list. Previously, if you wrote a custom ScrollPhysics, e.g., changing the damping factor based on user gestures, or dynamically changing allowImplicitScrolling, even if you passed a new ScrollPhysics instance with the same runtimeType in setState, the ScrollPosition had no idea the physics changed—unless you forcibly change the Key to rebuild the entire scroll view.
This merged PR #189664 adds a @mustCallSuper bool shouldUpdate(covariant ScrollPhysics old) method to the ScrollPhysics base class, directly enabling this optimization:
class CustomDampingScrollPhysics extends ScrollPhysics {
const CustomDampingScrollPhysics({required this.dampingFactor, super.parent});
final double dampingFactor;
@override
bool shouldUpdate(covariant CustomDampingScrollPhysics old) {
// Return true when custom parameters change, notifying ScrollPosition to immediately respond to new physics!
if (dampingFactor != old.dampingFactor) {
return true;
}
return super.shouldUpdate(old);
}
}
Others
Scrollable.ensureVisibleno longer gets blocked by pinned headers: previously callingensureVisibleto scroll to a widget would often scroll exactly under a pinned header and get covered. Now it evaluates pinned headers, letting the target widget settle in the visible area right below the header.SliverFillRemainingtail offset fixed: correctly computes the trailing scroll offset without spurious extra whitespace or overflow.TreeSliverandNestedScrollView:TreeSlivernow automatically clips root node children during expand/collapse animations to prevent visual bleed-through;NestedScrollViewfixes the occasional out-of-bounds position jump during fast flings (that one was genuinely annoying).- Desktop scrollbar mouse gesture tightened (
RawScrollbar.mouseCursor): previously the cursor changed when hovering near the scrollbar area; now it only triggers on the thumb and track. TextInputType.numberWithOptionsadds apasswordoption.- New
Hyphensenum to control whether soft hyphens are used at line breaks. - Drag to select text in
PageViewnow works. - Android can use
AutofillHints.emailOTPCodeto autofill one-time codes from email. DecorationImageadds aplaceholderImageProvider, so container backgrounds can show a fallback image while loading.FadeInImageTransition.fadeInOvercan smoothly fade in an image over an opaque placeholder.- Using
useLogicalSizeonResizeImageanduseLogicalCacheSizeonImagelets you scale cached images to screen density without aBuildContext. RawImagenow exposesblendMode, so custom controls can directly apply color blending when drawing images.
Routing
First, showDialog now has a barrierBuilder. Building frosted-glass dialogs is much more convenient. Previously there was only barrierColor, so you had to hand-roll a bunch of RawDialogRoute or PageRouteBuilder.
showDialog<void>(
context: context,
barrierBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
child: child, // child is the default barrier with click-to-close logic
),
);
},
builder: (context) => const AlertDialog(title: Text('Frosted glass background dialog')),
);
Second, Navigator.popUntilWithResult. Previously Navigator.popUntil did not support returning a value. That Issue #30112 has been open since 2019. People had to use global variables, EventBus, or manually loop pop to work around it. Now Navigator.popUntilWithResult is finally added, and pushNamed<T>() also fills in explicit generic return support:
// 1. On page A, await a strongly typed result:
final OrderResult? result = await Navigator.pushNamed<OrderResult>(
context,
'/checkout-step1',
);
// 2. On page D, pop all the way back to page A and hand the result precisely to that await!
Navigator.popUntilWithResult<OrderResult>(
context,
ModalRoute.withName('/cart'),
OrderResult(orderId: 'ORD-202610', success: true),
);
Finally, Hero animations and accessibility (WCAG):
- Hero animation debounce when keyboard pops up: When a nested
Navigatorchanges size due to keyboard appearance,Heronow uses live rect coordinates, finally fixing the visual jump of Hero flying off-path during page transitions with keyboard dismissal. Escapekey now closesTooltipfirst (WCAG 1.4.13): On desktop, when there's an openTooltip/RawTooltip, pressingEsccloses the tooltip and intercepts the key press so it doesn't bubble up.MediaQueryData.reduceMotionandSemanticsRole.slider: NewMediaQuery.reduceMotionOf(context)detects whether the user enabled "reduce motion" in system settings. Custom slider components can useSemanticsRole.sliderto properly notify screen readers.- Focus management and VoiceOver:
WidgetsApp.routerhas a defaultFocusScopenow. Focus ordering supportsFocusOrder.lexical/.numericshorthand, fixing focus loss on Web and iOS when elements scroll into view with VoiceOver on.
iOS Performance Optimizations
This version gives iOS a free performance boost. In the early Flutter architecture, iOS touch events (touchesMoved) were dispatched on the platform thread, but the CADisplayLink driving rendering ran on a separate UI thread.
To fix cross-thread event jitter, the engine previously inserted a bunch of buffering layers like PointerDataDispatcher, SmoothPointerDataDispatcher, and touchRateCorrectionVSyncClient. The cost was that touch events often had to wait for the next VSync cycle to actually trigger a build.
We've discussed this before.
Now that the threads are merged, CADisplayLink already runs on the main thread, and the current UIKit RunLoop is much better. The event dispatch phase is guaranteed to happen before the CADisplayLink callback phase.
So this release removes all the legacy async buffers and the extra CADisplayLink in FlutterMetalLayer, making the entire pipeline fully synchronous:
----- FRAME BOUNDARY (within the same frame) -----------------------------------------
[Event dispatch phase] touchesMoved triggers
| (fully synchronous, same RunLoop Turn)
|--> Framework processes pointer event and requests a new frame (Schedule Frame)
|--> VSyncWaiter::AwaitVSync registers wait
|
[DisplayLink phase] (immediately triggered in the same cycle!)
|--> CADisplayLink callback fires immediately
|--> Framework immediately produces the current frame and submits to Raster thread for display!
----- FRAME BOUNDARY -------------------------------------------------------
The most important part? iOS 27 adaptation is upgraded. On newly paired or newly upgraded iPhones, no more white-screen stutter. The Flutter CLI now automatically prepares devices and tells the user what to do if preparation fails.
Flutter 3.50 also fixes the intermittent startup crash when running flutter run on iOS 27 devices. Especially with Xcode 27 builds, flutter screenshot is finally restored on physical iPhones and iPads for the first time since iOS 17.
Android
On Android, as we've discussed, starting with 3.50, Intent Extras launching is blocked in release builds:
- In Release builds, custom routes, entrypoints, cached engines, and
--dart-flagsengine flags passed via external Intent are ignored. Normal deep links are completely unaffected. - In Debug/Profile mode, a clear warning is printed if ignored Intent extras are detected.
Then there's HCPP, which we've mentioned many times. The performance of Android native views (maps/WebView) is better now. This time Hybrid Composition++ (HCPP) has evolved to its complete form. By delegating the compositing of native Android Views to the operating system's underlying SurfaceControl / SurfaceFlinger, Flutter layers and native views align perfectly at the system compositor level.
HCPP eliminates scroll tearing and soft-keyboard text input stutter when containing native views. HCPP even supports layering frosted-glass background blur directly on top of native PlatformViews (SurfaceFlinger blur regions, PR #194288), as well as native views stretching and deforming in sync with list overscroll bounce (ui.ImageFilter.stretch, PR #191421). Perfect!
However, this requires the Impeller Vulkan backend on Android 14+, and also needs
flutter config --enable-hcpp.
Finally, AGP Built-in Kotlin. Starting with this version, Flutter plugins must directly migrate to the AGP built-in Kotlin, because later Android will enforce this in AGP 10. If not adapted, pub.dev will also deduct package quality scores (pub.dev score penalty).
Now in Flutter 3.50, the new project's Android template is:
- Java 17 (minimum)
- AGP 9.3.1 & Gradle 9.5.0
- KGP 2.4.20
- SDK defaults:
compileSdkVersion/targetSdkVersiondefault to API 36,minSdkVersiondefaults to API 24
Impeller
Impeller has received a lot of optimizations in this version. Although I often complain about it in posts, Impeller fixes many issues.
First, the SDF (signed distance field) renderer expands and text shadows caching:
- UberSDF rendering acceleration (PR #188577 / PR #196056 / PR #195703): Impeller's SDF (Signed Distance Field) renderer now handles not only basic shapes, but also officially takes over lines, thin rects, thin rounded rects, and linear/radial gradients. Shadow rendering is also unified under the UberSDF Shader. On macOS, the engine can even use SSBO (Shader Storage Buffer Objects) to greatly reduce UberSDF gradient GPU cost.
- Faster first frame startup (Background IO Shader Setup): On the OpenGL backend, shader initialization and compilation setup move to a background IO Context thread in parallel, significantly shortening cold-start first-frame time to screen.
- Text shadows cached by content: Previously, text with shadows recomputed blurred shadows on every frame during translation or scaling animations. Now text shadows are cached and reused by text content, greatly improving animation frame rates.
- Flutter GPU enhanced: Added 2D texture array support, and clearer Dart-layer error messages when pipeline configuration is invalid.
Second, optimizations for Android GPUs. We've probably discussed these several times recently:
- Mali GPU crash fix: Force re-binds textures before OpenGL buffer uploads, fully fixing driver crashes on some Mali GPU devices.
- PowerVR B-series GPUs automatically blacklist Vulkan: Because B-series PowerVR GPU Vulkan drivers are of poor quality (lol), the engine has added them to the Vulkan blacklist, automatically and safely falling back to stable OpenGL ES.
- OpenGL ES 2 compatibility expanded: Backing stores now use unsized texture formats, rescuing old low-end devices.
- Transparent animated image decoding fix: Fixed the bug where GIF/WebP animations that have an opaque first frame but subsequent frames with alpha channels decode incorrectly.
Desktop
Flutter desktop also gets a lot in 3.50:
| Platform | Update | Problems solved |
|---|---|---|
| Windows | Switch to OpenGL ES 3, fix offscreen MSAA and advanced filter infinite loop, Direct Flip, raise text gamma to match Skia, native UTF-8 file path support | 1. One less full-screen render copy, GPU usage drops significantly; 2. Previously text on Windows with the new pipeline was blurry and thin, now with higher gamma it's as clear and full as Skia; 3. Windows usernames or install paths with Chinese/Japanese/Emoji no longer fail to open files due to path encoding! |
| macOS | Window content adaptive sizing (Content-sized / shrinkWrap), show window only after first frame is ready (Deferred show) |
Previously making macOS pop-ups and small tooltips made it hard to auto-size the native window to the Flutter widget's height, and new windows flashed white for an instant. Now it can automatically wrap content size and wait for the first frame before showing the window, silky and flicker-free! |
| Linux | Fix offscreen MSAA antialiasing, WindowControllerLinux exposes borderless, transparent, and native drag APIs (PR #192356) |
Vector graphics edges no longer have jagged lines on Linux, and you can draw your own title bar and transparent shaped windows without third-party plugins!!!! |
| Cross-platform multi-window | WindowManager moved out of WidgetsApp |
Secondary windows can now mount a completely independent widget subtree, no longer forced to be under the same WidgetsApp tree as the main window. |
Especially the Linux PR #192356, which now exposes low-level capabilities in WindowControllerLinux:
setDecorated(false): turn off GTK's default system title bar and borders.setAppPaintable(true)andsetBackgroundColor(...): prevent GTK from filling the window with the system theme background color, so Flutter's unpainted areas become truly transparent cutouts.beginMoveDrag()andbeginResizeDrag(WindowDragEdge): hand off mouse press events to the Linux window manager, making it easy to implement custom Flutter widgets for window dragging and eight-direction edge resizing.
Finally, RegularWindow is now renamed to Window, and the shrinkWrap property replaces sizedToContent, aligning with Flutter layout naming. WindowManager now sits above WidgetsApp, so auxiliary windows can run their own independent UI and navigation state.
Web
First, Safari and Firefox now also enable WebAssembly (Wasm), which can double frame rates. Safari 26.2+ (macOS & iOS) and Firefox 147+ can now run Flutter Wasm as well:
In heavy widget stress benchmarks, Safari and Firefox running Wasm achieve frame rates more than 2x the traditional JavaScript compiled mode.
You can now configure flutter_bootstrap.js to add webkit (Safari) and gecko (Firefox) to the wasmAllowList whitelist. When building with flutter build web --wasm, the tool produces both Wasm and JS, falling back seamlessly to JS mode on older browsers.
In addition, old Web libraries are officially deprecated. To fully embrace Wasm, the long-standing dart:html, dart:js, dart:js_util, and package:js are officially marked deprecated and will be removed. All Web interop must use dart:js_interop and package:web.
Then the most practical built-in static asset content hashing: --web-content-hash now automatically fills in:
- Automatic full hashing: computes content hashes for JS/Wasm entry files, Source Maps (PR #193614), and all static assets under
build/web/assets/**, renames them, and injects the hash mapping intoflutter_bootstrap.jsand the asset manifest. - Now you can set
Cache-Control: public, max-age=31536000, immutable(permanent strong caching) on all hashed files on CDN/Nginx, while onlyindex.htmlandflutter_bootstrap.jsneedno-cache. - Automatic Service Worker manifest: automatically generates a precache manifest at
build/web/precache_manifest.json(PR #192953), making it easy to integrate custom Service Worker or Workbox offline pipelines.
Finally, the browser-native text shaping WebParagraph can reduce CanvasKit size by 31%. Why was Flutter Web's first load so slow before? Mostly because the CanvasKit renderer had to bundle an entire C++ font parsing and shaping engine (HarfBuzz / FreeType / ICU data tables) into WebAssembly to lay out text on Canvas. Just canvaskit.wasm was 5.46 MB. And when encountering rare Chinese characters, multilingual content, or new Emoji, it would make network requests to download fallback fonts; until those loaded, the screen was full of "tofu blocks (□)".
{{flutter_js}}
{{flutter_build_config}}
_flutter.loader.load({
config: {
wasmAllowList: {
blink: true,
webkit: true,
gecko: true,
},
},
});
After deep cooperation between the Flutter and Chrome teams, the new WebParagraph based on the browser's Enhanced TextMetrics API is here. Isn't it awesome:
- Let the browser shape system fonts for Canvas: delegate text shaping and measurement to native browser capabilities, then draw to Canvas.
- CanvasKit size drops ~31%: by removing the built-in C++ font parser, CanvasKit download size goes from ~5.46 MB to ~3.75 MB.
- Zero "tofu blocks", native colorful Emoji: directly calls the OS's local font library, no extra fallback font network requests; rare multilingual characters and system-native colored Emoji render directly.
Native Interop Stable
Now the new synchronous FFI native calls are officially Stable:
Basically all three native swords are in place:
FFIgen(officially Stable): automatically scans Apple platform headers and generates Dart binding code to call Objective-C and C APIs. For example, when new iOS system features launch, you can call them on the first day without waiting for third-party plugin updates.JNIgen(officially Stable): automatically scans Android / Java / Kotlin classes and generates Dart binding code to call Android native APIs directly.Pigeon(highly recommended for production): Pigeon's backend now fully usesFFIgenandJNIgento generate bindings. The official core pluginshared_preferenceshas already completed the migration and validation.
Dart 3.14 and Toolchain
First, the new command: dart migrate automatically handles breaking changes. It can automatically upgrade SDK version constraints in pubspec.yaml, fix breaking API changes, and modernize old syntax.
# Preview what upgrades will change without modifying files
dart migrate --dry-run
# After confirming, run one-click migration
dart migrate
Second, dart pub get and Dart Analysis Server are significantly faster. If you've worked in large multi-package projects (monorepo) or with analyzer plugins like Freezed / Riverpod, and the IDE often shows a spinner, or Format on Save takes seconds to respond, this is useful.
Dart 3.14 does deep surgery on the daily toolchain:
dart pub getskips repeated network downloads when package metadata hasn't changed.Find Referencesand startup speed up: file discovery is much faster, especially in multi-package monorepo workspaces with many Analysis Contexts. Restarting the analyzer after modifyingpackage_config.jsonoranalysis_options.yamlis significantly faster.- Analyzer Plugin scopes now converge precisely: the analyzer plugin's analysis root is strictly limited to the Context root where the plugin actually applies, avoiding full scans of unrelated packages. Plugin runtime speed skyrockets.
- Code writing and formatting optimizations:
- Previously background analysis tasks often blocked foreground operations like formatting. Now background tasks have much lower blocking priority.
documentColorrequests are greatly optimized (the LSP request that shows the color swatch next to code lines forColor(0xFF...)). Previously this request often clogged the analysis queue when editing files; now it completes instantly.
Also, as we mentioned before, developers can now place Agent Skills directly in the top-level skills/ directory of a pub.dev package. App developers can use dart run skills@ get to discover and install skills from package dependencies.
Before publishing, authors can run
package:skills_lintin CI to checkSKILL.mdfrontmatter, directory structure, and naming conventions that comply with the Agent Skills specification.
Finally, DartPad is also improved. preview.dartpad.dev has been refactored again:
- Zero remote server waiting (Local WebAssembly Toolchain): previously DartPad sent your code to Google's remote server for compilation and analysis, then sent results back. When the network lagged, even code completion stopped. Now DartPad compiles the Dart compiler, analyzer, and even the
pubclient into WebAssembly, running directly in a local Web Worker thread in your browser. - Supports importing any package from
pub.dev: any web-supported third-party package can be fetched and tried live directly in the browser. - Multi-file project support: no need to stuff all code into one
main.dart.
Deprecations
Because CocoaPods is about to become read-only, users need to migrate to Swift Package Manager (SPM) promptly:
- The deadline is December 2026. After that, CocoaPods Trunk will stop accepting any updates and become read-only.
- In Flutter 3.50, if your project hasn't enabled Swift Package Manager (or manually disabled it), the CLI will print a warning during builds.
Second, Intel Macs are no longer supported. The Flutter team has stopped running CI tests on Intel Macs. When you compile a Flutter 3.50 project on an Intel Mac, the CLI prints a one-time warning. Future versions will completely remove Intel Mac support.
Finally
I feel Flutter 3.50 is the most worthwhile version to upgrade to in recent times. There will still be pitfalls, but at least it's a release that brings huge improvements.
Top 2 from juejin.cn, machine-translated. The original thread is authoritative.
HarmonyOS hasn't adapted material_ui yet 🙄
You are really fast