Flutter 3.47 and Dart 3.13: Every Change That Actually Affects Your Codebase
- 5 hours ago
- 11 min read
For eight years, the first line of almost every Flutter file has been the same: import 'package:flutter/material.dart'. In November, that line gets formally deprecated.
On August 12, 2026, Flutter 3.47 and Dart 3.13 landed together, and the headline is that Material and Cupertino no longer live inside the SDK. They now ship as standalone packages on pub.dev, on their own release schedule, with their own version numbers. That single decision changes how you upgrade, how you pin dependencies, and how fast new components reach your app.

But the design system split is only the part with a countdown attached to it. This release also makes Impeller the default renderer on every desktop platform, raises the minimum iOS version to 15, and quietly sets a trap for anyone who builds with Xcode 27 this fall. Dart 3.13 adds the biggest syntax change the language has seen in years, plus two features that shrink your compiled bundle without touching a line of your logic. Here is what actually changed, what it costs you, and what to do about it before November.
The SDK Splits Apart: Material and Cupertino Become Packages
Flutter always shipped with batteries included. Two complete design systems – Material and Cupertino – lived inside the framework, pixel-perfect and zero-config. Nobody thought about it, which was the point.
Flutter 3.47 ends that arrangement. The material_ui and cupertino_ui packages hit version 1.0 on pub.dev, and the design systems are now decoupled from the core SDK.

The widgets themselves did not change. What changed is where they live and how fast they move:
Before 3.47 | After 3.47 | |
Location | Bundled in the SDK | Packages on pub.dev |
Release cadence | Quarterly, with the SDK | Weekly, independent |
Adopting a fix | Upgrade the whole SDK | Bump one dependency |
Community PRs | Frozen since April | Open again |
The practical payoff: a bug fix in a Slider no longer waits three months for the next SDK train.
There is a longer-term motive too. Flutter's base widgets – Row, Stack, GestureDetector – have no opinion about design language. Separating the opinionated layer from the neutral one lays the groundwork for a style-neutral core widget catalog, making custom design systems a supported path rather than a fight against defaults.
The deadline. The core SDK still includes the old libraries in 3.47, so nothing breaks on upgrade day. But the in-SDK versions are formally deprecated in the November stable release – roughly one quarter to migrate on your own terms. Maintain a package rather than an app? Treat the move as a major version bump – your consumers need the signal.
How much this release gives you depends on how you use Material today. Teams that wrap it in a house design system get the most: weekly upstream releases mean adopting a fix in days instead of quarters. Teams running stock widgets on a twice-a-year upgrade cycle mostly inherit a migration task and one more dependency to pin. The conclusion is identical in both cases, though – do this in September, not in November.
Migrating Without Breaking Your Dependency Tree
The migration is mostly automated. One command rewrites your imports across the project:
dart fix --apply --code=migrate_design_widgetsIt swaps package:flutter/material.dart and package:flutter/cupertino.dart for the standalone packages. There is one known early bug: the tool sometimes fails to update pubspec.yaml. If that happens, add the dependencies by hand and run the fix again:
flutter pub add material_ui
flutter pub add cupertino_ui
dart fix --applyRun this on a clean working tree, in its own branch, as a single commit. A rewrite that touches every file in the project is not something you want tangled with feature work during review.

