Commencer avec Flutter
Au cours de ce semestre, l’équipe à choisi de principalement travailler avec le framework Flutter. Ce dernier était un des frameworks proposés par l’entreprise CERES dans leur projet. Mon collègue Maxime Dénervaud a également présenté une LI sur Flutter vs Ionic ou il explore les avantages et inconvénients des deux solutions.
Voulant me lancer dans le développement avec Flutter j’ai trouvé des ressources très utiles fournies par Google dans leur codelabs et plus précisément lee tutoriel dee création de sa première app en Flutter.
Après avoir été guidé pour l’installation du SDK et de l’environnement de développement, nous pouvons créer notre première app. On y découvre rapidement le fichier pubspec.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
name: namer_app
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 0.0.1+1
environment:
sdk: '>=2.19.4 <4.0.0'
dependencies:
flutter:
sdk: flutter
english_words: ^4.0.0
provider: ^6.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
uses-material-design: true
Ce fichier sert de fichier de configuration permet de specificer les différents packages pub, d’où le nom pubspec. On pourra trouver tous les packages Flutter sur le site pub.dev. Là on pourra avoir des information utiles sur les différents packages, tels que leur popularité, notation, dernière date de mise à jour, etc.
Ensuite vient le tour du main.dart, le fichier principal de notre application. C’est là que nous allons créer notre interface et la logique qui s’y cache. Avant de s’y pencher on notera que Flutter fonctionne par le widget. Tout ce que l’on veut afficher sera affiché à travers des widgets placés à côté, à l’intérieur et autour d’autres widgets. On peut imaginer qu’une page Flutter est composées de pièces de puzzle qui s’emboîtent les unes avec les autres.
Dans ce main.dart on va créer un simple widget qui affiche un mot. On va ensuite lancer l’application en mode “debug” et voir apparaître une page blanche qui affiche notre mot. Sans fermer cette application on va modifier le code pour afficher un autre mot et l’application va faire un “hot reload” et afficher les changements sans devoir être fermée et rouverte.
On découvre ensuite les Stateful Widgets qui permettent à une interface de se modifier en temps réel si les données contenues à l’intérieur changent. Ces widgets sont opposés aux Stateless Widgets qui n’ont pas cette capacité.
Il existe également un thème visuel qui comprend toute l’application. En utilisant ce dernier on peut facilement améliorer la présentation de toute l’application. Ce thème peut être modifié au besoin et personnalisé pour chaque widget séparément.
La prochaine étape du tutoriel va nous aider à ajouter un bouton qui, lorsqu’il est utilisé, va afficher un mot composé de deux autres mots aléatoires.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
void getNext() {
// Get a new random word pair
current = WordPair.random();
// Notify any listeners of the change above
notifyListeners();
}
...
// Button that calls the creation of a new random name pair
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
...
On va également venir ajouter un moyen de navigation entre l’onglet favorites et la page principale au moyen d’un navigation rail. Après avoir créé la page qui affiche les favoris on arrive au résultat final de notre application :
Vous pouvez également trouver le code de mon main.dart ci-dessous :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
// Change your app's entire visual theme here
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blueGrey),
),
home: MyHomePage(),
),
);
}
}
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
void getNext() {
// Get a new random word pair
current = WordPair.random();
// Notify any listeners of the change above
notifyListeners();
}
var favorites = <WordPair>[];
void toggleFavorite() {
if (favorites.contains(current)) {
favorites.remove(current);
} else {
favorites.add(current);
}
notifyListeners();
}
}
class MyHomePage extends StatefulWidget {
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
// Main widget that will switch between showing the name generator page and the favorite page
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = FavoritesPage();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return LayoutBuilder(builder: (context, constraints) {
return Scaffold(
body: Row(
children: [
SafeArea(
// Navigation rail on the side of the screen
child: NavigationRail(
extended: constraints.maxWidth >= 600,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page,
),
),
],
),
);
});
}
}
// Widget containing the random name generator
class GeneratorPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
IconData icon;
if (appState.favorites.contains(pair)) {
icon = Icons.favorite;
} else {
icon = Icons.favorite_border;
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// BUtton that adds the current name pair to the favorites
ElevatedButton.icon(
onPressed: () {
appState.toggleFavorite();
},
icon: Icon(icon),
label: Text('Like'),
),
SizedBox(width: 10),
// Button that calls the creation of a new random name pair
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
);
}
}
// Widget that shows all favorites
class FavoritesPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
if (appState.favorites.isEmpty) {
return Center(
child: Text('No favorites yet.'),
);
}
return ListView(
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text('You have '
'${appState.favorites.length} favorites:'),
),
for (var pair in appState.favorites)
// List of all favorites
ListTile(
leading: Icon(Icons.favorite),
title: Text(pair.asLowerCase),
),
],
);
}
}
// Widget that shows the word pair in a fancy card format
class BigCard extends StatelessWidget {
const BigCard({
super.key,
required this.pair,
});
final WordPair pair;
@override
Widget build(BuildContext context) {
// Visual theme of the card
final theme = Theme.of(context);
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(
pair.asLowerCase,
style: style,
semanticsLabel: "${pair.first} ${pair.second}",
),
),
);
}
}