← All writing

Flutter 3.47: why Material and Cupertino left the SDK

Flutter 3.47 moves Material and Cupertino into their own packages, makes Impeller the desktop default, and stabilizes Widget Previews. Here is the problem each change solves, and what the November release is set to change next.

Flutter 3.47 shipped on August 12, 2026, with Dart 3.13. As of October 1, 2026, the latest stable build on that line is Flutter 3.47.6 with Dart 3.13.5. The later 3.47 builds are maintenance releases. The structural changes below arrived in 3.47.0.

The release is less about a new widget and more about where Flutter’s pieces live. Design libraries can update on their own, desktop rendering no longer compiles shaders the first time an animation plays, and a single widget can be previewed without launching the whole app.

A developer at a desk with a laptop and a phone showing a mobile app
Flutter 3.47 separates the design libraries from the SDK so each can move on its own schedule.

Dart 3.13 also made primary constructors stable. That post covers the language change. This one stays with the framework: why each Flutter 3.47 change exists, what it fixes, and what the team has already scheduled next.

The problem with design libraries inside the SDK

Material and Cupertino have always been part of package:flutter. That made the first flutter create project feel complete, and it also tied every button fix to the framework’s quarterly stable cycle. A small Cupertino correction waited for the same release train as the engine, the tool, and every other library.

The Flutter team froze contributions to those libraries inside flutter/flutter on April 7, 2026, then republished them. In 3.47, material_ui and cupertino_ui reached 1.0 on pub.dev. The 3.47 announcement is explicit about the goal: bug fixes and new components can ship on a weekly package schedule, and an app can pick up a newer Material style without upgrading the entire SDK.

The copies inside the SDK still work in 3.47. Moving is opt-in for this release. The same post schedules formal deprecation of those in-SDK libraries for the fall stable release in November.

How the move works

Add the packages and let the migration fix rewrite imports:

flutter pub add material_ui
dart fix --apply --code=migrate_design_widgets

Add cupertino_ui the same way if the project imports Cupertino. The fix rewrites package:flutter/material.dart and package:flutter/cupertino.dart to the new libraries. The 3.47 post notes an early bug where pubspec.yaml was not always updated; if the fix complains, add the packages yourself and run it again.

A plugin that still imports the SDK copy can sit next to an app that has already moved. Wrap the tree in the bridge the package ships for that gap:

import 'package:material_ui/material_ui.dart';

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: (context, child) {
        return MaterialUiCompatibilityBridge(child: child!);
      },
      home: const HomeScreen(),
    );
  }
}

Localization moved with the widgets. GlobalMaterialLocalizations.delegates now includes the Material, Cupertino, and Widgets delegates, so a screen that used to list three delegates can use that one list. Treat a package migration as a major version: the widgets are the same libraries, and the import path is the public change.

Impeller on desktop, and the stutter it removes

Impeller was already the only renderer on iOS. On desktop, the first time a new animation ran, Skia could compile that shader while the frame was on screen. That hitch is shader compilation jank. Impeller compiles a fixed set of shaders at build time and targets Metal on macOS and Vulkan on Windows and Linux, so the first frame of an animation uses the same path as the hundredth.

Flutter 3.47 turns Impeller on by default for macOS, Windows, and Linux. Wide-gamut color is also on by default on macOS, and desktop text uses signed-distance-field rendering so curves stay sharper on lower-density monitors.

If a desktop app still has to fall back, the switches are FLTEnableImpeller set to false in the macOS Info.plist, ImpellerSwitch::Disabled in the Windows runner, and fl_dart_project_set_enable_impeller(project, FALSE) on Linux. The release post says those fallbacks will be removed later, so a Skia-only bug is worth filing now.

Widget Previews, for the screen you are actually editing

Opening a full app to check one button means waiting on startup, navigation, and whatever state that screen needs. Widget Previews, experimental since 3.35, are stable in 3.47. You annotate a top-level function, a static method, or a public widget constructor that has no required arguments, and the previewer renders that widget on its own.

import 'package:flutter/widget_previews.dart';
import 'package:material_ui/material_ui.dart';

@Preview(name: 'Submit button', group: 'Form controls')
Widget submitButtonPreview() {
  return const SubmitButton();
}

The stable tool caches project setup in .widget_preview/, so the next open skips repeated work. Themes can be layered for a small matrix of light, dark, and brand variants, and web previews copy the host project’s web/ assets. The Widget Previewer guide has the annotation rules.

Apple’s next SDK, and the versions Flutter now requires