The dependency problem, solved. The obvious blocker is that your app does not migrate alone – it drags a dependency tree behind it. If three plugins still import from the core SDK, you are stuck waiting on maintainers you do not control.
MaterialUICompatibilityBridge removes that blocker. Wrap your app once and migrate immediately, even while parts of your tree are still on legacy imports:
import 'package:material_ui/material_ui.dart';
class DeliveryApp extends StatelessWidget {
const DeliveryApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Route Planner',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1B6B4C)),
),
builder: (context, child) =>
MaterialUiCompatibilityBridge(child: child!),
home: const ActiveOrdersScreen(),
);
}
}Localizations moved too. flutter_localizations has been unbundled – Material and Cupertino delegates and translations now live inside their respective packages. The setup gets shorter, because GlobalMaterialLocalizations.delegates now includes the Cupertino and Widgets delegates as well:
// Before
import 'package:flutter_localizations/flutter_localizations.dart';
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
// After
import 'package:material_ui/material_ui.dart';
localizationsDelegates: GlobalMaterialLocalizations.delegates,Small change, but it invalidates the localization setup in every tutorial written before August – worth knowing before you debug why a copy-pasted snippet no longer compiles.
The order that works in practice: upgrade to 3.47 first and confirm the app runs unchanged, then migrate imports in a separate commit, then add the bridge only if your dependency audit turns up laggards. Doing all three at once makes a failure impossible to attribute.
Impeller Becomes the Desktop Default
Impeller has been Flutter's answer to stutter since it replaced Skia on iOS, then Android. In 3.47 it becomes the default renderer on macOS, Windows, and Linux – the last platforms still running the legacy pipeline.
The difference is in when shaders get compiled:
Skia | Impeller | |
Shader compilation | At runtime, on first use | At build time, fixed set |
First animation | Brief stutter | Smooth from frame one |
Graphics API | Abstracted | Metal, Vulkan directly |
That runtime compilation is the source of shader compilation jank – the hitch users see the first time a transition plays, and the one that never reproduces on your machine because you already warmed the cache.
Two smaller changes matter more than their line count suggests. Desktop screens have lower pixel density but more compute headroom than phones, so the engine now uses Signed Distance Function rendering for text on all three desktop platforms – sharper glyphs and cleaner vector curves. And wide gamut color is on by default on macOS, which is why Flutter desktop apps stop looking subtly washed out next to native ones.
If you need to opt out, the switch is per-platform:
macOS → FLTEnableImpeller = false in Info.plist
Windows → project.set_impeller_switch(ImpellerSwitch::Disabled) in main.cpp
Linux → fl_dart_project_set_enable_impeller(project, FALSE)Treat these as a temporary escape hatch, not a configuration choice. The fallback path will be removed in a future release, and the team is explicit that reverting to Skia should come with a filed bug rather than a silent workaround.
The two complaints that followed Flutter desktop for years were jank on first animation and text that looked almost right. This release addresses both, and it does so by default rather than behind a flag – which is the part worth noticing. If desktop was on your roadmap and got deferred on rendering quality, the objection is now materially weaker than it was in July.
Apple's Autumn Deadline
Most of this release is opt-in. This chapter is not.
Xcode 27, iOS 27, and macOS 27 arrive this fall, and Flutter 3.47 raises its minimum supported OS versions to match:
Platform | Previous minimum | Flutter 3.47+ |
iOS | 13 | 15 |
macOs | 10.15 | 12 |
Check your analytics before assuming this is free. If you still serve users on iOS 13 or 14, upgrading the SDK drops them – that is a product decision, not just a build setting.

