Flutter 3.50: what’s new and what to do when you upgrade

Egor Dolgov · 12.11.2026
image: flutter.dev
Flutter 3.50 · Dart 3.14
November 11, 2026
Flutter 3.50 in three minutes
Subtitles
Flutter 3.50 came out on November 11 together with Dart 3.14. In three minutes: what changed and what to check in your project.

Flutter 3.50 came out on November 11, 2026, together with Dart 3.14. This note covers only what affects your code and your build: what may break, what to update, and which new features are worth using. The full list of changes is in the announcement and the release notes .

Contents:

  1. Before you upgrade
  2. material.dart is deprecated: moving to material_ui
  3. Android: an older project may stop building
  4. Android: a release build ignores the route from an Intent
  5. iOS and macOS: CocoaPods and Intel Macs
  6. Web: Wasm in Safari and Firefox, file hashes, old JS interop
  7. What breaks in code
  8. New in code
  9. Dart 3.14
  10. Briefly about the rest
  11. Summary

1. Before you upgrade

Upgrade Flutter with the flutter upgrade command. If you keep several Flutter versions with FVM, install 3.50 next to the old one — that makes it easy to go back if the project doesn’t build: FVM: several Flutter versions .

The sections below go in order of importance: first what can stop your project from building, then the new features.

2. material.dart is deprecated: moving to material_ui

The Material and Cupertino widgets have moved out of Flutter into separate packages, material_ui and cupertino_ui . Since 3.50 the old libraries are officially deprecated : the line import 'package:flutter/material.dart'; still works, but flutter analyze marks it as deprecated ( deprecated_member_use ).

You can migrate a project with one command in the project folder:

dart fix --apply --code=migrate_design_widgets

It replaces the imports. If the package didn’t appear in pubspec.yaml , add it yourself and run dart fix once more:

flutter pub add material_ui

For Cupertino, do the same with cupertino_ui . The import after the migration:

import 'package:material_ui/material_ui.dart';
  • In 3.50, flutter create makes new projects with material_ui from the start.
  • Widget translations ( flutter_localizations ) have moved into the package too: instead of a list of delegates, localizationsDelegates: GlobalMaterialLocalizations.delegates from material_ui is enough.
  • Latest versions on the date of checking: material_ui 1.11.0, cupertino_ui 1.1.6. Both require Flutter 3.47 or newer.

If one of your dependencies still uses the old material.dart , its widgets won’t see the theme and translations from material_ui . Until the package is updated, there is a bridge, `MaterialUiCompatibilityBridge` : you wrap the app with it in MaterialApp.builder . The bridge is temporary — it is itself marked as deprecated — and it won’t help if the package accepts or returns old Material types in its API.

3. Android: an older project may stop building

Flutter 3.50 checks the Android tool versions during the build and stops the build if they are below the minimum:

  • Android Gradle Plugin (AGP) — 9.1.1;
  • Gradle — 9.3.1;
  • Java — 17;
  • Kotlin Gradle Plugin — 2.4.10 (checked only if the project applies it itself instead of using the Kotlin built into AGP).

In Flutter 3.47 the minimums were AGP 8.11.1 and Gradle 8.14.0, so projects on AGP 8 have to move to version 9.

This is the error you get if the project uses AGP 8.11.1:

Error: Your project's Android Gradle Plugin version (Android Gradle Plugin version 8.11.1) is lower than Flutter's minimum supported version of Android Gradle Plugin version 9.1.1. Please upgrade your Android Gradle Plugin version.
  • The AGP version is in android/settings.gradle.kts (or settings.gradle ), in the plugins block, on the com.android.application plugin. In older projects without that block, it’s in android/build.gradle , on the line classpath 'com.android.tools.build:gradle:<version>' .
  • The Kotlin Gradle Plugin version is in the same plugins block, on the org.jetbrains.kotlin.android plugin.
  • The Gradle version is in android/gradle/wrapper/gradle-wrapper.properties , on the distributionUrl line.
  • The --android-skip-build-dependency-validation flag turns the check off, but it doesn’t guarantee that the project builds with the old tools.

A new project from flutter create in 3.50 gets AGP 9.3.1, Gradle 9.5.0, Kotlin Gradle Plugin 2.4.20 and Java 17. An easy way to find the right values for your project is to create an empty project next to it and compare these two files. The Flutter docs have no separate guide for moving an existing project to AGP 9. If, after the upgrade, android/gradle.properties gets the lines android.builtInKotlin=false and android.newDsl=false , Flutter added them itself: this keeps an older project building without the built-in Kotlin. How to move to it — the migration page .

4. Android: a release build ignores the route from an Intent

If another app opened yours with an Intent carrying a route ( EXTRA_INITIAL_ROUTE ) or an entrypoint, this no longer works in a release build: Flutter accepts that data only from the app itself. In debug and profile the check doesn’t get in the way, but if an Intent fails it, a warning is written to the log — so you can spot the affected places before release.