Xcode 27, iOS 27, and macOS 27 are due this fall. Apps built with the iOS 27 SDK have to use the UIScene lifecycle. An app that stays on the older application lifecycle fails to launch. For a stock Flutter iOS runner, the CLI migrates this during the build. A custom AppDelegate, or a plugin that still uses the old lifecycle, needs the manual steps in the UIScene adoption guide.

Supporting that SDK also raised the floor:

Platform Before 3.47 Flutter 3.47
iOS 13 15
macOS 10.15 12

Flutter is also winding down Intel Macs. Test runs on Intel hardware are off, and the CLI warns when you build on an Intel host or target both architectures. Those warnings become errors in a later release. flutter config --enable-macos-arm64-only opts into an Apple-silicon-only macOS build now.

Swift Package Manager has been the iOS and macOS default since 3.44. By the 3.47 post, 92 of the top 100 iOS plugins had migrated. CocoaPods is in maintenance, unmigrated plugins score lower on pub.dev, and a plugin that never moves will eventually stop building. If you turned SwiftPM off earlier, flutter config --enable-swift-package-manager turns it back on.

WebAssembly is opt-in, and the default is the destination

The 2026 roadmap says Wasm should become the default Flutter web build, for startup and runtime that feel closer to a native app. 3.47 does not flip that switch. You opt in per build:

flutter build web --release --wasm

Wasm compiles through dart2wasm, which does not support dart:html or package:js. Those libraries were deprecated in Dart 3.7. App code and plugins need package:web and dart:js_interop. Upgrading dependencies often clears this, because many packages have already moved.

Large apps also pay for shipping one Wasm module up front. Deferred loading is in preview. On the main channel, flutter build web --release --wasm --enable-wasm-deferred-loading splits the app into modules that load when a deferred library is first used. The embedder has to load those module bytes. Treat it as an experiment, and measure the initial load before you depend on it.

What is already on the calendar

The release schedule lists Flutter 3.50 with a branch cutoff of October 6, 2026 and a stable target in November. The 3.47 post ties that fall release to deprecating Material and Cupertino inside the SDK. The practical window for an app is this one: move imports while both paths still compile, and see which plugins still need the compatibility bridge.

A few items from the February roadmap are still ahead of stable, so they are plans until a release note says otherwise:

  • Impeller’s Android migration, including removal of the legacy Skia backend on Android 10 and above.
  • Wasm as the default flutter build web, once package:web is the normal interop path.
  • Dart augmentations, meant to make generated code less of a second file you have to keep in sync. Primary constructors already landed; augmentations have not.
  • More of the experimental desktop windowing work Canonical is leading. 3.47 already added popup windows on Linux and Windows, a windowHandle for the native window, and a sized-to-content API. Multi-window is usable for experiments, and it is still marked experimental.

genui 0.10 is a package update alongside this SDK, not a framework API. It splits protocol types into a2ui_core and lets an agent call client-side functions for validation and derived values. That matters if you are building agent-driven screens. It does not change a normal MaterialApp.

A sensible upgrade

  1. Run flutter upgrade and read the 3.47 breaking changes.
  2. Confirm the app’s iOS deployment target is at least 15, and the macOS target is at least 12.
  3. Build an iOS archive against the current Xcode you ship with, and try an Xcode 27 beta if you can, specifically to see whether UIScene migration is automatic for your runner.
  4. On a branch, add material_ui and run dart fix --apply --code=migrate_design_widgets. Keep the compatibility bridge until every direct dependency has moved.
  5. Run the desktop app once under Impeller and watch the first animation. File a bug if you still need the Skia switch.
  6. For web, try one --wasm release build and fix any remaining dart:html imports.

Android projects should also match the versions Flutter 3.47 was verified against: Java 17, Kotlin Gradle Plugin 2.4.0, Android Gradle Plugin 9.1.0, and Gradle 9.3.1. Prefer flutter.compileSdkVersion, flutter.targetSdkVersion, and flutter.minSdkVersion in Gradle files. In this release those defaults are API 36, API 36, and API 24.

Dart 3.13, bundled with this SDK, is worth a formatter pass too. At language version 3.13, dart format separates dart:, package:, and relative imports with blank lines and changes how some method chains wrap. The Dart 3.13 announcement also covers @RecordUse, which lets a native-assets link hook drop unused C, C++, or Rust symbols from the release binary. That is a binary-size fix for FFI packages, and it only helps once those bindings are annotated.

Flutter 3.47 asks you to decide which design system you depend on, then lets that system ship on its own. The November deprecation is the date that makes the decision urgent. The rest of the release — Impeller on desktop, stable previews, and a Wasm build you can opt into — is there so the app you ship in the meantime feels smoother while the framework gets smaller.

Something to add? I’d love to hear it.

Let’s talk about it ↗