yii.
← Writing
Flutter 技術 · · 16 min read · 閱讀中文版

Your App Might Not Launch This Fall — Flutter 3.47 and Dart 3.13

Material moves out of the SDK, iOS 27 forces UIScene, Impeller lands on desktop. This release isn’t mainly about new features; it moves the foundations. One change has a hard deadline, and another enters deprecation in November.

Sources: What’s new in Flutter 3.47 (Emma Twersky, 2026–08–12)
Announcing Dart 3.13 (Connie Ooi, 2026–08–12)


Preface: This release note needs a different reading#

Most Flutter releases, you skim the headings, note two new widgets, close the tab, and life goes on.

Not this one.

Two dates matter here, and neither is just about missing a feature. Apple sets the hard deadline: your app, built with Xcode 27, will fail on launch if it hasn’t adopted UIScene. Flutter’s next stable release in November starts the formal deprecation of the bundled Material and Cupertino libraries.

I read both official posts end to end, and the conclusion is simple: only two things in this release need a spot on your calendar. Everything else is background noise.

  • Material and Cupertino have moved out of the SDK. November’s Fall stable formally deprecates the built-in versions
  • iOS 27 forces the UIScene lifecycle. An app that hasn’t adopted it will fail on launch when built with Xcode 27

The rest — Impeller on desktop, Wasm progress, Dart’s primary constructors — is all good, and all of it can wait.

This article is ordered by “should you do this now,” not by the order of the official announcements.


1. Material moved out#

A Chip has 2px of extra padding. The Flutter team fixes it. You wait three months to get it.

That’s because package:flutter/material.dart lives inside the SDK, tied to rendering, gestures, and scheduler — the actual core — in the same repo, on the same release cadence. Change one line of Material and it still queues up behind the next quarterly stable, shipping alongside the whole engine.

It cuts the other way too. You want the new NavigationBar, so you have to bump the entire SDK version and take on every engine change that comes with it.

What the Flutter team did in 3.47 is cut that rope.

Key insight: material_ui and cupertino_ui are now on pub.dev at 1.0, on a weekly release cadence, no longer tied to the quarterly SDK release.

This demotes the design system from “part of the framework” to “a user of the framework.” Demotion sounds bad; it’s actually a release. Material gets to evolve at its own pace, and Flutter core stops pretending it knows anything about design.

The official framing is more direct: this paves the way for a style-neutral core widget catalog. In plain terms, future Flutter core won’t assume any design language. Want Material? Install Material. Want your own? Wire up your own. For teams maintaining an in-house design system, the barrier just dropped a lot.

Worth noting: community contributions to Material and Cupertino have been frozen since April, precisely so this move could land cleanly. That freeze is over, and the announcement is explicit: community PRs are open, with weekly releases planned. Moving folders is the surface change. The real one is that an area closed for four months just reopened.

The change in cadence is visible on pub.dev:

There’s roughly four months between 0.0.1 and 0.0.2, and then the gaps collapse: 13 days, 8 days, 1 day. The publisher is the verified flutter.dev, not a community fork.

Where the deadline is#

The core SDK in this release still bundles both libraries. Migration is opt-in, so nothing breaks if you do nothing this month.

The problem is the next release:

The Material / Cupertino bundled into the core SDK are expected to be formally deprecated in November’s Fall stable release.

So from now (mid-August) to November, you have about three months of runway. Three months isn’t tight for a mid-sized app. But if your project leans on a pile of third-party packages that also need to migrate, it might not be enough.

The good news is that the team thought about exactly that.


2. How to migrate, and the bridge that makes it possible#

Migration itself is one command:

dart fix --apply --code=migrate_design_widgets

It rewrites your imports from package:flutter/material.dart and package:flutter/cupertino.dart to the new standalone packages.

The pub.dev readme spells out the steps too:

There’s one known bug flagged officially: if the tool can’t update your pubspec.yaml, add the dependency by hand and run it again.

flutter pub add material_ui       # add cupertino_ui too if you use Cupertino
dart fix --apply --code=migrate_design_widgets

Here’s something the announcement doesn’t mention but pub.dev shows: the Min Dart SDK for material_ui 1.0.0 is 3.12, not 3.13.

Which means you don’t have to be on Flutter 3.47 to start migrating. Flutter 3.44 (Dart 3.12) can take the package. That lets you schedule “migrate the UI packages” and “upgrade the SDK” as two separate pieces of work, with much less risk in each.