The part that actually breaks apps. The iOS 27 SDK mandates the UIScene lifecycle for all UIKit-based apps. Apps built with Xcode 27 that have not adopted it fail to launch on startup – not a warning, not a degraded experience. They do not start.
For most projects the Flutter CLI handles this migration automatically during the build, and you will never notice. Manual migration is required in two cases:
Your AppDelegate contains custom native code
You depend on plugins still using the legacy application lifecycle
Both are common in apps older than a couple of years – push notification handling, deep link interception, and analytics initialization all tend to live in AppDelegate.
The recommendation from the Flutter team is to test against the Apple betas now, and it is worth taking literally. The failure mode here is not a subtle regression that surfaces in a support ticket; it is a launch crash discovered by whoever builds the release candidate in late September. Finding it in August costs an afternoon. Finding it during release week costs the release.
SwiftPM, CocoaPods and Intel Macs
Two long-running deprecations moved from "eventually" to "start planning" in this release.
CocoaPods is now in maintenance mode. The ecosystem has largely moved on: 92 of the top 100 iOS plugins have migrated to Swift Package Manager. If you turned SwiftPM off earlier, it is worth another try:
flutter config --enable-swift-package-managerThe consequences for plugins that stay behind are concrete:
Unmigrated plugins will eventually stop working
They already receive lower pub.dev scores
That second point gives you a free audit heuristic. A dependency with a suddenly dropped pub.dev score is telling you something about its maintenance status before its build starts failing.
Intel Macs are winding down. Flutter is aligning with Apple's transition to Apple Silicon:
Automated test runs on Intel hardware are disabled
The CLI prints warnings when building on Intel hosts or targeting dual architectures
Those warnings become errors in a future release
You can move ahead of it today:
flutter config --enable-macos-arm64-onlyThe warnings are easy to ignore locally, which is exactly why this deserves a calendar entry rather than a mental note. The place it bites is CI – a build farm still running Intel runners will keep working until one release turns those warnings into failures, and by then the fix involves procurement rather than a config flag. An inventory of your runners takes ten minutes now and saves a blocked pipeline later.
Dart 3.13: Primary Constructors Go Stable
Every Dart developer has written the same class three times over: once as field declarations, once as constructor parameters, once as assignments. Primary constructors, experimental in 3.12, are stable in Dart 3.13 – and they collapse all three into the declaration header.
A shipping address model, written the old way:
class ShippingAddress {
final String street;
final String city;
final String postalCode;
final bool isDefault;
const ShippingAddress(
this.street,
this.city,
this.postalCode, {
this.isDefault = false,
});
}The same class in 3.13:
class ShippingAddress(
final String street,
final String city,
final String postalCode, {
final bool isDefault = false,
});Eleven lines down to six, with the field names written once instead of three times. The trailing ";" replaces an empty body – part of the same concise constructor syntax that also applies to "new" and "factory" declarations.
The tooling shipped with the feature, which is the part that makes adoption realistic. Six new lints with automated fixes:
Lint | What it encourages |
empty_container_bodies | ; instead of {} |
initialize_in_field_declaration | Initialization at the field, not the constructor |
unnecessary_const_in_enum_constructor | Dropping redundant const |
unnecessary_primary_constructor_body | Removing empty bodies |
unnecessary_type_name_in_constructor | new instead of the type name |
use_declaring_parameters | Declaring parameters where possible |
Plus four IDE refactorings, including conversion in both directions – so a class can be moved to a primary constructor and back if the shape stops fitting.
The formatter changed too, and this one lands in every diff. dart format now inserts a blank line between import sections following Effective Dart ordering, and uses new heuristics for where to split method chains. Both changes are language-versioned: they appear only after you raise your code to 3.13.
// Before
import 'dart:io';
import 'package:http/http.dart';
import 'src/order_repository.dart';
// After
import 'dart:io';
import 'package:http/http.dart';
import 'src/order_repository.dart';Adopt the language version in one commit and reformat in the next one, separately from any feature work. A pull request that mixes logic changes with a project-wide reformat is unreviewable, and the reviewer will either rubber-stamp it or send it back – neither outcome is useful.
Primary constructors are worth adopting where they fit naturally: data models, value objects, small immutable types. They are not an invitation to compress every class in the codebase into a header. The classes that carry real construction logic – validation, computed defaults, asserts – still read better with a body, and the fact that the refactoring works in both directions is a hint that the team expects mixed usage rather than wholesale conversion.
Two Changes That Shrink Your Bundle
Dart has always tree-shaken unused Dart code. Native binaries were the exception – link a package wrapping SQLite and you shipped all of SQLite, whether you called three functions or three hundred.
Dart 3.13 closes that gap. With @RecordUse from package:meta and package:record_use, the compiler tracks which native bindings are actually reachable, and the link hook strips the rest.
Step one – annotate the FFI bindings:
import 'dart:ffi';
import 'package:meta/meta.dart';
@RecordUse()
@Native<Pointer<Void> Function(Pointer<Utf8>)>()
external Pointer<Void> image_decoder_open(Pointer<Utf8> path);
@RecordUse()
@Native<Int32 Function(Pointer<Void>)>()
external int image_decoder_close(Pointer<Void> handle);Step two – prune unused symbols in hook/link.dart:
import 'package:hooks/hooks.dart';
import 'package:native_toolchain_c/native_toolchain_c.dart';
import 'package:record_use/record_use.dart';
void main(List<String> arguments) async {
await link(arguments, (input, output) async {
final reachable = input.recordedUses?.calls.keys
.cast<Method>()
.map((method) => symbolMapping[method.name]!);
await decoderLibrary.link(
input: input,
output: output,
linkerOptions: LinkerOptions.treeshake(symbolsToKeep: reachable),
);
});
}Where this pays off:
Dependency type | Typical exposure | After tree-shaking |
SQLite, crypto | Hundreds of exported functions | Only the ones you call |
Image, audio codecs | Full codec matrix | Reachable paths only |
Unused native package | Full binary in the bundle | Dropped entirely |
That last row is the one worth reading twice: if your app never invokes a package's native bindings, the link hook drops the binary completely. Tooling cooperates too – ffigen annotates generated bindings automatically.
On the web, the lever is deferred loading. dart2wasm gets a preview of deferred loading, which splits a Wasm application into lazily loaded modules. On large apps it delivers significant initial page load improvements over dart2js:
dart compile wasm -O2 --enable-deferred-loadingFlutter exposes the same capability behind a flag on the main channel:
flutter build web --release --wasm --enable-wasm-deferred-loadingThe catch is the same one that has gated Wasm all along: dart:html and package:js are not supported, so the path runs through package:web and dart:js_interop. Upgrading dependencies resolves most of it automatically.
These two features sit at opposite ends of the readiness spectrum. Native tree-shaking is production-ready and pays off immediately for any app with heavy FFI dependencies – annotate, link, measure. Wasm deferred loading is a preview behind an experimental flag, worth benchmarking on a branch this quarter rather than planning a release around. Both point the same direction, though: the team is optimizing for what ships, not just what compiles.
Your Upgrade Checklist for This Quarter
Everything above, ordered by deadline rather than by topic.
This week – low risk, immediate payoff
Run flutter upgrade and confirm the app builds and runs unchanged
Turn Swift Package Manager back on if you disabled it: flutter config --enable-swift-package-manager
Audit dependencies for plugins with dropped pub.dev scores – the SwiftPM signal
Inventory your CI runners for Intel hardware

