skip to content

In Flutter 3.47, how does ThemeData.pageTransitionsTheme choose a MaterialPageRoute's transition on each platform, and what are the defaults?

level: middleimportance: should knowfreq 40%

answer

  1. a map keyed by TargetPlatform
  2. Theme.of(context).platform picks the key
  3. Android default changed in 3.38
  4. custom map replaces the defaults
  5. PageRouteBuilder never reads it

basics

~10 s

MaterialPageRoute asks the theme's PageTransitionsTheme for the builder mapped to Theme.of(context).platform. The 3.47 defaults are PredictiveBackPageTransitionsBuilder on Android, CupertinoPageTransitionsBuilder on iOS and macOS, and ZoomPageTransitionsBuilder on Windows and Linux.

solid answer

~40 s

`MaterialPageRoute`, `MaterialPage` and go_router's default pages in a `MaterialApp` delegate `buildTransitions` to `Theme.of(context).pageTransitionsTheme`, which looks up `builders[Theme.of(context).platform]`. `ThemeData.platform` defaults to `defaultTargetPlatform`, so overriding it changes the transition too. In 3.47 the default map is `PredictiveBackPageTransitionsBuilder` for Android (it falls back to `FadeForwardsPageTransitionsBuilder` outside a back gesture), `CupertinoPageTransitionsBuilder` for iOS and macOS, and `ZoomPageTransitionsBuilder` for Windows and Linux. Passing `builders:` replaces that whole map; a platform you leave out falls back to a built-in choice, which for iOS is still the Cupertino builder. The route's duration also comes from the builder: 450 ms for the Android default, 500 ms for Cupertino, 300 ms for Zoom. Routes with their own transition, such as `PageRouteBuilder` or `CupertinoPageRoute`, ignore the theme.

code

dart · 34 lines
dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class SlideUpPageTransitionsBuilder extends PageTransitionsBuilder {
  const SlideUpPageTransitionsBuilder();

  @override
  Duration get transitionDuration => const Duration(milliseconds: 350);

  @override
  Widget buildTransitions<T>(
    PageRoute<T> route,
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    final Animation<Offset> offset = Tween<Offset>(
      begin: const Offset(0, 1),
      end: Offset.zero,
    ).chain(CurveTween(curve: Curves.easeOutCubic)).animate(animation);
    return SlideTransition(position: offset, child: child);
  }
}

final ThemeData datingTheme = ThemeData(
  pageTransitionsTheme: const PageTransitionsTheme(
    builders: <TargetPlatform, PageTransitionsBuilder>{
      TargetPlatform.android: SlideUpPageTransitionsBuilder(),
      TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
      TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
    },
  ),
);

go deeper

for a junior

Recall that ThemeData.pageTransitionsTheme maps each platform to a builder, and name the Android and iOS defaults.

for a middle

Explain the lookup through Theme.of(context).platform, the fallback for missing keys, where the duration comes from, and which routes ignore the theme.

for a senior

Show the production consequences: what a custom map drops (predictive back, swipe-back), the 3.38 duration change in tests, the 3.44 import move.

for a principal

Argue one brand transition versus per-platform native motion, and when a custom builder earns the loss of platform back gestures.