The bridge is the real story#

Rewriting imports only solves your own code. What about the 30 third-party packages you depend on? They’re all still on import 'package:flutter/material.dart'.

Historically this had exactly one ending: you’re stuck, waiting for the ecosystem to catch up. Nothing to do but file issues and refresh GitHub.

3.47 ships something called MaterialUiCompatibilityBridge, and it unties the knot:

import 'package:material_ui/material_ui.dart';
 
class MyApp extends StatelessWidget {
  const MyApp({super.key});
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF6750A4)),
      ),
      // This line keeps dependencies on the old imports working
      builder: (BuildContext context, Widget? child) {
        return MaterialUiCompatibilityBridge(child: child!);
      },
      home: const HomeScreen(),
    );
  }
}

One builder wrapper, and your app can move to the standalone packages today while your dependencies take their time.

This design is worth a second look. It turns a large migration from a coordinated ecosystem-wide operation into something each party can finish independently.

Null safety went through the same trick. Mixed mode let un-migrated dependencies keep running, so you didn’t have to wait for the last package author to wake up. Migrations without that kind of transition mechanism usually drag on until everyone forgets about them.

What I’d do first is run the migration on a branch, purely to see two numbers: how many files change, and how many dependencies still pull the old imports. Those two numbers tell you whether this is a “finish it this week” job or a “book a sprint with the PM” job.

Localization got split too#

flutter_localizations is broken up the same way. The Material and Cupertino delegates and translated strings now live in material_ui and cupertino_ui respectively.

The official layer diagram makes it clear which layer was pulled out. Material_UI, Cupertino_UI, and flutter_localizations come out of the Framework layer, and everything below (Widgets, Rendering, Foundation) stays where it was:

The code actually gets simpler. You used to list three delegates:

import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter/material.dart';
 
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
  GlobalCupertinoLocalizations.delegate,
  GlobalMaterialLocalizations.delegate,
  GlobalWidgetsLocalizations.delegate,
],

Now it’s one line:

import 'package:material_ui/material_ui.dart';
 
localizationsDelegates: GlobalMaterialLocalizations.delegates,

Note that’s delegates, plural, not delegate. That getter already bundles the Cupertino and Widgets delegates.

If you maintain a package#

The announcement puts it strongly: treat this migration as a major release.

Not a patch, not a minor. The types on your package’s API surface now come from a different place, and your downstream needs to know.


3. The iOS 27 ultimatum#

Now the urgent one.

Xcode 27, iOS 27, and macOS 27 arrive this fall. And the iOS 27 SDK brings a hard requirement:

Every UIKit-based app must adopt the UIScene lifecycle. An app built with Xcode 27 that hasn’t adopted it will fail on launch.

Notice what kind of statement that is. Not a deprecation warning, not a pub.dev score penalty. Crash on launch.

Good news: most apps need to do nothing. The Flutter CLI handles this migration automatically at build time.

Bad news: there are two cases it can’t handle, and those need manual migration:

  1. Your AppDelegate has custom native code
  2. A plugin you depend on is still on the old application lifecycle

Why this one is worth doing now#

Because neither condition shows up while your CI is still on Xcode 26.

Your CI runs Xcode 26 today, all green. You’ll find out in the fall when Xcode 27 goes GA and the CI image rotates, and that’s likely to be while you’re racing a release deadline.

The second condition is worse, because it isn’t under your control at all. You have to go through your plugins one by one.

The manual steps are in Apple’s TN3187 technote.

So the official recommendation is right, and I’d put it more strongly: run your app against Apple’s beta right now. It’s the one thing in this release I’d say do this week.

Minimum versions went up too#

Supporting Xcode 27 pushed the minimums up:

  • iOS — 13 → 15
  • macOS — 10.15 → 12

If you still have iOS 13/14 users, that’s a conversation with your PM, not a call engineering makes alone.


4. The rest of the Apple reckoning#

Two more things wrap up in the same fall.

Intel Macs start their exit#

Flutter is following Apple toward Apple Silicon:

  • Automated testing on Intel hardware has already stopped
  • The CLI now prints warnings when building on an Intel host, or building both architectures
  • Those warnings will become errors

If you want to cut over early:

flutter config --enable-macos-arm64-only

CocoaPods enters maintenance mode#

Swift Package Manager migration is further along than I expected:

92 of the top 100 iOS plugins have completed their SwiftPM migration.

