1. Présentation
Flutter est un kit d'interface utilisateur Google qui permet de développer des applications esthétiques compilées de manière native pour les mobiles, le Web et les ordinateurs de bureau, à partir d'un seul codebase. Offert et Open Source, Flutter fonctionne avec votre code existant. Il est utilisé par les développeurs et les entreprises du monde entier.
Dans cet atelier de programmation, vous allez mettre en valeur une application musicale Flutter. Pour ce faire, cet atelier de programmation utilise des outils et des API présentés dans Material 3.
Ce que vous allez apprendre
- Développer une application Flutter facile à utiliser et d'aspect esthétique sur toutes les plates-formes.
- Concevoir le texte de votre application pour vous assurer qu'il contribue à l'expérience utilisateur.
- Choisir les bonnes couleurs, personnaliser les widgets, créer votre propre thème et implémenter rapidement et facilement le mode sombre.
- Créer des applications adaptatives et multiplates-formes.
- Créer des applications convenant à tous les écrans.
- Ajouter du mouvement à votre application Flutter pour la rendre vraiment attrayante.
Prérequis :
Cet atelier de programmation suppose que vous avez une certaine expérience de Flutter. Dans le cas contraire, vous devriez d'abord vous familiariser avec ses principes de base. Les liens suivants proposent des informations utiles :
- Suivez une visite guidée du framework de widgets Flutter
- Essayez l'atelier de programmation Écrire votre première application Flutter, partie 1
Ce que vous allez faire
Cet atelier de programmation vous aidera à créer l'écran d'accueil de MyArtist, une application de lecture de musique qui permet aux fans de suivre les nouveautés de leurs artistes préférés. Vous apprendrez à modifier l'apparence de votre application pour la mettre en valeur sur différentes plates-formes.
Les fichiers GIF animés suivants montrent le fonctionnement de l'application à la fin de cet atelier de programmation :
Qu'attendez-vous de cet atelier de programmation ?
2. Configurer votre environnement Flutter
Pour cet atelier, vous avez besoin de deux logiciels : le SDK Flutter et un éditeur.
Vous pouvez exécuter l'atelier de programmation sur l'un des appareils suivants :
- Un appareil Android ou iOS physique connecté à votre ordinateur et réglé en mode développeur.
- Le simulateur iOS (outils Xcode à installer).
- L'émulateur Android (qui doit être configuré dans Android Studio).
- Un navigateur (Chrome est requis pour le débogage).
- En tant qu'application de bureau Windows, Linux ou macOS. Vous devez développer votre application sur la plate-forme où vous comptez la déployer. Par exemple, si vous voulez développer une application de bureau Windows, vous devez le faire sous Windows pour accéder à la chaîne de compilation appropriée. Prenez également connaissance des exigences spécifiques aux systèmes d'exploitation, détaillées sur flutter.dev/desktop.
3. Obtenir l'application de démarrage de l'atelier de programmation
Cloner l'application depuis GitHub
Pour cloner cet atelier de programmation à partir de GitHub, exécutez les commandes suivantes :
git clone https://github.com/flutter/codelabs.git cd codelabs/boring_to_beautiful/step_01/
Pour vous assurer que tout fonctionne, exécutez l'application Flutter en tant qu'application de bureau, comme indiqué ci-dessous. Vous pouvez aussi ouvrir ce projet dans votre IDE et utiliser les outils qui s'y trouvent pour exécuter l'application.
Opération réussie. Le code de démarrage de l'écran d'accueil de MyArtist doit s'exécuter. Vous devriez voir l'écran d'accueil MyArtist. La mise en page semble correcte sur les ordinateurs, mais sur les appareils mobiles… ce n'est pas l'idéal. Tout d'abord, elle ne fait pas honneur à la technologie. Ne vous inquiétez pas, vous allez résoudre ce problème !
À la découverte du code
Parcourez ensuite le code.
Ouvrez lib/src/features/home/view/home_screen.dart
, qui contient les éléments suivants :
import 'package:adaptive_components/adaptive_components.dart';
import 'package:flutter/material.dart';
import '../../../shared/classes/classes.dart';
import '../../../shared/extensions.dart';
import '../../../shared/providers/providers.dart';
import '../../../shared/views/views.dart';
import '../../playlists/view/playlist_songs.dart';
import 'view.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
final PlaylistsProvider playlistProvider = PlaylistsProvider();
final List<Playlist> playlists = playlistProvider.playlists;
final Playlist topSongs = playlistProvider.topSongs;
final Playlist newReleases = playlistProvider.newReleases;
final ArtistsProvider artistsProvider = ArtistsProvider();
final List<Artist> artists = artistsProvider.artists;
return LayoutBuilder(
builder: (context, constraints) {
// Add conditional mobile layout
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2), // Modify this line
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(2), // Modify this line
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2), // Modify this line
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.all(2), // Modify this line
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
// Add spacer between tables
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.all(2), // Modify this line
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
Ce fichier importe material.dart
et implémente un widget avec état à l'aide de deux classes :
- L'instruction
import
rend les composants Material disponibles. - La classe
HomeScreen
représente toute la page affichée. - La méthode
build()
de la classe_HomeScreenState
créée la racine de l'arborescence des widgets, ce qui affecte la façon dont tous les widgets de l'interface utilisateur sont créés.
4. Exploiter la typographie
Le texte est présent partout. Il constitue un outil de communication utile avec l'utilisateur. Votre application est-elle conçue pour être conviviale et amusante, ou peut-être fiable et professionnelle ? Ce n'est pas pour rien que votre application bancaire préférée n'utilise pas Comic Sans. La façon dont le texte est présenté détermine la première impression de l'utilisateur face à votre application. Voici quelques astuces pour utiliser du texte de manière plus réfléchie.
Préférer l'image aux longs discours
Dans la mesure du possible, remplacez le texte par des visuels. Par exemple, dans l'application de démarrage, NavigationRail
contient des onglets pour chaque navigation principale, mais les icônes de démarrage sont identiques :
Cela ne facilite pas la tâche de l'utilisateur, qui doit lire le texte de chaque onglet. Commencez par ajouter des repères visuels afin que l'utilisateur puisse jeter un coup d'œil rapide aux icônes principales pour trouver l'onglet souhaité. Cela simplifie également la localisation et l'accessibilité.
Dans lib/src/shared/router.dart
, ajoutez des icônes de démarrage distinctes pour chaque destination de navigation (accueil, playlist et contacts) :
const List<NavigationDestination> destinations = [
NavigationDestination(
label: 'Home',
icon: Icon(Icons.home), // Modify this line
route: '/',
),
NavigationDestination(
label: 'Playlists',
icon: Icon(Icons.playlist_add_check), // Modify this line
route: '/playlists',
),
NavigationDestination(
label: 'Artists',
icon: Icon(Icons.people), // Modify this line
route: '/artists',
),
];
Des problèmes ?
Si votre application ne s'exécute pas correctement, vérifiez que vous n'avez pas fait de faute de frappe. Si nécessaire, utilisez le code contenu dans les liens suivants pour résoudre la situation.
Bien choisir ses polices
Les polices définissent la personnalité de votre application. Il est donc essentiel d'opter pour la police appropriée. Lorsque vous sélectionnez une police, tenez compte des points suivants :
- Sans Serif ou Serif : les polices Serif présentent des traits décoratifs ou des "empattements" à la fin des lettres, et sont considérées comme plus formelles. Les polices Sans Serif ne comportent pas de traits décoratifs et sont généralement considérées comme plus informelles. Un T majuscule en police Sans Serif et un T majuscule en police Serif
- Polices tout en majuscules : cette utilisation permet d'attirer l'attention sur de petites portions de texte (titres, par exemple) mais peut donner l'impression à l'utilisateur qu'on lui crie dessus, l'incitant à ignorer complètement le message.
- Majuscule au début de chaque mot : lorsque vous ajoutez un titre ou un libellé, tenez compte de la façon dont vous utilisez les majuscules : la casse de titre, avec une majuscule au début de chaque mot ("Voici Un Titre Écrit Avec Une Majuscule Au Début De Chaque Mot"), est plus formelle. Majuscule en début de phrase : seuls les noms propres et le premier mot du texte prennent une majuscule ("Voici un titre avec une majuscule en début de phrase"). Cette forme est plus informelle.
- Crénage (espacement entre chaque lettre) : longueur du trait (largeur du texte complet sur l'écran) et hauteur de la ligne (hauteur de chaque ligne de texte) : le recours insuffisant ou excessif à ce paramètre rend votre application moins lisible. Par exemple, vous risquez sûrement de perdre facilement le fil de votre lecture face à un grand bloc de texte ininterrompu.
Gardez cela à l'esprit, puis accédez à Google Fonts et choisissez une police Sans Serif. Vous pouvez par exemple opter pour Montserrat, car l'application musicale est conçue pour être ludique et amusante.
À partir de la ligne de commande, récupérez le package google_fonts
. Le fichier pubspec est également mis à jour pour ajouter les polices en tant que dépendance d'application.
$ flutter pub add google_fonts
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
// Make sure these lines are present from here...
<key>com.apple.security.network.client</key> //For macOS only
<true/>
// .. To here
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
Dans lib/src/shared/extensions.dart
, importez le nouveau package :
import 'package:google_fonts/google_fonts.dart'; // Add this line.
Définir TextTheme:
de Montserrat
TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line
Effectuez un hot reload de pour activer les modifications. (Utilisez le bouton de votre IDE ou saisissez r
dans la ligne de commande pour effectuer un hot reload) :
Vous devriez voir les nouvelles icônes NavigationRail
apparaître avec le texte affiché dans la police Montserrat.
Des problèmes ?
Si votre application ne s'exécute pas correctement, vérifiez que vous n'avez pas fait de faute de frappe. Si nécessaire, utilisez le code contenu dans les liens suivants pour résoudre la situation.
5. Définir le thème
Les thèmes permettent d'apporter une conception structurée et uniforme à une application en spécifiant un système de couleurs et de styles de texte défini. Grâce aux thèmes, vous pouvez rapidement mettre en œuvre une interface utilisateur sans avoir à vous soucier des détails mineurs, comme spécifier la couleur exacte de chaque widget.
Les développeurs Flutter conçoivent généralement des composants à thème personnalisé de l'une des deux manières suivantes :
- En créant des widgets personnalisés individuels, chacun avec son propre thème.
- En configurant des thèmes restreints pour les widgets par défaut.
Cet exemple utilise un fournisseur de thème situé dans lib/src/shared/providers/theme.dart
pour créer des widgets et des couleurs associés à un thème cohérent dans l'application :
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
class NoAnimationPageTransitionsBuilder extends PageTransitionsBuilder {
const NoAnimationPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return child;
}
}
class ThemeSettingChange extends Notification {
ThemeSettingChange({required this.settings});
final ThemeSettings settings;
}
class ThemeProvider extends InheritedWidget {
const ThemeProvider(
{Key? key,
required this.settings,
required this.lightDynamic,
required this.darkDynamic,
required Widget child})
: super(key: key, child: child);
final ValueNotifier<ThemeSettings> settings;
final ColorScheme? lightDynamic;
final ColorScheme? darkDynamic;
final pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: NoAnimationPageTransitionsBuilder(),
TargetPlatform.macOS: NoAnimationPageTransitionsBuilder(),
TargetPlatform.windows: NoAnimationPageTransitionsBuilder(),
},
);
Color custom(CustomColor custom) {
if (custom.blend) {
return blend(custom.color);
} else {
return custom.color;
}
}
Color blend(Color targetColor) {
return Color(
Blend.harmonize(targetColor.value, settings.value.sourceColor.value));
}
Color source(Color? target) {
Color source = settings.value.sourceColor;
if (target != null) {
source = blend(target);
}
return source;
}
ColorScheme colors(Brightness brightness, Color? targetColor) {
final dynamicPrimary = brightness == Brightness.light
? lightDynamic?.primary
: darkDynamic?.primary;
return ColorScheme.fromSeed(
seedColor: dynamicPrimary ?? source(targetColor),
brightness: brightness,
);
}
ShapeBorder get shapeMedium => RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
);
CardTheme cardTheme() {
return CardTheme(
elevation: 0,
shape: shapeMedium,
clipBehavior: Clip.antiAlias,
);
}
ListTileThemeData listTileTheme(ColorScheme colors) {
return ListTileThemeData(
shape: shapeMedium,
selectedColor: colors.secondary,
);
}
AppBarTheme appBarTheme(ColorScheme colors) {
return AppBarTheme(
elevation: 0,
backgroundColor: colors.surface,
foregroundColor: colors.onSurface,
);
}
TabBarTheme tabBarTheme(ColorScheme colors) {
return TabBarTheme(
labelColor: colors.secondary,
unselectedLabelColor: colors.onSurfaceVariant,
indicator: BoxDecoration(
border: Border(
bottom: BorderSide(
color: colors.secondary,
width: 2,
),
),
),
);
}
BottomAppBarTheme bottomAppBarTheme(ColorScheme colors) {
return BottomAppBarTheme(
color: colors.surface,
elevation: 0,
);
}
BottomNavigationBarThemeData bottomNavigationBarTheme(ColorScheme colors) {
return BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
backgroundColor: colors.surfaceVariant,
selectedItemColor: colors.onSurface,
unselectedItemColor: colors.onSurfaceVariant,
elevation: 0,
landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
);
}
NavigationRailThemeData navigationRailTheme(ColorScheme colors) {
return const NavigationRailThemeData();
}
DrawerThemeData drawerTheme(ColorScheme colors) {
return DrawerThemeData(
backgroundColor: colors.surface,
);
}
ThemeData light([Color? targetColor]) {
final _colors = colors(Brightness.light, targetColor);
return ThemeData.light().copyWith(
pageTransitionsTheme: pageTransitionsTheme,
colorScheme: _colors,
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(_colors),
bottomAppBarTheme: bottomAppBarTheme(_colors),
bottomNavigationBarTheme: bottomNavigationBarTheme(_colors),
navigationRailTheme: navigationRailTheme(_colors),
tabBarTheme: tabBarTheme(_colors),
drawerTheme: drawerTheme(_colors),
scaffoldBackgroundColor: _colors.background,
useMaterial3: true,
);
}
ThemeData dark([Color? targetColor]) {
final _colors = colors(Brightness.dark, targetColor);
return ThemeData.dark().copyWith(
pageTransitionsTheme: pageTransitionsTheme,
colorScheme: _colors,
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(_colors),
bottomAppBarTheme: bottomAppBarTheme(_colors),
bottomNavigationBarTheme: bottomNavigationBarTheme(_colors),
navigationRailTheme: navigationRailTheme(_colors),
tabBarTheme: tabBarTheme(_colors),
drawerTheme: drawerTheme(_colors),
scaffoldBackgroundColor: _colors.background,
useMaterial3: true,
);
}
ThemeMode themeMode() {
return settings.value.themeMode;
}
ThemeData theme(BuildContext context, [Color? targetColor]) {
final brightness = MediaQuery.of(context).platformBrightness;
return brightness == Brightness.light
? light(targetColor)
: dark(targetColor);
}
static ThemeProvider of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ThemeProvider>()!;
}
@override
bool updateShouldNotify(covariant ThemeProvider oldWidget) {
return oldWidget.settings != settings;
}
}
class ThemeSettings {
ThemeSettings({
required this.sourceColor,
required this.themeMode,
});
final Color sourceColor;
final ThemeMode themeMode;
}
Color randomColor() {
return Color(Random().nextInt(0xFFFFFFFF));
}
// Custom Colors
const linkColor = CustomColor(
name: 'Link Color',
color: Color(0xFF00B0FF),
);
class CustomColor {
const CustomColor({
required this.name,
required this.color,
this.blend = true,
});
final String name;
final Color color;
final bool blend;
Color value(ThemeProvider provider) {
return provider.custom(this);
}
}
Pour utiliser le fournisseur, créez une instance et transmettez-la à l'objet défini au niveau du thème dans MaterialApp
, situé dans lib/src/shared/app.dart
. Tous les objets Theme
imbriqués en hériteront :
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'playback/bloc/bloc.dart';
import 'providers/theme.dart';
import 'router.dart';
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final settings = ValueNotifier(ThemeSettings(
sourceColor: Colors.pink,
themeMode: ThemeMode.system,
));
@override
Widget build(BuildContext context) {
return BlocProvider<PlaybackBloc>(
create: (context) => PlaybackBloc(),
child: DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) => ThemeProvider(
lightDynamic: lightDynamic,
darkDynamic: darkDynamic,
settings: settings,
child: NotificationListener<ThemeSettingChange>(
onNotification: (notification) {
settings.value = notification.settings;
return true;
},
child: ValueListenableBuilder<ThemeSettings>(
valueListenable: settings,
builder: (context, value, _) {
final theme = ThemeProvider.of(context); // Add this line
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: theme.light(settings.value.sourceColor), // Add this line
routeInformationParser: appRouter.routeInformationParser,
routerDelegate: appRouter.routerDelegate,
);
},
),
)),
),
);
}
}
Maintenant que le thème est configuré, choisissez les couleurs de l'application.
Choisir la palette de couleurs appropriée n'est pas toujours simple. Vous avez peut-être une idée de la couleur principale, mais vous voudrez sûrement que votre application en ait plusieurs. Quelle couleur de texte souhaitez-vous ? Titre ? Contenu ? Liens ? Qu'en est-il de la couleur d'arrière-plan ? Material Theme Builder est un outil Web (introduit dans Material 3) qui vous aide à sélectionner un ensemble de couleurs complémentaires pour votre application.
Pour choisir une couleur source pour l'application, ouvrez Material Theme Builder et explorez les différentes couleurs pour l'interface utilisateur. Il est important de sélectionner une couleur qui correspond à l'esthétique de la marque et/ou à vos préférences.
Après avoir créé un thème, effectuez un clic droit sur la bulle de couleur principale : cette action ouvre une boîte de dialogue contenant la valeur hexadécimale. Copiez cette valeur. (Vous pouvez aussi définir la couleur à l'aide de cette boîte de dialogue.)
Transmettez la valeur hexadécimale de la couleur principale au fournisseur de thème. Par exemple, la couleur hexadécimale #00cbe6
est spécifiée en tant que Color(0xff00cbe6)
. ThemeProvider
génère ThemeData
, qui contient l'ensemble des couleurs complémentaires que vous avez prévisualisées dans Material Theme Builder :
final settings = ValueNotifier(ThemeSettings(
sourceColor: Color(0xff00cbe6), // Replace this color
themeMode: ThemeMode.system,
));
Redémarrez l'application à chaud. Une fois la couleur principale configurée, l'application commence à être plus expressive. Accédez à toutes les nouvelles couleurs en référençant le thème dans le contexte et en saisissant ColorScheme
:
final colors = Theme.of(context).colorScheme;
Pour utiliser une couleur spécifique, accédez à un rôle de couleur sur colorScheme
. Accédez à lib/src/shared/views/outlined_card.dart
et attribuez une bordure à OutlinedCard
:
class _OutlinedCardState extends State<OutlinedCard> {
@override
Widget build(BuildContext context) {
return MouseRegion(
cursor: widget.clickable
? SystemMouseCursors.click
: SystemMouseCursors.basic,
child: Container(
child: widget.child,
// Add from here...
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
),
// ... To here.
),
);
}
}
Material 3 introduit des rôles de couleur nuancés qui se complètent et peuvent s'utiliser sur l'ensemble de l'interface utilisateur pour ajouter de nouvelles couches d'expression. Ces nouveaux rôles de couleur sont les suivants :
Primary
,OnPrimary
,PrimaryContainer
,OnPrimaryContainer
Secondary
,OnSecondary
,SecondaryContainer
,OnSecondaryContainer
Tertiary
,OnTertiary
,TertiaryContainer
,OnTertiaryContainer
Error
,OnError
,ErrorContainer
,OnErrorContainer
Background
,OnBackground
Surface
,OnSurface
,SurfaceVariant
,OnSurfaceVariant
Shadow
,Outline
,InversePrimary
De plus, les nouveaux jetons de conception sont compatibles avec les thèmes clair et sombre :
Ces rôles de couleur peuvent être utilisés pour attribuer une signification et une importance à différentes parties de l'interface utilisateur. Même si un composant n'est pas très visible, il peut toujours bénéficier d'une couleur dynamique.
L'utilisateur peut régler la luminosité de l'application dans les paramètres système de l'appareil. Dans lib/src/shared/app.dart
, lorsque l'appareil est défini sur le mode sombre, rétablissez le thème et le mode sombre sur MaterialApp
.
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: theme.light(settings.value.sourceColor),
darkTheme: theme.dark(settings.value.sourceColor), // Add this line
themeMode: theme.themeMode(), // Add this line
routeInformationParser: appRouter.routeInformationParser,
routerDelegate: appRouter.routerDelegate,
);
Cliquez sur l'icône en forme de lune en haut à droite pour activer le mode sombre.
Des problèmes ?
Si votre application ne fonctionne pas correctement, utilisez le code contenu dans le lien suivant pour trouver une solution.
6. Ajouter un design adaptatif
Avec Flutter, vous pouvez créer des applications qui s'exécutent presque partout, mais cela ne veut pas dire que chaque application aura le même comportement où qu'elle se trouve. Les utilisateurs s'attendent désormais à des comportements et des fonctionnalités différents selon les plates-formes.
Material Design propose des packages Flutter pour faciliter l'utilisation des mises en page adaptatives. Ils sont disponibles sur GitHub.
Lorsque vous créez une application adaptative et multiplate-forme, tenez compte des différences suivantes selon les plates-formes :
- Mode de saisie : souris, mode tactile ou manette de jeu
- Taille de police, orientation de l'appareil et distance de visionnage
- Taille et format de l'écran : téléphone, tablette, pliable, ordinateur, Web
Le fichier lib/src/shared/views/adaptive_navigation.dart
contient une classe de navigation dans laquelle vous pouvez fournir une liste de destinations et de contenus pour l'affichage du corps. Comme vous utilisez cette mise en page sur plusieurs écrans, vous devez transmettre une mise en page de base partagée à chaque thème enfant. Les rails de navigation conviennent aux ordinateurs et aux grands écrans, mais vous pouvez adapter la mise en page aux mobiles en affichant à la place une barre de navigation inférieure sur mobile.
import 'package:flutter/material.dart';
class AdaptiveNavigation extends StatelessWidget {
const AdaptiveNavigation({
Key? key,
required this.destinations,
required this.selectedIndex,
required this.onDestinationSelected,
required this.child,
}) : super(key: key);
final List<NavigationDestination> destinations;
final int selectedIndex;
final void Function(int index) onDestinationSelected;
final Widget child;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, dimens) {
// Tablet Layout
if (dimens.maxWidth >= 600) { // Add this line
return Scaffold(
body: Row(
children: [
NavigationRail(
extended: dimens.maxWidth >= 800,
minExtendedWidth: 180,
destinations: destinations
.map((e) => NavigationRailDestination(
icon: e.icon,
label: Text(e.label),
))
.toList(),
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
),
Expanded(child: child),
],
),
);
} // Add this line
// Mobile Layout
// Add from here...
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
destinations: destinations,
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
),
);
// ... To here.
},
);
}
}
Tous les écrans ne sont pas de la même taille. Si vous essayez d'afficher la version classique de votre application sur votre téléphone, vous devrez plisser les yeux et zoomer pour tout voir. Vous voulez que l'application change d'apparence en fonction de l'écran sur lequel elle s'affiche ? Grâce au responsive design, vous vous assurez que votre application s'affiche correctement sur tous les écrans.
Pour accroître la réactivité de votre application, ajoutez quelques points d'arrêt adaptatifs (à ne pas confondre avec les points d'arrêt de débogage). Ces points d'arrêt indiquent les tailles d'écran pour lesquelles votre application doit modifier sa mise en page.
Les écrans plus petits ne peuvent pas afficher autant de contenu que les plus grands sans réduire la taille des données. Pour éviter que l'application ne ressemble à une application de bureau qui a été réduite, créez une mise en page distincte pour les mobiles et utilisez des onglets pour répartir le contenu. Vous donnerez ainsi l'impression qu'il pourrait s'agir d'une application mobile native.
Les méthodes d'extension suivantes (définies dans le projet MyArtist sous lib/src/shared/extensions.dart
) constituent un bon point de départ pour concevoir des mises en page optimisées pour différentes cibles.
extension BreakpointUtils on BoxConstraints {
bool get isTablet => maxWidth > 730;
bool get isDesktop => maxWidth > 1200;
bool get isMobile => !isTablet && !isDesktop;
}
Un écran de plus de 730 pixels (dans le sens de la longueur), mais de moins de 1 2 00 pixels, est considéré comme une tablette. Tout écran supérieur à 1 200 pixels est considéré comme un ordinateur de bureau. Si un appareil n'est ni une tablette, ni un ordinateur de bureau, alors il est considéré comme un mobile. Pour en savoir plus sur les points d'arrêt adaptatifs, consultez la page material.io. Vous pouvez envisager d'utiliser le package adaptive_breakpoints.
Le format responsif de l'écran d'accueil utilise AdaptiveContainer
et AdaptiveColumn
en fonction de la grille à 12 colonnes à l'aide des packages adaptive_components et adaptive_breakpoints pour implémenter une mise en page responsive en grille dans Material Design.
return LayoutBuilder(
builder: (context, constraints) {
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 40,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 20,
),
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(15),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 25),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
Une mise en page adaptative nécessite deux mises en page : une pour les mobiles et une mise en page responsive pour les plus grands écrans. Pour le moment, LayoutBuilder
ne renvoie qu'une mise en page pour ordinateur. Dans lib/src/features/home/view/home_screen.dart
, créez la mise en page pour mobile en tant que TabBar
et TabBarView
avec quatre onglets.
import 'package:adaptive_components/adaptive_components.dart';
import 'package:flutter/material.dart';
import '../../../shared/classes/classes.dart';
import '../../../shared/extensions.dart';
import '../../../shared/providers/providers.dart';
import '../../../shared/views/views.dart';
import '../../playlists/view/playlist_songs.dart';
import 'view.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
final PlaylistsProvider playlistProvider = PlaylistsProvider();
final List<Playlist> playlists = playlistProvider.playlists;
final Playlist topSongs = playlistProvider.topSongs;
final Playlist newReleases = playlistProvider.newReleases;
final ArtistsProvider artistsProvider = ArtistsProvider();
final List<Artist> artists = artistsProvider.artists;
return LayoutBuilder(
builder: (context, constraints) {
// Add from here...
if (constraints.isMobile) {
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
centerTitle: false,
title: const Text('Good morning'),
actions: const [BrightnessToggle()],
bottom: const TabBar(
isScrollable: true,
tabs: [
Tab(text: 'Home'),
Tab(text: 'Recently Played'),
Tab(text: 'New Releases'),
Tab(text: 'Top Songs'),
],
),
),
body: LayoutBuilder(
builder: (context, constraints) => TabBarView(
children: [
SingleChildScrollView(
child: Column(
children: [
const HomeHighlight(),
HomeArtists(
artists: artists,
constraints: constraints,
),
],
),
),
HomeRecent(
playlists: playlists,
axis: Axis.vertical,
),
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
],
),
),
),
);
}
// ... To here.
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 40,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 20,
),
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(15),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 25),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
Des problèmes ?
Si votre application ne fonctionne pas correctement, utilisez le code contenu dans le lien suivant pour trouver une solution.
Utiliser les espaces blancs
Les espaces blancs constituent un outil visuel important pour votre application. Ils permettent de créer une pause organisationnelle entre les sections.
Il vaut mieux avoir trop d'espaces blancs que pas assez. Il est préférable d'ajouter un espace blanc, plutôt que de réduire la taille de la police ou des éléments visuels pour mieux s'adapter à l'espace.
Le manque d'espace blanc peut poser des difficultés aux personnes souffrant de problèmes de vue. Trop d'espaces blancs peuvent nuire à la cohésion de votre mise en page et donner l'impression que votre interface utilisateur est mal organisée. Par exemple, consultez les captures d'écran suivantes :
Vous allez maintenant ajouter un espace blanc à l'écran d'accueil pour lui donner plus d'espace. Vous allez ensuite modifier la mise en page pour ajuster l'espacement.
Encapsulez un widget avec un objet Padding
pour ajouter des espaces blancs autour de ce widget. Augmentez à 35 toutes les valeurs de marge intérieure actuellement contenues dans lib/src/features/home/view/home_screen.dart
:
Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.all(35), // Modify this line
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) => PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
// Add spacer between tables
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.all(35), // Modify this line
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) => PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
Effectuez un hot reload de l'application. Elle devrait avoir le même aspect que précédemment, mais avec plus d'espaces blancs entre les widgets. La marge intérieure supplémentaire présente mieux, mais la bannière en surbrillance située en haut de page est encore trop proche des bords.
Dans lib/src/features/home/view/home_highlight.dart
, définissez la marge intérieure sur 35 :
class HomeHighlight extends StatelessWidget {
const HomeHighlight({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Clickable(
child: SizedBox(
height: 275,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(
'assets/images/news/concert.jpeg',
fit: BoxFit.cover,
),
),
),
onTap: () => launch('https://docs.flutter.dev'),
),
),
),
],
);
}
}
Effectuez un hot reload de l'application. Les deux playlists du bas ne contiennent pas d'espaces blancs. Elles semblent donc appartenir à la même table. Ce n'est pas le cas, et vous allez y remédier.
Ajoutez un espace blanc entre les playlists en insérant un widget de taille dans Row
qui les contient. Dans lib/src/features/home/view/home_screen.dart
, ajoutez SizedBox
avec une largeur de 35 :
Padding(
padding: const EdgeInsets.all(35),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
],
),
),
const SizedBox(width: 35), // Add this line
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
],
),
),
],
),
),
Effectuez un hot reload de l'application. L'application devrait avoir l'aspect suivant :
Il y a maintenant beaucoup d'espace pour le contenu de l'écran d'accueil, mais tout semble trop séparé et il n'y a pas de cohésion entre les sections.
Jusqu'à présent, vous avez défini toutes les marges intérieures (horizontales et verticales) des widgets de l'écran d'accueil sur 35 avec la valeur EdgeInsets.all(35)
, mais vous pouvez également définir la marge intérieure de chacun des bords indépendamment. Personnalisez la marge intérieure pour qu'elle s'adapte mieux à l'espace.
EdgeInsets.LTRB()
définit individuellement les valeurs à gauche, en haut, à droite et en bas.EdgeInsets.symmetric()
définit les marges intérieures à la verticale (en haut et en bas) et à l'horizontale (à gauche et à droite) de sorte qu'elles soient équivalentes.EdgeInsets.only()
définit uniquement les bords spécifiés.
Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 25, 20, 10), // Modify this line
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
), // Modify this line
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(15), // Modify this line
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 8, bottom: 8), // Modify this line
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 25),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 8, bottom: 8), // Modify this line
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
Dans lib/src/features/home/view/home_highlight.dart
, définissez la marge intérieure gauche et droite sur 35, et la marge intérieure inférieure et supérieure sur 5 :
class HomeHighlight extends StatelessWidget {
const HomeHighlight({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: Padding(
// Modify this line
padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 5),
child: Clickable(
child: SizedBox(
height: 275,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(
'assets/images/news/concert.jpeg',
fit: BoxFit.cover,
),
),
),
onTap: () => launch('https://docs.flutter.dev'),
),
),
),
],
);
}
}
Effectuez un hot reload de l'application. La mise en page et l'espacement sont nettement meilleurs ! Pour finir, ajoutez un peu de mouvement et d'animation.
Des problèmes ?
Si votre application ne fonctionne pas correctement, utilisez le code contenu dans le lien suivant pour trouver une solution.
7. Ajouter du mouvement et de l'animation
Le mouvement et l'animation sont particulièrement efficaces pour introduire du dynamisme et de l'énergie, ainsi que pour fournir un retour d'information lorsque l'utilisateur interagit avec l'application.
Configurer des animations de transition d'écran
ThemeProvider
définit PageTransitionsTheme
avec des animations de transition d'écran pour les plates-formes mobiles (iOS, Android). Les utilisateurs d'ordinateur reçoivent déjà un retour d'information après avoir cliqué avec leur souris ou appuyé sur leur pavé tactile, de sorte qu'une animation de transition de page n'est pas nécessaire.
Flutter offre des animations de transition d'écran que vous pouvez configurer pour votre application en fonction de la plate-forme cible, comme illustré dans lib/src/shared/providers/theme.dart
:
final pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: NoAnimationPageTransitionsBuilder(),
TargetPlatform.macOS: NoAnimationPageTransitionsBuilder(),
TargetPlatform.windows: NoAnimationPageTransitionsBuilder(),
},
);
Transmettez PageTransitionsTheme
aux thèmes clair et sombre dans lib/src/shared/providers/theme.dart
ThemeData light([Color? targetColor]) {
final _colors = colors(Brightness.light, targetColor);
return ThemeData.light().copyWith(
pageTransitionsTheme: pageTransitionsTheme, // Add this line
colorScheme: ColorScheme.fromSeed(
seedColor: source(targetColor),
brightness: Brightness.light,
),
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(),
tabBarTheme: tabBarTheme(_colors),
scaffoldBackgroundColor: _colors.background,
);
}
ThemeData dark([Color? targetColor]) {
final _colors = colors(Brightness.dark, targetColor);
return ThemeData.dark().copyWith(
pageTransitionsTheme: pageTransitionsTheme, // Add this line
colorScheme: ColorScheme.fromSeed(
seedColor: source(targetColor),
brightness: Brightness.dark,
),
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(),
tabBarTheme: tabBarTheme(_colors),
scaffoldBackgroundColor: _colors.background,
);
}
Sans animation sur iOS | Avec animation sur iOS |
Des problèmes ?
Si votre application ne fonctionne pas correctement, utilisez le code contenu dans le lien suivant pour trouver une solution.
Ajouter des états de pointage
Pour ajouter du mouvement à une application de bureau, vous pouvez configurer des états de pointage, où un widget change d'état (couleur, forme ou contenu) lorsque l'utilisateur le survole avec le curseur.
Par défaut, la classe _OutlinedCardState
(utilisée pour les mosaïques de la playlist "Écouté récemment") renvoie MouseRegion
, qui transforme la flèche du curseur en pointeur au survol, mais vous pouvez ajouter un retour visuel plus important.
Ouvrez lib/src/shared/views/outlined_card.dart et remplacez son contenu par l'intégration suivante pour introduire un état _hovered
.
import 'package:flutter/material.dart';
class OutlinedCard extends StatefulWidget {
const OutlinedCard({
Key? key,
required this.child,
this.clickable = true,
}) : super(key: key);
final Widget child;
final bool clickable;
@override
State<OutlinedCard> createState() => _OutlinedCardState();
}
class _OutlinedCardState extends State<OutlinedCard> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final borderRadius = BorderRadius.circular(_hovered ? 20 : 8);
const animationCurve = Curves.easeInOut;
return MouseRegion(
onEnter: (_) {
if (!widget.clickable) return;
setState(() {
_hovered = true;
});
},
onExit: (_) {
if (!widget.clickable) return;
setState(() {
_hovered = false;
});
},
cursor: widget.clickable ? SystemMouseCursors.click : SystemMouseCursors.basic,
child: AnimatedContainer(
duration: kThemeAnimationDuration,
curve: animationCurve,
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
borderRadius: borderRadius,
),
foregroundDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSurface.withOpacity(
_hovered ? 0.12 : 0,
),
borderRadius: borderRadius,
),
child: TweenAnimationBuilder<BorderRadius>(
duration: kThemeAnimationDuration,
curve: animationCurve,
tween: Tween(begin: BorderRadius.zero, end: borderRadius),
builder: (context, borderRadius, child) => ClipRRect(
clipBehavior: Clip.antiAlias,
borderRadius: borderRadius,
child: child,
),
child: widget.child,
),
),
);
}
}
Effectuez un hot reload de l'application, puis passez le curseur sur l'une des mosaïques de la playlist "Écouté récemment".
OutlinedCard
change l'opacité et arrondit les angles.
Enfin, animez les numéros de chansons d'une playlist en les changeant en bouton de lecture à l'aide du widget HoverableSongPlayButton
défini dans lib/src/shared/views/hoverable_song_play_button.dart
. Dans lib/src/features/playlists/view/playlist_songs.dart
, encapsulez le widget Center
(qui contient le numéro du titre) avec HoverableSongPlayButton
:
HoverableSongPlayButton( // Add this line
hoverMode: HoverMode.overlay, // Add this line
song: playlist.songs[index], // Add this line
child: Center( // Modify this line
child: Text(
(index + 1).toString(),
textAlign: TextAlign.center,
),
),
), // Add this line
Effectuez un hot reload de l'application, puis pointez le curseur sur le numéro du titre dans la playlist Top titre du jour ou Nouveautés.
Le nombre s'anime et affiche un bouton de lecture qui permet de lire le titre lorsque vous cliquez dessus.
Consultez le code final du projet sur GitHub.
8. Félicitations !
Vous avez terminé cet atelier de programmation ! Vous avez appris qu'il existe de nombreuses petites modifications que vous pouvez apporter à une application pour la rendre plus agréable, mais aussi plus accessible, plus localisable et mieux adaptée à plusieurs plates-formes. On peut citer, entre autres, les techniques suivantes :
- Typographie : le texte est bien plus qu'un outil de communication. Servez-vous du mode d'affichage du texte pour produire un effet positif sur l'expérience des utilisateurs et leur perception de votre application.
- Thématisation : établissez un système de conception que vous pouvez utiliser de manière fiable sans avoir à prendre de décisions de conception pour chaque widget.
- Adaptabilité : tenez compte de l'appareil et de la plate-forme sur lesquels l'utilisateur exécute votre application et de ses fonctionnalités. Pensez à la taille de l'écran et à la manière dont votre application s'affiche.
- Mouvement et animation : ajouter de l'animation à votre application donne de l'énergie à l'expérience de navigation et, plus concrètement, fournit un retour d'information aux utilisateurs.
Grâce à quelques ajustements, vous pouvez mettre en valeur votre application.
Avant | Après |
Liens associés
Nous espérons que cet atelier vous aidera à créer de superbes applications dans Flutter !
Si vous appliquez l'un des conseils ou astuces mentionnés ici (ou si vous en avez à partager), n'hésitez pas à nous le faire savoir. Contactez-nous sur Twitter (@rodydavis) et @khanhnwin.
Les ressources suivantes peuvent également vous être utiles.
Thématisation
- Material Theme Builder (outil)
Ressources adaptatives et responsives :
- Decoding Flutter on Adaptive vs Responsive (vidéo)
- Adaptive layouts (vidéo du Boring Flutter Development Show)
- Creating responsive and adaptive apps (flutter.dev)
- Adaptive Material components for Flutter (bibliothèque sur GitHub)
- 5 things you can do to prepare your app for large screens (vidéo de la conférence Google I/O 2021)
Ressources générales de conception :
- The little things: Becoming the mythical designer-developer (vidéo de Flutter Engage)
- Material Design 3 for Foldable Devices (material.io)
Vous pouvez également échanger avec la communauté Flutter.
Lancez-vous et mettez en valeur l'univers des applications !