## What PageTransitionsTheme is **`PageTransitionsTheme`** is the part of `ThemeData` (the `pageTransitionsTheme` field) that decides how Material-style routes animate. It holds a map, `builders`, from **`TargetPlatform`** (`android`, `iOS`, `macOS`, `windows`, `linux`, `fuchsia`) to a **`PageTransitionsBuilder`** — an object whose `buildTransitions<T>(route, context, animation, secondaryAnimation, child)` method wraps the page in its animation and whose `transitionDuration` getter sets how long that takes. The theme lets one app look native on every platform without any route knowing which platform it is on. ## How a route picks its builder 1. You push a `MaterialPageRoute` (or put a `MaterialPage` in a `pages` list; go_router's `GoRoute.builder` produces a `MaterialPage` inside a `MaterialApp`). 2. The route's Material transition mixin calls `PageTransitionsTheme.buildTransitions`. 3. That reads `Theme.of(context).platform`, which defaults to **`defaultTargetPlatform`**, and looks the platform up in `builders`. 4. If the map has no entry for that platform, the 3.47 source falls back to `CupertinoPageTransitionsBuilder` for iOS and `ZoomPageTransitionsBuilder` for Android, Windows and Linux. 5. The chosen builder's `buildTransitions` produces the animated widget, and its `transitionDuration` becomes the route's duration when it is pushed. Because the lookup keys on `Theme.of(context).platform`, setting `ThemeData(platform: TargetPlatform.iOS)` on an Android build gives Android users the Cupertino slide — a common way to preview iOS behaviour during development. ## The 3.47 defaults | Platform | Default builder | Duration | Notes | |---|---|---|---| | Android | `PredictiveBackPageTransitionsBuilder` | 450 ms | follows the back gesture on Android 14+; otherwise plays `FadeForwardsPageTransitionsBuilder` | | iOS, macOS | `CupertinoPageTransitionsBuilder` | 500 ms | horizontal slide with the edge swipe-back gesture | | Windows, Linux | `ZoomPageTransitionsBuilder` | 300 ms | Android 10-style zoom | Older builders remain available for a deliberate look: `FadeUpwardsPageTransitionsBuilder` (Android 8 style), `OpenUpwardsPageTransitionsBuilder` (Android 9 style) and `PredictiveBackFullscreenPageTransitionsBuilder`, a full-screen predictive-back variant. ## Customising it To give a dating app a slide-up on Android while iOS keeps its native slide, write a `PageTransitionsBuilder` subclass and map it: ```dart ThemeData( pageTransitionsTheme: const PageTransitionsTheme( builders: <TargetPlatform, PageTransitionsBuilder>{ TargetPlatform.android: SlideUpPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), }, ), ) ``` Things to keep in mind: - **The map replaces the defaults; it does not merge with them.** Every platform you care about belongs in it explicitly, even where the fallback happens to match. - **Replacing the Android entry drops predictive back.** Your builder receives no back-gesture progress, so the preview is gone and the route only pops after the user releases. - **Mapping iOS to anything other than `CupertinoPageTransitionsBuilder` drops the edge swipe-back**, because that gesture detector lives inside the Cupertino transition. - **Duration follows the builder.** Override `transitionDuration` in your subclass; it defaults to 300 ms in the base class. - **Only Material-transition routes read the theme.** `PageRouteBuilder`, `CupertinoPageRoute`, `CupertinoPage` and go_router's `CustomTransitionPage` carry their own transition and ignore it. ## Writing your own builder - Subclass **`PageTransitionsBuilder`** with a `const` constructor so it can sit inside a `const PageTransitionsTheme`. - Override **`buildTransitions<T>(route, context, animation, secondaryAnimation, child)`**; the `route` argument lets you read flags such as `route.fullscreenDialog` or `route.popGestureInProgress` and vary the animation. - Override **`transitionDuration`** and, if the exit should differ, **`reverseTransitionDuration`**; both default to 300 ms in the base class. - Override **`delegatedTransition`** only if the route underneath should animate in step with yours; it is null by default, so a covered route using a different transition family stays still. ## What changed recently - **Flutter 3.38** made `PredictiveBackPageTransitionsBuilder` the Android default, replacing `ZoomPageTransitionsBuilder`; the everyday Android push now plays `FadeForwardsPageTransitionsBuilder` and takes 450 ms instead of 300 ms, which broke tests that pumped a hard-coded 300 ms. Mapping Android back to `ZoomPageTransitionsBuilder` restores the old look at the cost of predictive back. - **Flutter 3.44** moved `CupertinoPageTransitionsBuilder` from `package:flutter/material.dart` to `package:flutter/cupertino.dart`; a file that names it now imports both libraries. - The in-SDK doc comment on the `PageTransitionsTheme` constructor still describes the old Zoom default; the map in the source is what runs.

  • How do you restore the pre-3.38 Android zoom transition, and what does it cost?
    Map `TargetPlatform.android` to `ZoomPageTransitionsBuilder()` in `PageTransitionsTheme`. Pushes go back to the 300 ms zoom, but the route no longer follows Android's predictive back gesture: the user sees no preview and the route pops only when the gesture commits.
  • Why might widget tests that pumped 300 ms after a push start failing on 3.38 or later?
    The Android default builder now takes 450 ms, so after 300 ms the new route is still mid-transition and both pages are on screen. The migration guide suggests reading the duration from the route rather than hard-coding it, for example with flutter_test's `TransitionDurationObserver`.

saying these in an interview costs you the question

  • PageTransitionsTheme applies to every route, including PageRouteBuilder.
  • A custom builders map is merged with the default map.
  • Android still defaults to ZoomPageTransitionsBuilder in current Flutter.
  • The platform comes from dart:io Platform, so ThemeData.platform cannot change it.
  • Swapping the iOS builder only changes the look, not the gestures.