Listes et Navigation
Listes
Section titled “Listes”Le widget ListView permet d’afficher les listes verticales ou horizontales.
ListView( children: items .map((item) => ListTile(title: Text(item)),) .toList(),)ListView constructeurs
Section titled “ListView constructeurs”ListView propose plusieurs constructeurs. ListView.builder a l’avantage de recycler les élements et d’optimiser leur instanciation, améliorant ainsi les performances proportionnellement aux nombres d’élements dans la liste.
ListView.builder( itemCount: users.length, itemBuilder: (context, index) => UserTile(users[index]),)
ListView.separated( itemCount: users.length, itemBuilder: (context, index) => UserTile(users[index]), separatorBuilder: (context, index) => const Divider(),)ListTile
Section titled “ListTile”Pour les item de la liste, il existe un widget ListTile permettant de définir :
- le contenu de l’item
- un
title - un
subtitle - un
trailing: un emplacement en fin de ligne, en général pour un icône - un
leading: un emplacement en début de ligne, en général pour un icône
- un
- l’état de l’item :
selectedenabled
- des écouteurs d’évenement :
onTaponLongPress
- les couleurs :
textColortileColoriconColorselectedColorselectedTileColorhoverColorfocusColor
- des options de layout :
densecontentPaddinghorizontalTitleGapminLeadingWidthminVerticalPadding
ListTile( leading: const Icon(Icons.account_circle), trailing: selection.contains(item) ? const Icon(Icons.check) : null, title: Text(item), subtitle: Text('Sous-titre $item'), onTap: () { selection.contains(item) ? selection.remove(item) : selection.add(item); setState(() {}); },)Exemple complet ListTile
Section titled “Exemple complet ListTile”class ListScreen extends StatefulWidget { const ListScreen({Key? key}) : super(key: key);
@override State<ListScreen> createState() => _ListScreenState();}
class _ListScreenState extends State<ListScreen> { final List<String> items = const ['Item A', 'Item B', 'Item C']; final List<String> selection = [];
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: ListView( children: items.map((item) { final selected = selection.contains(item); return ItemTile(item, selected: selected, onTap: () { selected ? selection.remove(item) : selection.add(item); setState(() {}); }); }).toList(), ), ); }}
class ItemTile extends StatelessWidget { final String item; final bool selected; final VoidCallback onTap;
const ItemTile( this.item, { required this.selected, required this.onTap, Key? key, }) : super(key: key);
@override Widget build(BuildContext context) { return ListTile( leading: const Icon(Icons.account_circle), trailing: selected ? const Icon(Icons.check) : null, title: Text(item), subtitle: Text('Sous-titre $item'), onTap: onTap, ); }}GridView
Section titled “GridView”class MainScreen extends StatelessWidget { const MainScreen({Key? key}) : super(key: key);
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: GridView.count( crossAxisCount: 2, crossAxisSpacing: 8, mainAxisSpacing: 8, childAspectRatio: 16 / 9, children: List.generate( 37, (index) => GridTile( key: Key('item$index'), header: const Text('Header'), footer: const Text('Footer'), child: Center( child: Text( 'ITEM $index', overflow: TextOverflow.ellipsis, maxLines: 10, ), ), ), ), ), ); }}Naviguer entre des écrans
Section titled “Naviguer entre des écrans”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 widgetNavigatorRouter, une API en partie déclarative, beaucoup plus complexe, mais permettant des scénarios plus évolués, notamment sur le web
Navigator
Section titled “Navigator”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()), ),)Material Routes
Section titled “Material Routes”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);Named Routes
Section titled “Named Routes”Il est possible d’associer un nom à chaque route de l’application. Cette association peut être déclarée :
- via
MaterialApp.routesetMaterialApp.initialRoute - ou
MaterialApp.onGenerateRouteetMaterialApp.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, ), ], ), ), ); }}Route Arguments
Section titled “Route Arguments”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,);Transitions
Section titled “Transitions”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, ); }, ),)Multi-navigators
Section titled “Multi-navigators”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); }, ),);Router (API 2.0)
Section titled “Router (API 2.0)”Cette nouvelle API étant particulièrement complexe, plusieurs packages ont été développés pour en offrir les avantages en s’épargnant cette complexité.
- https://medium.com/flutter/learning-flutters-new-navigation-and-routing-system-7c9068155ade
- https://www.raywenderlich.com/19457817-flutter-navigator-2-0-and-deep-links
- Tutoriel Navigator 2
go_router
Section titled “go_router”C’est, à ce jour, le package go_router recommandé.
- navigation :
context.go/context.pushetcontext.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
Go_router config
Section titled “Go_router config”MaterialApp.routerpermet d’initialiser une MaterialApp avec un GoRouter.GoRouterimplé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()), ],);Go_router navigation
Section titled “Go_router navigation”context.push(path)=> ajoute la route à la pilecontext.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'))), );}Dialogs
Section titled “Dialogs”Flutter permet d’ouvrir des popups grâce à showDialog(context, builder)
Plusieurs type de fenêtre de dialogue sont disponibles :
SimpleDialog: untitleet une liste d’optionsSimpleDialogOptionAlertDialog(etCupertinoDialogAction) : untitle, uncontentet une liste d’actionsDialog
final choice = await showDialog<bool>( context: context, builder: (context) { return AlertDialog( title: const Text('Attention'), content: const Text('Êtes-vous vous sûr ?'), actions: [ ElevatedButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Non'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), child: const Text('Oui'), ), ], ); },);print('Confirmation : $choice')BottomSheets
Section titled “BottomSheets”ModalBottomSheet
Section titled “ModalBottomSheet”showModalBottomSheet(context,builder) permet d’ouvrir une BottomSheet.
final result = await showModalBottomSheet( context: context, enableDrag: true, builder: (context) { return Padding( padding: const EdgeInsets.all(8.0), child: ListView( children: [ Padding( padding: const EdgeInsets.all(12.0), child: Text('Partager avec', style: textTheme.headlineMedium), ), Wrap( children: [ ShareOptionView( label: 'Email', icon: Icons.mail, onSelection: () => Navigator.of(context).pop(ShareOptions.mail), ), ShareOptionView( label: 'Twitter', icon: Icons.flutter_dash, onSelection: () => Navigator.of(context).pop(ShareOptions.twitter), ), ShareOptionView( label: 'SMS', icon: Icons.message, onSelection: () => Navigator.of(context).pop(ShareOptions.sms), ), ], ), ], ), ); },);Packages pour des sheet plus évoluées :
Postframe callback
Section titled “Postframe callback”Il peut arriver de vouloir ouvrir une popup “automatiquement” dès l’ouverture d’un écran, pour cela il est possible d’appeler addPostFramCallback. Ce callback ne sera executé qu’une seule fois.
WidgetsBinding.instance!.addPostFrameCallback( (timeStamp) => _openOnboardingPopup(),);