Before September – the Apple track
Check analytics against the new floors: iOS 15, macOS 12
Build against the Xcode 27 beta and verify the app launches
Migrate UIScene manually if you have custom AppDelegate code or legacy-lifecycle plugins
Opt into ARM64-only macOS builds: flutter config --enable-macos-arm64-only
Before November – the design system track
Run dart fix --apply --code=migrate_design_widgets on a clean branch
Add MaterialUiCompatibilityBridge if dependencies still use core SDK imports
Update localization delegates to GlobalMaterialLocalizations.delegates
Bump a major version if you maintain a package
Optional, measurable wins
Annotate FFI bindings with @RecordUse and measure the bundle delta
Benchmark Wasm deferred loading on a branch
Adopt primary constructors in data models, with the reformat in its own commit
The two tracks are independent, and the Apple one is less forgiving – a missed UIScene migration breaks launches, while a missed design system migration produces deprecation warnings you can still act on. Sequence accordingly.
Conclusion
Three threads run through the August 2026 release.
The SDK is becoming modular. Material and Cupertino now ship as material_ui and cupertino_ui on pub.dev, releasing weekly instead of quarterly – with the in-SDK versions deprecated in November.
Performance parity is done. Impeller is the default renderer on every desktop platform, which retires shader compilation jank across all targets and closes the gap that made desktop builds look almost, but not quite, native.
Code and bundles get lighter. Primary constructors collapse three declarations into one header, and @RecordUse strips unused native symbols – or entire unreferenced libraries – from the final binary.
The one non-optional item is Apple's timeline: iOS 15 and macOS 12 minimums, plus a mandatory UIScene migration that will otherwise stop apps from launching under Xcode 27.
Two deadlines, one quarter. September is a planned task. November is a fire drill during release week.
Don't Let November Set Your Schedule
Migrations like this one rarely fail on the technical details – they fail on timing, on the plugin nobody audited, on the AppDelegate nobody opened in three years. If you would rather find those in August than during release week, that is the kind of work our team does well.
Get in touch – let's get your codebase ready before the deadlines set the schedule for you.






Comments