Migrate custom page transitions after the Flutter page transition builders reorganization (Flutter 3.44 to 3.47)
Flutter moved PageTransitionsBuilder and two built-in builders into the widgets layer and CupertinoPageTransitionsBuilder out of Material. What actually breaks (one import, with a misleading 'Not a constant expression' error), what dart fix does and gets wrong for material_ui projects, and how to rewrite custom builders and routes so they no longer depend on Material at all.
For most apps this is a five-minute migration with exactly one source-breaking change: since Flutter 3.44, CupertinoPageTransitionsBuilder lives in the Cupertino library, so any file that imports only package:flutter/material.dart (or package:material_ui/material_ui.dart) and puts that builder into a PageTransitionsTheme stops compiling. Add the Cupertino import and you are done. The rest of the reorganization, which moved the PageTransitionsBuilder base class plus FadeUpwardsPageTransitionsBuilder and OpenUpwardsPageTransitionsBuilder into package:flutter/widgets.dart in Flutter 3.38 and 3.41, is non-breaking, but it is the part worth acting on: your custom builders can now drop their Material dependency entirely, which is what keeps them working when you move to the standalone design packages. Everything below was compiled and tested on Flutter 3.44.8 with Dart 3.12.2, and checked against the current stable, Flutter 3.47.6, with material_ui 1.6.0 and cupertino_ui 1.1.2.
Why the builders were moved
PageTransitionsBuilder started life as a Material class because PageTransitionsTheme and MaterialPageRoute were its only consumers. That made no sense for a Cupertino app, or for a team with its own design system built on WidgetsApp: to reuse a transition object, you had to import Material. Flutter’s issue #172929 (“Move platform specific page transitions outside of Material and Cupertino”) tracked untangling that, as part of the larger effort to ship Material and Cupertino as separate packages.
The concrete outcomes:
- Custom builders no longer need Material. A
PageTransitionsBuildersubclass can import onlypackage:flutter/widgets.dartand be used by a hand-writtenPageRoute, aWidgetsApp, aCupertinoApp, orPageTransitionsTheme. - Cupertino apps get the iOS builder without pulling in Material.
CupertinoPageTransitionsBuildernow sits next toCupertinoPageRouteincupertino/route.dart. - Builders survive the
material_uimigration. Because the base class lives in the widgets layer, which is not moving out of the SDK, a builder written againstwidgets.dartis the same type for the in-SDKPageTransitionsThemeand for the one inmaterial_ui.
What breaks
| Area | Change | Landed in stable | Severity |
|---|---|---|---|
PageTransitionsBuilder | Moved from Material to widgets.dart (PR #174321) | 3.38 | none, Material re-exports widgets |
FadeUpwardsPageTransitionsBuilder | Moved to widgets.dart (PR #175560) | 3.41 | none |
OpenUpwardsPageTransitionsBuilder | Moved to widgets.dart (PR #177080) | 3.41 | none |
CupertinoPageTransitionsBuilder | Moved from Material to cupertino.dart (PR #179776) | 3.44 | high for Material-only files, one import fixes it |
ZoomPageTransitionsBuilder, FadeForwardsPageTransitionsBuilder, PredictiveBackPageTransitionsBuilder, PageTransitionsTheme | Unchanged, still Material | n/a | none |
The first three rows are invisible to a Material app because material.dart does export 'package:flutter/widgets.dart'. A file that writes extends PageTransitionsBuilder with only a Material import resolves the class through that re-export, exactly as before. The official breaking change page lists FadeUpwardsPageTransitionsBuilder and OpenUpwardsPageTransitionsBuilder under “Material” for the same reason: from a Material import, that is where they appear to come from.
Pre-flight checklist
-
Know which Flutter you are on:
flutter --version. The break needs 3.44 or later. 3.47.x is current stable. -
Find every reference before you touch anything:
# Any Flutter version grep -rn "PageTransitionsBuilder\|PageTransitionsTheme" lib test packages -
Note whether the project is on the SDK libraries (
package:flutter/material.dart) or the standalone packages (package:material_ui/material_ui.dart). The fix is the same idea, but the import line differs, anddart fixgets one of them wrong (see step 3). -
Check your path and git dependencies too. A package that references
CupertinoPageTransitionsBuilderwith only a Material import breaks your build in the same way, and you cannot fix it from your app.
Migration steps
-
Upgrade and reproduce the failure. Move to the target SDK and run the analyzer, which gives a much clearer message than the compiler:
# Flutter 3.44.8 or later flutter upgrade flutter analyzeTake this
ThemeDatafrom a typical app, which compiled fine on 3.41:// Flutter 3.44.8, Dart 3.12.2 -- fails to compile import 'package:flutter/material.dart'; final ThemeData theme = ThemeData( pageTransitionsTheme: const PageTransitionsTheme( builders: <TargetPlatform, PageTransitionsBuilder>{ TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), }, ), );flutter analyzereports the real cause,undefined_method: “The method ‘CupertinoPageTransitionsBuilder’ isn’t defined”, plusinvalid_constantandnon_constant_map_valuenoise for each entry. Verify: you see oneundefined_methodperCupertinoPageTransitionsBuilderusage and no other new errors. -
Add the Cupertino import to each affected file. On the SDK libraries:
// Flutter 3.44+, SDK libraries import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart';On the standalone packages:
// Flutter 3.47.6, material_ui 1.6.0, cupertino_ui 1.1.2 import 'package:cupertino_ui/cupertino_ui.dart'; import 'package:material_ui/material_ui.dart';material_uialready depends oncupertino_ui, but importing a transitive dependency trips thedepend_on_referenced_packageslint, so add it explicitly withflutter pub add cupertino_ui. Verify:flutter analyzeis clean for those files. -
Or let
dart fixdo it, then check the result. Both libraries ship a data-driven fix for this move (thereplacedByentry infix_material.yaml):# Flutter 3.44+ dart fix --dry-run dart fix --applyOn an SDK-libraries project this inserts
import 'package:flutter/cupertino.dart';and nothing else, which is correct. On amaterial_ui1.6.0 project, the fix data still points atpackage:flutter/cupertino.dart, the frozen SDK copy, not atcupertino_ui. Your code compiles, because the SDK builder extends the same widgets-layer base class, but you have just reintroduced an SDK design library import into a project you migrated away from it. Replace that line with thecupertino_uiimport by hand. Verify:grep -rn "package:flutter/cupertino.dart" libreturns nothing on a migrated project. -
Retarget custom builders at the widgets layer. A builder that only composes
SlideTransition,FadeTransition,ScaleTransitionand curves has no reason to import Material any more:// Flutter 3.44+, Dart 3.12 -- no Material import needed import 'package:flutter/widgets.dart'; class FadeSlidePageTransitionsBuilder extends PageTransitionsBuilder { const FadeSlidePageTransitionsBuilder(); @override Duration get transitionDuration => const Duration(milliseconds: 250); @override Widget buildTransitions<T>( PageRoute<T> route, BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child, ) { final Animation<Offset> position = animation.drive( Tween<Offset>(begin: const Offset(0.0, 0.08), end: Offset.zero) .chain(CurveTween(curve: Curves.easeOutCubic)), ); return FadeTransition( opacity: animation, child: SlideTransition(position: position, child: child), ); } }The same class still drops into a Material theme unchanged, because
PageTransitionsTheme.buildersis typed against this exact base class. Verify: the file’s only Flutter import iswidgets.dartandflutter analyzeis clean. -
Replace
PageRouteBuilderboilerplate with a route that delegates to a builder. This is the pattern the reorganization was designed for: one route class, any transition, no Material:// Flutter 3.44+, Dart 3.12 import 'package:flutter/widgets.dart'; class BuilderPageRoute<T> extends PageRoute<T> { BuilderPageRoute({ required this.builder, this.transitionsBuilder = const FadeSlidePageTransitionsBuilder(), super.settings, }); final WidgetBuilder builder; final PageTransitionsBuilder transitionsBuilder; @override Duration get transitionDuration => transitionsBuilder.transitionDuration; @override Duration get reverseTransitionDuration => transitionsBuilder.reverseTransitionDuration; @override DelegatedTransitionBuilder? get delegatedTransition => transitionsBuilder.delegatedTransition; @override Color? get barrierColor => null; @override String? get barrierLabel => null; @override bool get maintainState => true; @override Widget buildPage( BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, ) => builder(context); @override Widget buildTransitions( BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child, ) => transitionsBuilder.buildTransitions<T>( this, context, animation, secondaryAnimation, child, ); }Forwarding
transitionDuration,reverseTransitionDurationanddelegatedTransitionmatters. The official sample hardcodes 300 ms, which silently ignores the duration the builder declares, and withoutdelegatedTransitionaCupertinoPageTransitionsBuilderpassed to this route animates the incoming page but leaves the previous page frozen instead of sliding it left. Verify with a widget test (next section). -
Plug the route into whatever app widget you use. For a
WidgetsApp-based design system, pass it aspageRouteBuilder:// Flutter 3.44+ WidgetsApp( color: const Color(0xFF0B57D0), pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) => BuilderPageRoute<T>(builder: builder, settings: settings), home: const HomeScreen(), );In a Material app, keep using
PageTransitionsThemefor the default and pushBuilderPageRouteonly where a screen needs a different transition. Verify: navigating to a pushed screen shows the new animation, andflutter analyzeis clean.
Verification
Do not trust your eyes on a 250 ms animation. Pump the route halfway and assert on the transition widget:
// Flutter 3.44.8, flutter_test
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/transitions.dart';
void main() {
testWidgets('BuilderPageRoute uses the builder duration', (tester) async {
final navigator = GlobalKey<NavigatorState>();
await tester.pumpWidget(WidgetsApp(
navigatorKey: navigator,
color: const Color(0xFF000000),
pageRouteBuilder: <T>(RouteSettings s, WidgetBuilder b) =>
BuilderPageRoute<T>(builder: b, settings: s),
home: const Text('home', textDirection: TextDirection.ltr),
));
navigator.currentState!.push(BuilderPageRoute<void>(
builder: (_) => const Text('second', textDirection: TextDirection.ltr),
));
await tester.pump();
await tester.pump(const Duration(milliseconds: 125));
final fade = tester.widget<FadeTransition>(find
.ancestor(of: find.text('second'), matching: find.byType(FadeTransition))
.first);
expect(fade.opacity.value, 0.5);
await tester.pumpAndSettle();
expect(find.text('second'), findsOneWidget);
});
}
On Flutter 3.44.8 this passes with opacity exactly 0.5 at 125 ms, which proves the route picked up the builder’s 250 ms duration. If someone hardcodes 300 ms again, the value drops to about 0.42 and the test fails. Beyond that:
flutter analyzereports noundefined_methodorundefined_hidden_namediagnostics.flutter testpasses, including golden tests that capture mid-transition frames, if you have any.- On an iOS simulator, swipe back from the left edge on a screen that uses
CupertinoPageTransitionsBuilderand confirm the previous page moves with the gesture.
Rollback plan
The code changes are additive: an extra import and a few classes that no longer need Material. All of them compile on 3.41 too, except that on 3.41 CupertinoPageTransitionsBuilder resolves through the Material import, so the added cupertino.dart import is merely redundant. Rolling back the SDK with flutter downgrade or a pinned version in CI does not require reverting any of this. The one thing that cannot go back below 3.38 is a builder that imports only widgets.dart, since the base class was not there yet.
Gotchas
The compiler error points at the wrong problem. Inside a const map, which is how almost every PageTransitionsTheme is written, the front end does not say the name is undefined. flutter build and flutter test print only:
lib/main.dart:14:33: Error: Not a constant expression.
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
People delete const, which turns it into “The method ‘CupertinoPageTransitionsBuilder’ isn’t defined for the type ‘App’”, and then go looking for a method. Run flutter analyze first; it shows the undefined_method diagnostic alongside the constant noise.
A partial builders map does not merge with the defaults. Passing builders: replaces the whole default map, and missing platforms fall back at runtime to CupertinoPageTransitionsBuilder on iOS only and ZoomPageTransitionsBuilder everywhere else, macOS included. If you only list Android and iOS, macOS gets the zoom transition. While you are in this file anyway, list every platform you ship.
Mixing the SDK and package Cupertino imports in one file. On a material_ui project, a file that imports both package:flutter/cupertino.dart (left behind by dart fix) and package:cupertino_ui/cupertino_ui.dart gets ambiguous_import errors for every Cupertino name. Keep exactly one.
Stale hide clauses. Some codebases wrote import 'package:flutter/material.dart' hide CupertinoPageTransitionsBuilder; to avoid a clash with a local class of the same name. On 3.44+ that name no longer exists in the Material namespace, and the analyzer flags undefined_hidden_name. Delete the clause.
Third-party builders keep working. SharedAxisPageTransitionsBuilder from animations and similar classes extend the base class through their own Material import, which re-exports the widgets-layer type, so they still slot into your theme. Only packages that themselves reference CupertinoPageTransitionsBuilder with a Material-only import break, and those need a package release, not a change in your app.
Subclassing a Material builder still needs Material. ZoomPageTransitionsBuilder, FadeForwardsPageTransitionsBuilder and the predictive back builders stayed in Material. If your custom builder extends one of them to tweak a duration, it keeps its Material (or material_ui) import.
Related
- The import change here is one slice of the bigger move covered in migrating Flutter Material and Cupertino imports to the material_ui and cupertino_ui packages.
- For the release that started the decoupling, see Flutter 3.44 splitting Material and Cupertino into packages.
- Another 3.44 compile error with the same root cause: fixing “Undefined name ‘awaitNotRequired’” with material_ui and cupertino_ui.
- If your custom transition is really a shared element, a Hero animation between two screens may be the better tool.
- Routers that build their own pages, such as go_router’s
CustomTransitionPage, are compared in go_router vs auto_route vs Navigator 2.0.
Sources
- Page transition builders reorganization, Flutter breaking changes.
PageTransitionsBuilderAPI reference.- flutter/flutter#172929, the tracking issue.
- PRs #174321, #175560, #177080 and #179776.
- Data-driven fixes in the Dart docs.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.