Mert Özen Further With Every Line
Frontend Mobile with Flutter — #17

What Changed in Flutter 3 and Dart 3: Impeller, Patterns, Widget Previews, material_ui and What's Next

Mert Özen Nov 15, 2026 8 min 5 views
What Changed in Flutter 3 and Dart 3: Impeller, Patterns, Widget Previews, material_ui and What's Next

Dart 3.0 made null safety mandatory and brought records, patterns and sealed classes; 3.6 to 3.13 took small steps that change everyday code, from digit separators to primary constructors. On the Flutter side there are Impeller, SwiftPM, UIScene, Widget Previews and, between 3.44 and 3.50, Material moving into the material_ui package; we show with real output that the language version comes from the pubspec, and how to upgrade.

Version note: The examples in this post were run with Flutter 3.47.8 (Dart 3.13.7). Flutter 3.50 (Dart 3.14) became stable on 11 November 2026; everything about 3.50 comes from the Flutter and Dart docs and was not run with the 3.50 SDK. The version information was checked on 15 November 2026 against dart.dev's language evolution page, Flutter's breaking changes page and the Flutter release list.

Guess First

Since the fourth post, the Dart samples of Bookstore, the online bookstore used as the shared example across this site's series, contain this line:

class const Publisher(final String name, {final String country = 'GB'});

What happens if you try to compile the same file with the Dart 3.12 language version, while the installed SDK stays the same? The SDK is 3.13.7, so the feature is already in it; will the compiler accept it?

Dart 3 at a Glance

Dart 3.0 came in May 2023 with Flutter 3.10 and was the biggest change to the language: sound null safety became mandatory, and code without null safety no longer compiles. The same release brought records, patterns, switch expressions and class modifiers. Later releases took smaller steps that still change everyday code; according to dart.dev's language evolution page:

DartDateLanguage featureIn the series
3.0May 2023records, patterns, switch expressions, class modifiers (sealed, final, base, interface)03, 04, the JSON conversion in 13
3.2November 2023promotion of private final fields after a null checkthis post's sample
3.3February 2024extension typesMoney
3.6December 2024digit separators (100_000)03
3.7February 2025wildcard variables (_ does not bind)03
3.8May 2025null-aware elements (?subtitle)03
3.10November 2025dot shorthands (.paperback)04, Book.format
3.12May 2026private named parameters (required this._remote)04, 14
3.13August 2026primary constructors04
3.14November 2026no new language feature-

The releases in between (3.1, 3.4, 3.5, 3.9, 3.11) have no new language features; they bring type analysis and tooling improvements. We collected all of them in one file, dart_samples/bin/dart3_tour.dart, with each block naming the version that brought it. A part of it:

// 3.13: primary constructor; 3.12: private named parameter
class const Book(
  final String title,
  final Money price, {
  final String? subtitle,
});

class Cart {
  Cart({required this._lines});
  final List<(Book, int)> _lines;

  // 3.2: a private final field is promoted after a null check
  final String? _coupon = null;
  String get coupon => _coupon != null ? _coupon.toUpperCase() : 'none';
dart run bin/dart3_tour.dart
3.0  3 books, placed, shipped (TR123456)
3.2  coupon: none
3.3  total 1298.00 TRY, under free shipping: false
3.7  forEach((_, _) => ...) ran 2 times
3.8  [Dune, The Hobbit, There and Back Again]
3.10 shipping 49.90 TRY
3.13 Dune 489.00, const: true

_coupon.toUpperCase() on the 3.2 line compiles without !: since the field is private and final, the compiler knows the value cannot change after the null check. Before 3.2, this line needed a copy into a local variable. The (_, _) on the 3.7 line used to be a "name defined twice" error.

The Language Version Comes from the pubspec

The answer to "Guess First": no. We added // @dart=3.12 at the top of the file and ran analyze:

error - zz_langver_tmp.dart:26:17 - This requires the 'primary-constructors' language feature to be enabled. Try updating your pubspec.yaml to set the minimum SDK constraint to 3.13.0 or higher, and running 'pub get'. - experiment_not_enabled

In Dart, the language version does not come from the installed SDK but from the lower bound of environment: sdk: in the package's pubspec.yaml. A package that says sdk: ^3.13.0 is compiled with the 3.13 language; a dependency that says ^3.8.0 is compiled with the 3.8 rules on the same SDK. That is how old packages keep working on a new SDK and how every package moves to new features at its own pace. It is similar to LangVersion in C#, but the default comes from the SDK lower bound in the project file. The practical consequence: to use a new language feature, updating the SDK is not enough; you also have to raise the lower bound in the pubspec.

Flutter 3: Rendering and the Platform Side

Flutter 3.0 came out in May 2022; the important changes since then are mostly in the engine and in platform integration:

  • Impeller: it became the default on iOS in 3.10 and is now the only rendering engine on iOS. On Android it has been the default since 3.27 on API 29 and above, on devices that support Vulkan; the others fall back to the old OpenGL renderer. On macOS, Windows and Linux it has been the default since 3.47. The web still uses Skia.
  • WebAssembly on the web: flutter build web --wasm has been stable since 3.22. The default build is still JavaScript.
  • Swift Package Manager: the default on iOS and macOS since 3.44. CocoaPods is in maintenance mode: Flutter falls back to it if a plugin does not support SwiftPM, but according to the Flutter docs the CocoaPods registry becomes permanently read-only on 2 December 2026. Bookstore's iOS folder has no Pods; the plugins come through FlutterGeneratedPluginSwiftPackage.
  • The UIScene life cycle: according to the Flutter docs, UIKit apps built with Xcode 27 (the iOS 27 SDK) that have not moved to UIScene do not launch. Flutter added support in 3.38, it has been the default for new projects since 3.41, and Flutter migrates projects whose AppDelegate is unmodified by itself during flutter run or flutter build ios. Bookstore's Info.plist already has UIApplicationSceneManifest and FlutterSceneDelegate.
  • Widget Previews: introduced as experimental in 3.35, stable in 3.47; we used them with @Preview in the second post.

Material 3 also became the default in this period: useMaterial3 has been true since 3.16. The ColorScheme.fromSeed theme from the ninth post is built on top of it.

Material and Cupertino Leave the SDK

This is the change that affected the series most, and it was completed while the series was being written. According to Flutter's migration guide, the timeline is:

FlutterWhat happened
3.44The Material and Cupertino code inside the SDK was frozen
3.47material_ui and cupertino_ui 1.0 on pub.dev, opt-in
3.50The libraries inside the SDK are formally deprecated; no removal date

Bookstore has imported package:material_ui/material_ui.dart since the second post, so the deprecation warnings in 3.50 do not affect us. For an older project, the guide's command for the migration is dart fix --apply --code=migrate_design_widgets; a dry run on Bookstore:

dart fix --dry-run --code=migrate_design_widgets
Computing fixes in bookstore_app (dry run)...
Nothing to fix!

The guide has two more notes. GlobalMaterialLocalizations.delegates now comes from material_ui and includes the Cupertino and widgets delegates too; that is the single line we used in the ninth post. For working alongside a package that uses the SDK's Material widgets, there is MaterialUiCompatibilityBridge to put in MaterialApp.builder, but it does not solve compile-time type mismatches; you have to wait for the dependencies to migrate as well. That is why go_router 18.0.0 moved to material_ui and requires at least Flutter 3.44. Another addition in go_router 18 is onEnter: a callback that runs before every navigation and can block it. The package's docs call redirect "legacy" but keep supporting it; we left the auth guard from the twelfth post on redirect.

What Is in Flutter 3.50?

Besides the Material migration, 3.50's breaking changes list has a few items that concern Bookstore: enabled added to DropdownButtonFormField with onChanged made optional, restricted Android engine flags in release mode, stricter Android Intent verification for the app's entry points, and SafeArea support with viewport-fit=cover on the web. The interesting thing about the first item: according to material_ui's changelog, the enabled parameter already arrived in version 1.8.0 of the package. Since Bookstore uses material_ui 1.11.0 and not the Material inside the SDK, we have this parameter on 3.47 as well; in the SDK's DropdownButtonFormField on 3.47, onChanged is still required. That is the practical result of the split: new Material features now arrive with the package without waiting for a Flutter release. Dart 3.14 brought no new language feature. The plans for Dart 3.15 include type promotion of this and better promotion at control flow join points; it has not been released yet.

We wrote the series on the 3.47 line and did not run it with 3.50. We expect the code to compile unchanged on 3.50, because the only affected area we use is the Material imports, and those already come from material_ui; but you can only verify that by running flutter analyze and flutter test with 3.50. The upgrade is your next task.

Upgrading in Practice

Flutter ships a stable release every quarter, and Dart follows the same schedule. Moving to each release soon after it ships is less work than big jumps at once. The order:

  • flutter upgrade in a separate branch (or changing the version in CI; our workflow is pinned with flutter-version: 3.47.8).
  • Read the list for every release in between on the breaking changes page; most items will not affect you, and the ones that do have a migration guide.
  • dart fix --dry-run, then dart fix --apply: a large part of the deprecated APIs is fixed automatically.
  • Look at the packages with flutter pub outdated, then flutter analyze and all the tests.

A short extract of flutter pub outdated on Bookstore today:

direct dependencies:
cupertino_icons                *1.0.9   *1.0.9      2.0.0       2.0.0
9 upgradable dependencies are locked (in pubspec.lock) to older versions.
To update these dependencies, use `flutter pub upgrade`.
1 dependency is constrained to a version that is older than a resolvable version.
To update it, edit pubspec.yaml, or run `flutter pub upgrade --major-versions`.

The nine locked packages are transitive dependencies (sqlite3 3.5.2 / 3.9.1 is one of them); cupertino_icons 2.0.0 is a major version and needs the constraint in the pubspec changed. This is where tests that run in 10 seconds pay off: verifying the whole app after every upgrade step takes less than a minute.

Production Notes

Store rules can change faster than Flutter. Changes such as the UIScene requirement with Xcode 27, Play's yearly target API increase or CocoaPods going read-only make an upgrade mandatory even if the app's code does not change at all. In an app that has not been upgraded for a year, these steps pile up.

Plugins are the slowest link. If a plugin has not moved to SwiftPM, UIScene or material_ui, the project waits for it. When choosing a dependency, looking at the last release date on pub.dev and the supported Flutter version reduces the waiting later.

Pin the SDK version. Everyone on the team and CI should use the same Flutter version; the pinned version in the workflow and committing pubspec.lock are there for that.

Common Pitfalls

The first pitfall is updating the SDK, leaving the sdk lower bound in the pubspec as it was, and searching for why the new language feature does not work.

The second pitfall is skipping the breaking changes page and looking only at compile errors. Some changes compile but change behavior, such as a SnackBar with an action no longer closing by itself since 3.38.

The third pitfall is ignoring deprecation warnings. A deprecation without a removal date also turns into a removal one day, and that day you face hundreds of accumulated warnings.

A Small Experiment

★☆☆ Lower the bound in the dart_samples pubspec to ^3.10.0 and run dart analyze. Which files fail, because of which features?

★★☆ Upgrade Bookstore to Flutter 3.50 in a separate branch: compare the output of flutter analyze, flutter test and dart fix --dry-run; did the golden test stay the same?

★★★ Add a DropdownButtonFormField for a category filter to CatalogScreen, load the categories from GET /api/categories, and disable the field while loading with the enabled parameter. Then write the same widget temporarily with the package:flutter/material.dart import: which error do you get on 3.47?

With this post, the "Mobile with Flutter" series ends. Bookstore's mobile app went from what you know of C# and Angular to Dart, from widgets to layout, to the MVVM structure in the official guide, go_router, the ASP.NET Core API, storage on the device, tests and the store process; each post's code lives in its own snapshot. What stayed out of scope, namely push notifications, platform channels, in-app purchases, analytics and crash reporting, may be covered in a separate series later.