Skip to content

Navigation Flutter

Flutter offre deux mécanismes de navigation :

  • Navigator, une API impérative couvrant les besoins de la plupart des cas d’utilisations, grâce à un widget Navigator
  • Router, une API en partie déclarative, beaucoup plus complexe, mais permettant des scénarios plus évolués, notamment sur le web

L’API originelle de navigation permet essentiellement de gérer une stack de routes. La MaterialApp s’ouvre sur une route initiale, au dessus de laquelle il est ensuite possible d’empiler d’autres écrans/routes.

Le widget MaterialApp gère un Navigator, et le met à disposition grâce au principe de InheritedWidget, une sorte de Higher Order Component auquel il est possible d’accéder à partir des “écrans-enfants”.

Ce mécanisme est utilisé pour plusieurs fonctionnalités dans Flutter :

  • Theme
  • MediaQuery
ElevatedButton.icon(
label: const Text('Créer un dossier'),
icon: const Icon(Icons.create_new_folder),
onPressed: ()=> Navigator.of(context).push(
MaterialPageRoute(builder: (context) => Screen2()),
),
)

Chaque écran est défini en tant que Route (cf. MaterialRoutePage) ajoutée (push) ou retirée (pop) d’une pile de routes.

Navigator.of(context).push(route);

Plutôt qu’ajouter une route à la pile, il est possible de remplacer la route actuelle par une nouvelle route.

Navigator.of(context).pushReplacement(route, result : routeResult);

Pour quitter/fermer une route :

Navigator.of(context).pop(result);
Navigator.of(context).popUntil(result);

Il est possible d’associer un nom à chaque route de l’application. Cette association peut être déclarée :

  • via MaterialApp.routes et MaterialApp.initialRoute
  • ou MaterialApp.onGenerateRoute et MaterialApp.onGenerateInitialRoutes
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/list': (context) => ListScreen(),
'/form': (context) => FormScreen(),
},
);
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(),
body: Center(
child: ElevatedButton.icon(
label: const Text('Afficher la liste'),
icon: const Icon(Icons.list),
onPressed: () => Navigator.of(context).pushNamed('/list'),
),
),
);
}
class ListScreen extends StatelessWidget {
const ListScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Liste'),
TextButton(
child: const Text('Retour'),
onPressed: Navigator.of(context).pop,
),
],
),
),
);
}
}

Les routes nommées peuvent recevoir un argument. Il est possible d’accéder à l’argument de la route actuelle :

  • dans un widget : à l’aide de ModalRoute.of(context)!.settings.arguments
  • en utilisant MaterialApp.onGenerateRoute
Navigator.of(context).pushNamed(routeName, arguments:args);
Navigator.of(context).pushReplacementNamed(
routeName,
arguments:args,
result : routeResult,
);
Navigator.of(context).popAndPushNamed(
routeName,
arguments:args,
result:screenResult,
);

Il est possible de personnaliser l’animation de transition entre les deux routes/écrans.

Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, anim1, anim2) => FormScreen(user),
transitionsBuilder: (context, anim1, anim2, child) {
const begin = Offset(0.0, 1.0);
const end = Offset.zero;
final tween = Tween(begin: begin, end: end);
final offsetAnim = anim1.drive(tween);
return SlideTransition(
position: offsetAnim,
child: child,
);
},
),
)

Il est possible d’imbriquer plusieurs Navigator, de manière à créer plusieurs “piles” de navigation.

Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: Navigator.of(context, rootNavigator: true).pop,
),
),
body: Navigator(
initialRoute: '/',
onGenerateRoute: (settings) {
Widget screen;
switch (settings.name) {
case '/page2':
screen = const FormScreen2();
break;
case '/page3':
screen = const FormScreen3();
break;
default:
screen = const FormScreen1();
break;
}
return MaterialPageRoute(builder: (context) => screen);
},
),
);

Cette nouvelle API étant particulièrement complexe, plusieurs packages ont été développés pour en offrir les avantages en s’épargnant cette complexité.

C’est, à ce jour, le package go_router recommandé.

  • navigation : context.go / context.push et context.pop
  • paramètres :
    • liens dynamiques : ‘/item/:id
    • paramètres de requête (query parameters) : ‘/search**?query=dupont**’
    • paramètre extra : context.go(‘/path’, extra:item)
  • sub-routes
  • Video : Intro to go_router
  • MaterialApp.router permet d’initialiser une MaterialApp avec un GoRouter.
  • GoRouter implémente l’interface permettant d’utiliser les fonctionnalités de l’API de “navigation avancée”.
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp.router(routerConfig: router);
}
}
final router = GoRouter(
routes: [
GoRoute(path: '/', builder: (context, routerState) => HomeScreen()),
GoRoute(path: '/list', builder: (context, routerState) => ListScreen()),
],
);
  • context.push(path) => ajoute la route à la pile
  • context.go(path) => remplace la route actuelle
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) => Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('HOME'),
ElevatedButton(
onPressed: () => context.push('/list'),
child: Text('Push ListScreen'),
),
ElevatedButton(
onPressed: () => context.go('/list'),
child: Text('Go to ListScreen'),
),
],
),
),
);
}
  • context.pop([Object? result]) => referme la route, peut optionnellement renvoyer un résultat.
  • context.canPop() => renvoie true si une route est présente “derrière” la route actuelle
class ListScreen extends StatelessWidget {
const ListScreen({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
leading: GoRouter.of(context).canPop()
? IconButton(onPressed: context.pop, icon: Icon(Icons.arrow_back))
: SizedBox.shrink(),
),
body: ListView(
children: List.generate(12, (index) => Text('Item $index'))),
);
}