Start Debugging

Migre transições de página personalizadas após a reorganização dos page transition builders do Flutter (Flutter 3.44 a 3.47)

O Flutter moveu PageTransitionsBuilder e dois builders embutidos para a camada widgets e CupertinoPageTransitionsBuilder para fora do Material. O que realmente quebra (um import, com um erro enganoso 'Not a constant expression'), o que o dart fix faz e erra em projetos com material_ui, e como reescrever builders e rotas personalizados para que não dependam mais do Material.

Para a maioria dos apps, esta é uma migração de cinco minutos com exatamente uma mudança que quebra o código-fonte: desde o Flutter 3.44, CupertinoPageTransitionsBuilder fica na biblioteca Cupertino, então qualquer arquivo que importe apenas package:flutter/material.dart (ou package:material_ui/material_ui.dart) e coloque esse builder em um PageTransitionsTheme deixa de compilar. Adicione o import do Cupertino e pronto. O restante da reorganização, que moveu a classe base PageTransitionsBuilder além de FadeUpwardsPageTransitionsBuilder e OpenUpwardsPageTransitionsBuilder para package:flutter/widgets.dart no Flutter 3.38 e 3.41, não quebra nada, mas é a parte que vale a pena aproveitar: seus builders personalizados agora podem abandonar por completo a dependência do Material, o que os mantém funcionando quando você migrar para os pacotes de design independentes. Tudo abaixo foi compilado e testado no Flutter 3.44.8 com Dart 3.12.2, e verificado contra a versão estável atual, Flutter 3.47.6, com material_ui 1.6.0 e cupertino_ui 1.1.2.

Por que os builders foram movidos

PageTransitionsBuilder nasceu como uma classe do Material porque PageTransitionsTheme e MaterialPageRoute eram seus únicos consumidores. Isso não fazia sentido para um app Cupertino, nem para uma equipe com seu próprio design system construído sobre WidgetsApp: para reutilizar um objeto de transição, era preciso importar o Material. A issue #172929 do Flutter (“Move platform specific page transitions outside of Material and Cupertino”) acompanhou o trabalho de desfazer esse acoplamento, como parte do esforço maior de distribuir Material e Cupertino como pacotes separados.

Os resultados concretos:

O que quebra

ÁreaMudançaChegou ao stableGravidade
PageTransitionsBuilderMovido do Material para widgets.dart (PR #174321)3.38nenhuma, o Material reexporta widgets
FadeUpwardsPageTransitionsBuilderMovido para widgets.dart (PR #175560)3.41nenhuma
OpenUpwardsPageTransitionsBuilderMovido para widgets.dart (PR #177080)3.41nenhuma
CupertinoPageTransitionsBuilderMovido do Material para cupertino.dart (PR #179776)3.44alta para arquivos só com Material, um import resolve
ZoomPageTransitionsBuilder, FadeForwardsPageTransitionsBuilder, PredictiveBackPageTransitionsBuilder, PageTransitionsThemeSem alteração, continuam no Materialn/anenhuma

As três primeiras linhas são invisíveis para um app Material porque material.dart faz export 'package:flutter/widgets.dart'. Um arquivo que escreve extends PageTransitionsBuilder com apenas um import do Material resolve a classe por esse reexport, exatamente como antes. A página oficial de breaking changes lista FadeUpwardsPageTransitionsBuilder e OpenUpwardsPageTransitionsBuilder sob “Material” pelo mesmo motivo: a partir de um import do Material, é de lá que eles parecem vir.

Checklist prévio

Passos da migração

  1. Atualize e reproduza a falha. Mude para o SDK de destino e execute o analisador, que dá uma mensagem muito mais clara que o compilador:

    # Flutter 3.44.8 or later
    flutter upgrade
    flutter analyze

    Pegue este ThemeData de um app típico, que compilava normalmente na 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(),
        },
      ),
    );

    O flutter analyze informa a causa real, undefined_method: “The method ‘CupertinoPageTransitionsBuilder’ isn’t defined”, além do ruído de invalid_constant e non_constant_map_value para cada entrada. Verifique: você vê um undefined_method por uso de CupertinoPageTransitionsBuilder e nenhum outro erro novo.

  2. Adicione o import do Cupertino a cada arquivo afetado. Nas bibliotecas do SDK:

    // Flutter 3.44+, SDK libraries
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';

    Nos pacotes independentes:

    // 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';

    O material_ui já depende do cupertino_ui, mas importar uma dependência transitiva dispara o lint depend_on_referenced_packages, então adicione-o explicitamente com flutter pub add cupertino_ui. Verifique: o flutter analyze não reporta nada para esses arquivos.

  3. Ou deixe o dart fix fazer isso e depois confira o resultado. Ambas as bibliotecas trazem uma correção orientada a dados para essa mudança (a entrada replacedBy em fix_material.yaml):

    # Flutter 3.44+
    dart fix --dry-run
    dart fix --apply

    Em um projeto com as bibliotecas do SDK, isso insere import 'package:flutter/cupertino.dart'; e nada mais, o que está correto. Em um projeto com material_ui 1.6.0, os dados da correção ainda apontam para package:flutter/cupertino.dart, a cópia congelada do SDK, e não para o cupertino_ui. Seu código compila, porque o builder do SDK estende a mesma classe base da camada widgets, mas você acabou de reintroduzir um import de biblioteca de design do SDK em um projeto que já tinha migrado para fora dela. Substitua essa linha pelo import do cupertino_ui manualmente. Verifique: grep -rn "package:flutter/cupertino.dart" lib não retorna nada em um projeto migrado.

  4. Redirecione os builders personalizados para a camada widgets. Um builder que apenas compõe SlideTransition, FadeTransition, ScaleTransition e curvas não tem mais motivo para importar o Material:

    // 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),
        );
      }
    }

    A mesma classe continua se encaixando em um tema Material sem alterações, porque PageTransitionsTheme.builders é tipado exatamente contra essa classe base. Verifique: o único import do Flutter no arquivo é widgets.dart e o flutter analyze não reporta nada.

  5. Substitua o boilerplate de PageRouteBuilder por uma rota que delega a um builder. Este é o padrão para o qual a reorganização foi projetada: uma classe de rota, qualquer transição, sem 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,
      );
    }

    Encaminhar transitionDuration, reverseTransitionDuration e delegatedTransition é importante. O exemplo oficial fixa 300 ms no código, o que ignora silenciosamente a duração que o builder declara, e sem delegatedTransition um CupertinoPageTransitionsBuilder passado para essa rota anima a página que entra, mas deixa a página anterior congelada em vez de deslizá-la para a esquerda. Verifique com um teste de widget (próxima seção).

  6. Conecte a rota ao widget de app que você usa. Para um design system baseado em WidgetsApp, passe-a como pageRouteBuilder:

    // Flutter 3.44+
    WidgetsApp(
      color: const Color(0xFF0B57D0),
      pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) =>
          BuilderPageRoute<T>(builder: builder, settings: settings),
      home: const HomeScreen(),
    );

    Em um app Material, continue usando PageTransitionsTheme para o padrão e faça push de BuilderPageRoute apenas onde uma tela precisar de uma transição diferente. Verifique: navegar para uma tela empilhada mostra a nova animação, e o flutter analyze não reporta nada.