What to do: open the screen you need through ordinary deep links — Intent.ACTION_VIEW with an address declared in an <intent-filter> in the manifest.

A release build also no longer reads engine flags ( --dart-flags ) from an Intent . For performance measurements, use flutter run --profile .

5. iOS and macOS: CocoaPods and Intel Macs

  • CocoaPods. On December 2, 2026, the CocoaPods repository stops accepting new library versions ; builds that work today won’t break. Since 3.44, Flutter uses Swift Package Manager by default. If you turned it off, Flutter 3.50 warns you when building for iOS and macOS: “Enabling Swift Package Manager will be required in a future version of Flutter”. Turn it back on:
  flutter config --enable-swift-package-manager

If SwiftPM is turned off in the project’s pubspec.yaml ( enable-swift-package-manager: false under flutter: config: ), remove that line — it takes precedence over the global setting . For dependencies that don’t support SwiftPM yet, Flutter still uses CocoaPods.

  • Intel Macs. Flutter is phasing out support for Intel-based Macs: the first time you run a flutter command on such a computer, 3.50 warns once that a future version will require an Apple Silicon Mac. If your macOS app isn’t needed by users of Intel Macs, release and profile builds can target Apple Silicon only: flutter config --enable-macos-arm64-only .

6. Web: Wasm in Safari and Firefox, file hashes, old JS interop

Wasm in Safari and Firefox. A flutter build web --wasm build can now run in Safari 26.2+ and Firefox 147+, but by default these browsers get the JavaScript version. Chrome and Edge (Chromium 137+) run Wasm by default. To turn Wasm on for Safari and Firefox, create a flutter_bootstrap.js file in the project’s web folder — Flutter will use it instead of the default one ( more details ):

{{flutter_js}}
{{flutter_build_config}}

_flutter.loader.load({
  config: {
    wasmAllowList: { blink: true, webkit: true, gecko: true },
  },
});

Hashes in file names. The --web-content-hash flag ( flutter build web --web-content-hash ) adds a content hash to the names of the build files — so that after you deploy a new version, the browser doesn’t take old files from its cache. index.html , flutter_bootstrap.js and flutter.js get no hash: the docs recommend serving them with Cache-Control: no-cache , and the hashed files with public, max-age=31536000, immutable . The flag doesn’t work with deferred imports ( deferred as ) yet.

Old JS interop. dart:html , dart:js , dart:js_util and the js package are deprecated and will be removed from Dart; they don’t work with Wasm already. The replacements are package:web and dart:js_interop — migration guide . To find old imports in the project and its dependencies: flutter build web --no-deprecated-js-interop — the build fails on such imports.

7. What breaks in code

ignoringSemantics is removed from IgnorePointer , AbsorbPointer and SliverIgnorePointer . To hide part of the screen from screen readers, wrap it in ExcludeSemantics ( migration ):

import 'package:material_ui/material_ui.dart';

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

  @override
  Widget build(BuildContext context) {
    // Before: IgnorePointer(ignoringSemantics: true, child: ...)
    return const ExcludeSemantics(
      child: IgnorePointer(
        child: TextField(),
      ),
    );
  }
}

Soft hyphens are now visible. When a line breaks at a soft hyphen (U+00AD), Flutter draws a hyphen on Android, iOS and desktop. To get the old look back, use hyphens: Hyphens.hidden :

import 'package:material_ui/material_ui.dart';

// \u00AD is a soft hyphen: a place where the word may break.
const longWord = 'Interna\u00ADtionalization';

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

  @override
  Widget build(BuildContext context) {
    return const Column(
      children: [
        // Since 3.50: a hyphen is drawn at the break.
        SizedBox(width: 120, child: Text(longWord)),
        // As before: a break without a hyphen.
        SizedBox(width: 120, child: Text(longWord, hyphens: Hyphens.hidden)),
      ],
    );
  }
}

Two more changes affect only a few apps:

  • A WidgetSpan inside right-to-left text (Arabic, Hebrew) now sits in the right place . If you used to shift such widgets by hand, remove that workaround.
  • On the web with viewport-fit=cover in web/index.html , SafeArea and MediaQuery.paddingOf now take the screen notch into account . If your UI moved under the notch, wrap it in SafeArea — or remove viewport-fit=cover from the <meta name="viewport"> tag if you don’t need edge-to-edge display.

8. New in code

All of these APIs are in Flutter itself; you can reach them through the material_ui import too.

ListView.separated row heights in advance. itemExtentBuilder tells the list each item’s height in advance — the list scrolls faster to far positions. The indexes are interleaved: even ones are rows, odd ones are separators. For 100 rows there are 199 items; for an index beyond that, return null :