The official wording has shifted from recommendation to warning: CocoaPods is now in maintenance mode, plugins that haven’t migrated will eventually stop working, and their pub.dev score takes a hit.

If you tried it before, found it flaky, and turned it off, it’s worth another try:

flutter config --enable-swift-package-manager

Build times improved a little along the way, too: community contributor @lukemmtt made the build pipeline filter out unnecessary SwiftPM package schemes earlier.


5. Impeller on desktop, Skia on the clock#

Deadlines done. Here’s the prettiest thing in this release.

First, that stutter you’ve definitely seen#

Your animation hitches the first time it plays, then runs smoothly forever after. You may have assumed it was “warming up,” or “just how debug mode is.”

It isn’t. That’s shader compilation jank.

Skia compiles shaders dynamically at runtime. The first time a shader is needed, it gets compiled, compilation takes time, and that frame is gone. Which is exactly what you observe: rough the first time, smooth afterward, because the shader is cached.

Impeller moves the whole thing to build time:

Impeller precompiles a fixed set of shaders at compile time. No dynamic compilation at runtime. The first frame is already smooth.

This isn’t an optimization, it’s deleting a category of problem. Jitter from dynamic compilation never gets tuned away, it just gets moved to a different moment. Precompilation means the step isn’t there at all.

What changed in 3.47#

Impeller is now the default renderer on macOS, Windows, and Linux, going through each platform’s modern graphics API: Metal on macOS, Vulkan on Windows and Linux.

You can still opt out, but the official language is firm:

macOS   → FLTEnableImpeller = false in Info.plist
Windows → project.set_impeller_switch(flutter::ImpellerSwitch::Disabled) in main.cpp
Linux   → fl_dart_project_set_enable_impeller(project, FALSE) in my_application.cc

“The fallback option will be removed in a future release. If you must fall back to Skia, please file an issue.”

Translation: these switches exist so you can report problems, not so you can depend on them. If you hit something, open an issue rather than quietly turning it off.

Two visual upgrades desktop gets along the way#

  • Wide Gamut Color is on by default on macOS: more saturated, more accurate color on supported hardware
  • Desktop text now renders with SDF (Signed Distance Function): desktop screens usually have lower pixel density than phones but more GPU headroom, and SDF trades that headroom for sharpness — text and vector curves both come out crisper

Put the three together and the signal is clear: Flutter is starting to treat desktop as a first-class high-quality graphics target.

What else desktop picked up#

A few practical desktop items worth a mention:

  • Windows and Linux support flavors now, with assets routed per flavor:
flutter:
  assets:
    - path: assets/flavor_a/images
      flavors:
        - flavor_a
flutter build windows --flavor flavor_a
flutter build linux --flavor flavor_a
  • Multi-window (built with Canonical, still experimental): Linux and Windows support popup windows, which is what you need for native context menus and tool panels. You can also query windowHandle for the underlying native window pointer (HWND / NSWindow / GtkWindow) for deeper integrations like Windows dockable panes

I’d stay conservative on multi-window. The official screenshot still has a DEBUG banner in the corner and shows a reference app rather than a product, which is roughly where this line is in terms of maturity. Shipping a desktop product usually needs exactly this feature, and it’s still tagged experimental. Not something to bet on yet.


6. Wasm gets its last missing piece#

The long-term direction for Flutter Web has been clear for a while: Wasm by default.

It’s still opt-in today:

flutter build web --release --wasm

And the prerequisite is hard, with no room to negotiate:

dart:html and package:js are entirely unsupported on dart2wasm. You must move to package:web + dart:js_interop.

Both legacy libraries were deprecated back in Dart 3.7, and the announcement says dart2js will eventually drop support as well. Most of the time, upgrading your package dependencies resolves it automatically. The painful part is wherever you call those APIs directly yourself.

Why deferred loading is the key#

Wasm had an awkward property: great for small apps, but large apps ended up with an initial bundle so big that first load lost to dart2js.

3.47 / 3.13 fill that gap with deferred loading.

On the Dart side (experimental flag):

dart compile wasm -O2 --enable-deferred-loading

On the Flutter side (main channel, experimental):

flutter build web --release --wasm --enable-wasm-deferred-loading

The official claim is that in large applications using deferred loading, dart2wasm shows significant improvement in initial page load (IPL) compared to dart2js, and that both DOM-based web apps and Flutter web apps benefit.

