跪拜 Guibai
← Back to the summary

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:

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:

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 (repaint and semanticsUpdate separated) 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

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):

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:

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:

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:

Second, optimizations for Android GPUs. We've probably discussed these several times recently:

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:

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:

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:

Native Interop Stable

Now the new synchronous FFI native calls are officially Stable:

Basically all three native swords are in place:

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:

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_lint in CI to check SKILL.md frontmatter, directory structure, and naming conventions that comply with the Agent Skills specification.

Finally, DartPad is also improved. preview.dartpad.dev has been refactored again:

Deprecations

Because CocoaPods is about to become read-only, users need to migrate to Swift Package Manager (SPM) promptly:

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.

Comments

Top 2 from juejin.cn, machine-translated. The original thread is authoritative.

清扬婉兮

HarmonyOS hasn't adapted material_ui yet 🙄

栈与烟火

You are really fast