import 'package:material_ui/material_ui.dart';

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

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemCount: 100,
      itemBuilder: (context, index) => ListTile(title: Text('Contact $index')),
      separatorBuilder: (context, index) => const Divider(height: 1),
      itemExtentBuilder: (index, dimensions) {
        if (index >= 199) return null;
        return index.isEven ? 56 : 1;
      },
    );
  }
}

The “Reduce Motion” setting. MediaQuery.reduceMotionOf(context) tells you whether the user turned on Reduce Motion in the iOS settings. Flutter doesn’t turn off its own animations because of it — that’s up to you:

import 'package:material_ui/material_ui.dart';

class Pill extends StatelessWidget {
  const Pill({super.key, required this.selected});

  final bool selected;

  @override
  Widget build(BuildContext context) {
    final reduceMotion = MediaQuery.reduceMotionOf(context);
    return AnimatedContainer(
      duration: reduceMotion ? Duration.zero : const Duration(milliseconds: 300),
      width: selected ? 120 : 60,
      height: 40,
      color: selected ? Colors.blue : Colors.grey,
    );
  }
}

Your own backdrop under a dialog. barrierBuilder lets you wrap the dimmed barrier under a dialog — for example, to blur the screen. In 3.50 it exists on showGeneralDialog ; showDialog from material_ui 1.11.0 doesn’t have it yet, although the announcement and the release notes say otherwise:

import 'dart:ui';

import 'package:material_ui/material_ui.dart';

void showBlurDialog(BuildContext context) {
  showGeneralDialog<void>(
    context: context,
    barrierDismissible: true,
    barrierLabel: 'Close',
    barrierBuilder: (context, details, barrier) => BackdropFilter(
      filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6),
      child: barrier,
    ),
    pageBuilder: (context, animation, secondaryAnimation) => const Center(
      child: Card(
        child: Padding(padding: EdgeInsets.all(24), child: Text('Hello!')),
      ),
    ),
  );
}

A numeric keyboard for a PIN. TextInputType.numberWithOptions(password: true) opens a numeric password keyboard on Android. On iOS, the web and desktop the parameter changes nothing:

import 'package:material_ui/material_ui.dart';

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

  @override
  Widget build(BuildContext context) {
    return const TextField(
      keyboardType: TextInputType.numberWithOptions(password: true),
      obscureText: true,
      decoration: InputDecoration(labelText: 'PIN code'),
    );
  }
}

A placeholder while an image loads. DecorationImage now has placeholder — an image shown until the main one has loaded. The assets/avatar.png picture has to be added to the project and listed in pubspec.yaml :

import 'package:material_ui/material_ui.dart';

class Avatar extends StatelessWidget {
  const Avatar({super.key, required this.url});

  final String url;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 64,
      height: 64,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        image: DecorationImage(
          image: NetworkImage(url),
          placeholder: const AssetImage('assets/avatar.png'),
          fit: BoxFit.cover,
        ),
      ),
    );
  }
}

A code from an email. AutofillHints.emailOTPCode tells Android autofill that the field expects a one-time code from an email.

A disabled DropdownButtonFormField . In material_ui it has a new enabled parameter, and onChanged is no longer required. Old code with onChanged: null keeps working . Mind the difference: enabled: false turns off the field’s own validation ( autovalidateMode ); if you need errors on a disabled field via AutovalidateMode.always , keep onChanged: null .

9. Dart 3.14

Dart 3.14 has no new language features . What’s useful:

  • A fix in type analysis. A subpattern in a list, map or record pattern no longer promotes the type of the whole matched property. The fix applies to all code regardless of the language version in pubspec.yaml , so after the upgrade the analyzer may report new errors in code with switch and case patterns.
  • dart migrate raises the SDK version in pubspec.yaml and updates the code for it. It doesn’t run without a flag: to preview the changes — dart migrate --dry-run , to write them — dart migrate --apply . One run raises the version by one minor step (for example, from ^3.12.0 to ^3.13.0 ).

10. Briefly about the rest

  • Touch response on iOS. According to the announcement, on iOS the picture after a touch appears one frame earlier: 16 ms at 60 Hz and 8 ms at 120 Hz. Nothing to do.
  • Hybrid Composition++ — an experimental (since Flutter 3.44) way to show native Android views (platform views) inside Flutter; 3.50 improves it. Off by default; requires Android 14 (API 34) and Impeller on Vulkan. How to turn it on .
  • For plugin authors: it’s time to replace the Kotlin Gradle Plugin with the Kotlin built into AGP — guide .

11. Summary

  • Move your imports to material_ui : dart fix --apply --code=migrate_design_widgets .
  • Android: AGP at least 9.1.1, Gradle at least 9.3.1, Java 17 — otherwise the build stops.
  • iOS: keep Swift Package Manager on — from December 2, 2026, CocoaPods gets no new library versions.
  • Check your code for the removed ignoringSemantics and for soft hyphens; for web projects, replace dart:html and package:js .

Send questions and suggestions about the article to contact@flutter.su.