So Wasm flips from “only pays off for small apps” to “pays off more the larger you get.” That’s the precondition for making it the default. You don’t ship a default that punishes bigger projects.

(One implementation detail: deferred loading on the Dart side needs your embedder to supply a callback that loads the wasm module bytes. The details are in the CompiledApp.instantiate docs inside the generated <app>.mjs.)


7. Dart 3.13: four layers on a diet#

The official theme for Dart 3.13 is “conciseness and simplicity.” Read it through and that turns out to be more than marketing: the language, the formatter, the compiler, and the linker are all genuinely shedding weight.

Language: primary constructors are stable#

After being experimental in 3.12, the syntax is official:

class Point(final int x, final int y);

One line. Fields, types, and constructor, all inside it. The whole block of boilerplate you used to write is gone.

Paired with the concise constructor syntax around the new and factory keywords, an empty declaration body can just end in ;.

What I find more notable is the support around it. Nobody dropped the syntax and walked away. Six lints landed with it, all with auto-fixes:

  • empty_container_bodies — use ; instead of an empty {}
  • initialize_in_field_declaration — move field initialization from the constructor to the declaration
  • unnecessary_const_in_enum_constructor — drop const on enum constructors
  • unnecessary_primary_constructor_body — drop unneeded primary constructor bodies
  • unnecessary_type_name_in_constructor — use new instead of repeating the type name
  • use_declaring_parameters — use a declaring parameter where you can

Plus four IDE refactorings: Convert to primary constructor, Convert to in-body constructor, Convert to declaring parameter, and Move initialization to the field declaration.

Syntax, lints, auto-fixes, and IDE refactorings all at once. That’s a complete migration toolchain, not an isolated piece of syntax sugar.

Formatting: dart format now groups your imports#

I think this is the item with the biggest day-to-day impact in the whole release, because it touches every file you have.

The formatter now inserts blank lines between import “sections” following the Effective Dart rules. Note that it groups but does not sort:

// Before:
import 'dart:io';
import 'dart:math';
import 'package:args/args.dart';
import 'package:test/test.dart';
import 'my_library.dart';
 
// After:
import 'dart:io';
import 'dart:math';
 
import 'package:args/args.dart';
import 'package:test/test.dart';
 
import 'my_library.dart';

Two other visible changes:

First, an optimization bug got fixed. A method call wrapping a large collection literal used to format wrong:

// Before:
await MethodChannelContainer()
    .onMethodChannelInvoke('reportCrash', <String, Object?>{
      'time': nowTime,
      'errorValue': errorName,
    });
 
// After:
await MethodChannelContainer().onMethodChannelInvoke(
  'reportCrash',
  <String, Object?>{
    'time': nowTime,
    'errorValue': errorName,
  },
);

Second, the line-breaking heuristic for method chains changed. When the chain’s target is a collection literal or function call with a single element or argument, the formatter now prefers to split the chain rather than the target:

// Before, splitting the target:
function(
  argument,
).method().another();
 
// After, splitting the chain:
function(argument)
    .method()
    .another();

One practical trap here: apart from that bug fix, these formatting changes are language-versioned, meaning they only take effect once your code moves to Dart 3.13.

In other words, your first commit after upgrading will probably be a whole-repo reformat. Land it as its own formatting-only commit, separate from any functional change, or code review turns into a disaster.

The team acknowledges formatting changes cause churn, which is why they’re conservative about them. But there’s a line in the announcement I liked: in an era where generative AI means people are reviewing more code than ever, readability improvements have real value.

Linking: native libraries can finally be tree-shaken#

This is the most structural change in the release.

The long-standing situation: you bind a native library through dart:ffi and Code Assets (SQLite, crypto, image decoding, an audio engine), and the Dart compiler tree-shakes away the Dart wrapper functions you don’t use, while the native binary underneath ships whole, untouched.

Put it this way: SQLite exposes hundreds of functions, your app might touch a handful, and the machine code for the rest is still sitting in your bundle.

@RecordUse() (in package:meta) plus package:record_use solves it:

Step one, mark the FFI binding (bindings generated by ffigen get marked automatically, so you usually don’t write this by hand):

import 'dart:ffi';
import 'package:meta/meta.dart';
 
@RecordUse()
@Native<Int32 Function(Int32, Int32)>()
external int sqlite3_open(
  Pointer<Utf8> filename,
  Pointer<Pointer<sqlite3>> ppDb,
);

