Mettez en valeur votre application Flutter

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 :

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 :

4a0f6509a18aaf30.gif 1557a5d9dab19d75.gif

Qu'attendez-vous de cet atelier de programmation ?

Je suis novice en la matière et je voudrais avoir un bon aperçu. Je connais un peu le sujet, mais j'aimerais revoir certains points. Je recherche un exemple de code à utiliser dans mon projet. Je cherche des explications sur un point spécifique.

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.

a3c16fc17be25f6c.png Exécutez 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 !

9ebe486bc7dfa36b.png 1b30e16df3cde215.png

À 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 :

86c5f73b3aa5fd35.png

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é.

a3c16fc17be25f6c.png 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',
  ),
];

23278e4f4610fbf4.png

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. 34bf54e4cad90101.png 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.

a3c16fc17be25f6c.png À 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>

a3c16fc17be25f6c.png Dans lib/src/shared/extensions.dart, importez le nouveau package :

import 'package:google_fonts/google_fonts.dart';  // Add this line.

a3c16fc17be25f6c.png Définir TextTheme: de Montserrat

TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line

a3c16fc17be25f6c.png Effectuez un hot reload de 7f9a9e103c7b5e5.png pour activer les modifications. (Utilisez le bouton de votre IDE ou saisissez r dans la ligne de commande pour effectuer un hot reload) :

ff6f09f4cc39c21e.png

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

a3c16fc17be25f6c.pngPour 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.

a3c16fc17be25f6c.pngPour 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.)

a6201933c4be275c.gif

a3c16fc17be25f6c.pngTransmettez 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;

a3c16fc17be25f6c.pngPour 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 :

7b51703ed96196a4.png

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.

a3c16fc17be25f6c.png 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.

60ad6e64df0c5957.gif

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

a3c16fc17be25f6c.png 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.
      },
    );
  }
}

d1825d6f0d23314.png

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

a3c16fc17be25f6c.pngUne 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,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
     },
   );
 }
}

2e4115a01d76e7ae.png

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 :

f50d2fe899e57e42.png

cdf5a34a7658a15e.png

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.

a3c16fc17be25f6c.png 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,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );

a3c16fc17be25f6c.png 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.

a3c16fc17be25f6c.png 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'),
            ),
          ),
        ),
      ],
    );
  }
}

a3c16fc17be25f6c.png 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.

df1d9af97d039cc8.png

a3c16fc17be25f6c.png 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,
            ),
          ],
        ),
      ),
    ],
  ),
),

a3c16fc17be25f6c.png Effectuez un hot reload de l'application. L'application devrait avoir l'aspect suivant :

89411cc17daf641b.png

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.

a3c16fc17be25f6c.png 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,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );

a3c16fc17be25f6c.png 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'),
            ),
          ),
        ),
      ],
    );
  }
}

a3c16fc17be25f6c.png 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.

2776abfa6ca738af.png

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

a3c16fc17be25f6c.png 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.

a3c16fc17be25f6c.png 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,
        ),
      ),
    );
  }
}

a3c16fc17be25f6c.png Effectuez un hot reload de l'application, puis passez le curseur sur l'une des mosaïques de la playlist "Écouté récemment".

61c08e46a5926e10.gif

OutlinedCard change l'opacité et arrondit les angles.

a3c16fc17be25f6c.png 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

a3c16fc17be25f6c.pngEffectuez 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.

82587ceb5452eedf.gif

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

Ressources adaptatives et responsives :

Ressources générales de conception :

Vous pouvez également échanger avec la communauté Flutter.

Lancez-vous et mettez en valeur l'univers des applications !