Verificação

Não confie nos seus olhos para uma animação de 250 ms. Avance a rota até a metade e faça uma asserção sobre o widget de transição:

// 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);
  });
}

No Flutter 3.44.8 este teste passa com opacidade exatamente 0.5 em 125 ms, o que prova que a rota assumiu a duração de 250 ms do builder. Se alguém fixar 300 ms no código de novo, o valor cai para cerca de 0.42 e o teste falha. Além disso:

Plano de rollback

As mudanças de código são aditivas: um import extra e algumas classes que não precisam mais do Material. Todas compilam também na 3.41, exceto que na 3.41 CupertinoPageTransitionsBuilder é resolvido pelo import do Material, então o import adicionado de cupertino.dart é apenas redundante. Fazer rollback do SDK com flutter downgrade ou uma versão fixada no CI não exige reverter nada disso. A única coisa que não pode voltar para antes da 3.38 é um builder que importa apenas widgets.dart, já que a classe base ainda não existia lá.

Armadilhas

O erro do compilador aponta para o problema errado. Dentro de um mapa const, que é como quase todo PageTransitionsTheme é escrito, o front end não diz que o nome é indefinido. flutter build e flutter test imprimem apenas:

lib/main.dart:14:33: Error: Not a constant expression.
            TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

As pessoas removem o const, o que o transforma em “The method ‘CupertinoPageTransitionsBuilder’ isn’t defined for the type ‘App’”, e aí saem procurando um método. Execute o flutter analyze primeiro; ele mostra o diagnóstico undefined_method junto com o ruído de constantes.

Um mapa builders parcial não é mesclado com os padrões. Passar builders: substitui o mapa padrão inteiro, e as plataformas ausentes recorrem, em runtime, a CupertinoPageTransitionsBuilder apenas no iOS e a ZoomPageTransitionsBuilder em todos os outros lugares, macOS incluído. Se você listar apenas Android e iOS, o macOS recebe a transição de zoom. Já que você está neste arquivo de qualquer forma, liste todas as plataformas que você publica.

Misturar os imports Cupertino do SDK e do pacote no mesmo arquivo. Em um projeto com material_ui, um arquivo que importa tanto package:flutter/cupertino.dart (deixado pelo dart fix) quanto package:cupertino_ui/cupertino_ui.dart recebe erros ambiguous_import para todo nome do Cupertino. Mantenha exatamente um.

Cláusulas hide obsoletas. Algumas bases de código escreveram import 'package:flutter/material.dart' hide CupertinoPageTransitionsBuilder; para evitar um conflito com uma classe local de mesmo nome. Na 3.44+, esse nome não existe mais no namespace do Material, e o analisador sinaliza undefined_hidden_name. Apague a cláusula.

Builders de terceiros continuam funcionando. SharedAxisPageTransitionsBuilder, do pacote animations, e classes semelhantes estendem a classe base por meio de seu próprio import do Material, que reexporta o tipo da camada widgets, então continuam se encaixando no seu tema. Só quebram os pacotes que eles mesmos referenciam CupertinoPageTransitionsBuilder com um import apenas do Material, e esses precisam de uma nova versão do pacote, não de uma mudança no seu app.

Estender um builder do Material ainda exige o Material. ZoomPageTransitionsBuilder, FadeForwardsPageTransitionsBuilder e os builders de predictive back permaneceram no Material. Se o seu builder personalizado estende um deles para ajustar uma duração, ele mantém seu import do Material (ou do material_ui).

Leitura relacionada

Fontes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Voltar