Step two, the compiler tracks reachability. During whole-program compilation and tree-shaking in an AOT build, it records which marked bindings are actually reached from executable code. Calls sitting inside already tree-shaken Dart code are excluded automatically.

Step three, the link hook trims native symbols. In your hook/link.dart, read input.recordedUses and tell the native toolchain to keep only the symbols Dart actually calls:

void main(List<String> arguments) async {
  await link(arguments, (input, output) async {
    // Pull out the symbols actually called from reachable Dart code
    final symbolsToKeep = input.recordedUses?.calls.keys
        .cast<Method>()
        .map((method) => recordUseMapping[method.name]!);
 
    await cLibrary.link(
      input: input,
      output: output,
      linkerOptions: LinkerOptions.treeshake(
        symbolsToKeep: symbolsToKeep,
      ),
    );
  });
}

The prettiest case is this one: if your app never calls a package’s native bindings at all, the link hook drops the entire native binary from the final bundle.

To be clear about the audience: this is primarily a feature for package authors, not app developers. You add the annotations, you write the lookup logic in the link hook. App-side, you just receive the benefit.

What it actually means, though, is that the cost of pulling in a heavyweight native dependency is tied to how much of it you use, for the first time.

Inside the engine#

The announcement closes with three months of under-the-hood work. Two are worth remembering:

  • A type promotion unsoundness was fixed (a rare case involving nested functions). That’s a correctness issue in the type system, not a performance tweak.
  • A memory cage was added around the Dart heap, hardening memory safety in the native runtime.

Two directional signals as well: DDC is unifying its module system (clearing out legacy), and there’s early experimentation with dynamic modules (dynamic code linking), where the use case described is workflow improvement like quickly sharing prototypes within a team. Worth watching.


8. How to schedule the next three months#

Everything above, compressed into an order of operations:

This week (highest risk, an Xcode 26 CI run can’t catch it)

  1. Run your app against the Xcode 27 / iOS 27 beta and confirm UIScene is fine
  2. Check whether your AppDelegate has custom native code (if you need the manual migration, follow Apple TN3187)
  3. Inventory your plugins for anything still on the old application lifecycle
  4. Confirm with your PM how many users the iOS 15 / macOS 12 minimums affect

This month (November deadline)

  1. Run dart fix --apply --code=migrate_design_widgets on a branch to size the damage
  2. Decide whether to migrate early behind MaterialUiCompatibilityBridge (remember: Min Dart SDK is only 3.12, no need to wait for 3.47)
  3. If you maintain a package, schedule a major release
  4. While you’re there, switch localization to GlobalMaterialLocalizations.delegates

This quarter (no hard deadline, but the direction is clear)

  1. Try SwiftPM again (flutter config --enable-swift-package-manager)
  2. Build web once with --wasm and see how much legacy interop is left
  3. Move to Dart 3.13, and land the whole-repo dart format reformat as its own commit
  4. Intel Mac dev machines: start planning replacements

Just be aware

  1. Impeller default on desktop, Wide Gamut, SDF text — automatic changes, but still worth a rendering smoke test
  2. Widget Previews are stable, with a local cache (.widget_preview/), PreviewThemeData for multi-theme matrix testing, and automatic web/ asset syncing when previewing web widgets
  3. Android dependency matrix: Java 17, KGP 2.4.0, AGP 9.1.0, Gradle 9.3.1; compileSdk / targetSdk = 36, minSdk = 24
  4. Multi-window is still experimental — wait and see

9. Wrapping up#

Flutter 3.47 presents itself as “modular by design.” What it actually is, is a foundation move: Material goes from part of the framework to a user of the framework, Skia goes from default renderer to transitional option, and Web shifts from JS toward Wasm. Add Dart 3.13 trimming weight across the language, formatter, compiler, and linker, and the whole platform is converging on a smaller core with more flexible edges.

For your work this month, though, it comes down to two sentences:

The iOS 27 UIScene requirement is a hard failure, and CI on Xcode 26 can’t catch it. Verify against the beta now.

The bundled Material libraries enter formal deprecation in November, but MaterialUiCompatibilityBridge means you don’t have to wait for the ecosystem. You can start today.

Everything else can wait.

Before upgrading, it’s worth scanning the official Breaking Changes page along with both original announcements: What’s new in Flutter 3.47 and Announcing Dart 3.13.


Resources#

本文原刊登於 Medium。