Mache deine Flutter-App langweilig zu schön

1. Einführung

Flutter ist das UI-Toolkit von Google, mit dem Sie mit einer einzigen Codebasis ansprechende, nativ kompilierte Apps für Mobilgeräte, Web und Desktop erstellen können. Flutter arbeitet mit bereits vorhandenem Code, wird von Entwicklern und Organisationen auf der ganzen Welt verwendet und ist kostenlos und Open Source.

In diesem Codelab verbessern Sie eine Flutter-Musikanwendung und bringen sie von langweilig in schön. Dazu werden in diesem Codelab Tools und APIs verwendet, die in Material 3 vorgestellt wurden.

Lerninhalte

  • Hier erfahren Sie, wie Sie eine Flutter-App programmieren, die plattformübergreifend nutzerfreundlich und ansprechend ist.
  • Wie Sie Text in Ihrer App gestalten, um die User Experience zu verbessern
  • Hier erfahren Sie, wie Sie die richtigen Farben auswählen, Widgets anpassen, ein eigenes Design erstellen und den dunklen Modus schnell und einfach implementieren.
  • Hier erfahren Sie, wie Sie plattformübergreifende adaptive Apps erstellen.
  • So erstellst du Apps, die auf jedem Bildschirm gut aussehen
  • Hier erfährst du, wie du deiner Flutter-App Bewegung hinzufügen kannst, damit sie richtig gut zur Geltung kommt.

Voraussetzungen:

In diesem Codelab wird davon ausgegangen, dass Sie bereits mit Flutter vertraut sind. Falls nicht, sollten Sie sich zunächst mit den Grundlagen vertraut machen. Die folgenden Links sind hilfreich:

Aufgaben

In diesem Codelab erfährst du, wie du den Startbildschirm einer App namens MyArtist erstellst. Mit dieser Musikplayer-App können sich Fans über ihre Lieblingskünstler auf dem Laufenden halten. Darin erfahren Sie, wie Sie Ihr App-Design ändern können, damit es auf allen Plattformen ansprechend aussieht.

Die folgenden Videos zeigen, wie die App nach Abschluss dieses Codelabs funktioniert:

Was möchten Sie in diesem Codelab lernen?

<ph type="x-smartling-placeholder"></ph> Ich kenne dieses Thema noch nicht und möchte einen guten Überblick erhalten. Ich weiß etwas über dieses Thema, möchte aber meine Kenntnisse auffrischen. Ich suche nach Beispielcode für mein Projekt. Ich suche nach einer Erklärung zu etwas Bestimmtem.

2. Flutter-Entwicklungsumgebung einrichten

Für dieses Lab benötigen Sie zwei Softwareprogramme: das Flutter SDK und einen Editor.

Sie können das Codelab auf jedem dieser Geräte ausführen:

  • Ein physisches Android- oder iOS, das mit Ihrem Computer verbunden ist und sich im Entwicklermodus befindet.
  • Den iOS-Simulator (erfordert die Installation von Xcode-Tools).
  • Android-Emulator (Einrichtung in Android Studio erforderlich)
  • Ein Browser (zur Fehlerbehebung wird Chrome benötigt)
  • Als Windows-, Linux- oder macOS-Desktopanwendung Die Entwicklung muss auf der Plattform erfolgen, auf der Sie die Bereitstellung planen. Wenn Sie also eine Windows-Desktop-App entwickeln möchten, müssen Sie die Entwicklung unter Windows ausführen, damit Sie auf die entsprechende Build-Kette zugreifen können. Es gibt betriebssystemspezifische Anforderungen, die unter docs.flutter.dev/desktop ausführlich beschrieben werden.

3. Codelab-Starter-App herunterladen

Von GitHub klonen

Führen Sie die folgenden Befehle aus, um dieses Codelab von GitHub zu klonen:

git clone https://github.com/flutter/codelabs.git
cd codelabs/boring_to_beautiful/step_01/

Um sicherzugehen, dass alles funktioniert, führen Sie die Flutter-Anwendung wie unten gezeigt als Desktopanwendung aus. Alternativ können Sie dieses Projekt in Ihrer IDE öffnen und die Anwendung mit den zugehörigen Tools ausführen.

a3c16fc17be25f6c.png App ausführen

Fertig! Der Startcode für den Startbildschirm von MyArtist sollte jetzt ausgeführt werden. Nun sollte der Startbildschirm von MyArtist angezeigt werden. Auf Desktop-Computern sieht das gut aus, auf Mobilgeräten ist es jedoch... Nicht so toll. Zum einen wird der Punkt nicht berücksichtigt. Keine Sorge, das klappt schon!

1e67c60667821082.png d1139cde225de452.png

Code ansehen

Sehen Sie sich als Nächstes den Code an.

Öffnen Sie lib/src/features/home/view/home_screen.dart. Dort finden Sie Folgendes:

lib/src/features/home/view/home_screen.dart

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({super.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,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}

Diese Datei importiert material.dart und implementiert ein zustandsorientiertes Widget mithilfe von zwei Klassen:

  • Durch die Anweisung import werden die Materialkomponenten verfügbar.
  • Die Klasse HomeScreen repräsentiert die gesamte angezeigte Seite.
  • Mit der Methode build() der _HomeScreenState-Klasse wird der Stamm des Widget-Baums erstellt. Dies wirkt sich darauf aus, wie alle Widgets in der Benutzeroberfläche erstellt werden.

4. Typografie nutzen

Text ist überall. Text ist ein hilfreiches Mittel, um mit Nutzenden zu kommunizieren. Soll Ihre App freundlich und unterhaltsam oder vielleicht vertrauenswürdig und professionell sein? Es gibt einen Grund, warum deine bevorzugte Banking-App Comic Sans nicht verwendet. Die Art und Weise, wie Text präsentiert wird, vermittelt den Nutzern einen ersten Eindruck von Ihrer App. Hier sind einige Möglichkeiten, wie Sie Text sinnvoller einsetzen können.

Bilder sagen mehr als Worte

Wo immer möglich, „anzeigen“ statt „tell“ zu schreiben. Die NavigationRail in der Starter-App hat beispielsweise Tabs für jede Hauptroute, die führenden Symbole sind jedoch identisch:

86c5f73b3aa5fd35.png

Dies ist nicht hilfreich, da die Nutzenden immer noch den Text der einzelnen Tabs lesen müssen. Fügen Sie zunächst visuelle Hinweise hinzu, damit Nutzende schnell einen Blick auf die führenden Symbole werfen können, um den gewünschten Tab zu finden. Das verbessert auch die Lokalisierung und Zugänglichkeit.

a3c16fc17be25f6c.png Fügen Sie in lib/src/shared/router.dart für jedes Navigationsziel (Startseite, Playlist und Personen) eindeutige vorangestellte Symbole hinzu:

lib/src/shared/router.dart

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

Gibt es Probleme?

Wenn Ihre App nicht richtig ausgeführt wird, suchen Sie nach Tippfehlern. Verwenden Sie bei Bedarf den Code unter den folgenden Links, um wieder auf Kurs zu kommen.

Schriftarten mit Bedacht auswählen

Schriftarten bestimmen den Charakter Ihrer App, daher ist die Auswahl der richtigen Schriftart entscheidend. Beachten Sie bei der Auswahl einer Schriftart Folgendes:

  • Sans-Serif- oder Serifenschriften: Serifenschriften haben dekorative Striche oder "Zahl". am Ende von Buchstaben und werden als formeller empfunden. Sans Serif-Schriftarten haben keine dekorativen Striche und werden tendenziell als informeller wahrgenommen. 34bf54e4cad90101.png Ein serifenloses großes T und ein großes T mit Serifen
  • Großbuchstaben: Sie eignen sich, um die Aufmerksamkeit auf kleine Textmengen zu lenken, z. B. auf Überschriften. Übermäßige Verwendung kann jedoch als Schreien empfunden werden, die dazu führt, dass der Nutzer den Text vollständig ignoriert.
  • Erster Buchstabe im Satz groß: Achten Sie beim Hinzufügen von Titeln oder Labels darauf, wie Sie Großbuchstaben verwenden: Erster Buchstabe immer groß. Der erste Buchstabe jedes Worts wird groß geschrieben („This is a Title case Title“). Bei der Option Erster Buchstabe im Satz groß werden Eigennamen und das erste Wort im Text („Dies ist der Titel des Satzes“) großgeschrieben werden. Sie ist dialogorientierter und informeller.
  • Kerning (Abstand zwischen den einzelnen Buchstaben), Zeilenlänge (Breite des gesamten Textes auf dem Bildschirm) und Zeilenhöhe (wie hoch die einzelnen Textzeilen ist): Zu viele oder zu wenige Zeilen führen dazu, dass Ihre App weniger gut lesbar ist. So kann es zum Beispiel schnell passieren, dass Sie beim Lesen eines großen, ununterbrochenen Textblocks den Überblick verlieren.

Rufen Sie daher Google Fonts auf und wählen Sie eine serifenlose Schriftart wie Montserrat aus, da die Musik-App spielerisch und unterhaltsam sein soll.

a3c16fc17be25f6c.png Laden Sie über die Befehlszeile das google_fonts-Paket herunter. Dadurch wird auch die Datei pubspec aktualisiert und die Schriftarten als App-Abhängigkeit hinzugefügt.

$ flutter pub add google_fonts

macos/Runner/DebugProfile.entitlements

<?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>
        <true/>
        <!-- To here. -->
        <key>com.apple.security.network.server</key>
        <true/>
</dict>
</plist>

a3c16fc17be25f6c.png Importieren Sie das neue Paket in lib/src/shared/extensions.dart:

lib/src/shared/extensions.dart

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

a3c16fc17be25f6c.png Montserrat festlegen TextTheme:

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

a3c16fc17be25f6c.png Hot Refresh 7f9a9e103c7b5e5.png, um die Änderungen zu übernehmen. Verwenden Sie die Schaltfläche in Ihrer IDE oder geben Sie in der Befehlszeile r ein, um einen Hot Refresh auszuführen.

1e67c60667821082.png

Die neuen NavigationRail-Symbole sollten zusammen mit dem Text in der Schriftart Montserrat angezeigt werden.

Gibt es Probleme?

Wenn Ihre App nicht richtig ausgeführt wird, suchen Sie nach Tippfehlern. Verwenden Sie bei Bedarf den Code unter den folgenden Links, um wieder auf Kurs zu kommen.

5. Design festlegen

Designs tragen dazu bei, einer App ein strukturiertes Design und Einheitlichkeit zu verleihen, indem sie ein festgelegtes System aus Farben und Textstilen festlegen. Mit Designs können Sie schnell eine Benutzeroberfläche implementieren, ohne sich um kleinere Details wie das Festlegen der genauen Farbe für jedes einzelne Widget kümmern zu müssen.

Flutter-Entwickler erstellen benutzerdefinierte Komponenten in der Regel auf eine von zwei Arten:

  • Du kannst individuelle Widgets mit jeweils einem eigenen Design erstellen.
  • Auf einen Bereich reduzierte Designs für Standard-Widgets erstellen.

In diesem Beispiel wird ein Designanbieter aus lib/src/shared/providers/theme.dart verwendet, um Widgets und Farben in der gesamten App einheitlich zu gestalten:

lib/src/shared/providers/theme.dart

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(
     {super.key,
     required this.settings,
     required this.lightDynamic,
     required this.darkDynamic,
     required super.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.surfaceContainerHighest,
     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.pngWenn Sie den Anbieter verwenden möchten, erstellen Sie eine Instanz und übergeben Sie sie an das Bereichsobjekt in MaterialApp (lib/src/shared/app.dart). Sie wird von allen verschachtelten Theme-Objekten übernommen:

lib/src/shared/app.dart

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

Nachdem Sie das Design eingerichtet haben, wählen Sie die Farben für die Anwendung aus.

Die Auswahl der richtigen Farben ist nicht immer einfach. Vielleicht haben Sie schon eine Vorstellung von der Hauptfarbe, aber die Chancen stehen gut, dass Ihre App mehr als nur eine Farbe haben soll. Welche Farbe soll der Text haben? Titel? Inhalt? Links? Was ist mit der Hintergrundfarbe? Der Material Theme Builder ist ein webbasiertes Tool (in Material 3 eingeführt), mit dem Sie Komplementärfarben für Ihre App auswählen können.

a3c16fc17be25f6c.pngUm eine Quellfarbe für die Anwendung auszuwählen, öffnen Sie den Material Theme Builder und sehen Sie sich die verschiedenen Farben für die Benutzeroberfläche an. Es ist wichtig, eine Farbe auszuwählen, die zur Markenästhetik und/oder zu Ihren persönlichen Vorlieben passt.

Nachdem Sie ein Design erstellt haben, klicken Sie mit der rechten Maustaste auf das Infofeld Primäre Farbe. Daraufhin wird ein Dialogfeld mit dem Hexadezimalwert der Primärfarbe geöffnet. Kopieren Sie diesen Wert. Sie können in diesem Dialogfeld auch die Farbe festlegen.

a3c16fc17be25f6c.pngÜbergeben Sie den Hexadezimalwert der Primärfarbe an den Designanbieter. Der Hex-Farbcode #00cbe6 wird beispielsweise als Color(0xff00cbe6) angegeben. ThemeProvider generiert eine ThemeData, die den Satz Komplementärfarben enthält, die Sie in Material Theme Builder als Vorschau angesehen haben:

final settings = ValueNotifier(ThemeSettings(
   sourceColor:  Color(0xff00cbe6), // Replace this color
   themeMode: ThemeMode.system,
 ));

Starten Sie die App noch einmal neu. Sobald die Hauptfarbe vorhanden ist, wirkt die App ausdrucksstärker. Greifen Sie auf alle neuen Farben zu, indem Sie auf das Thema im Kontext verweisen und das ColorScheme auswählen:

final colors = Theme.of(context).colorScheme;

a3c16fc17be25f6c.pngWenn Sie eine bestimmte Farbe verwenden möchten, rufen Sie eine Farbrolle für das colorScheme auf. Gehen Sie zu lib/src/shared/views/outlined_card.dart und weisen Sie OutlinedCard einen Rahmen zu:

lib/src/shared/views/outlined_card.dart

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 bietet nuancierte Farbrollen, die sich ergänzen und über die gesamte Benutzeroberfläche zum Hinzufügen neuer Ausdrucksebenen verwendet werden können. Zu diesen neuen Farbrollen gehören:

  • 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

Außerdem unterstützen neue Designtokens sowohl das helle als auch das dunkle Design:

7b51703ed96196a4.png

Diese Farbrollen können verwendet werden, um verschiedenen Teilen der Benutzeroberfläche Bedeutung und Betonung zuzuweisen. Auch wenn eine Komponente nicht auffällig ist, können dynamische Farben genutzt werden.

a3c16fc17be25f6c.png Der Nutzer kann die App-Helligkeit in den Systemeinstellungen des Geräts festlegen. Wenn sich das Gerät in lib/src/shared/app.dart im dunklen Modus befindet, werden auch das dunkle Design und der Designmodus auf MaterialApp zurückgesetzt.

lib/src/shared/app.dart

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

Klicken Sie rechts oben auf das Mondsymbol, um den dunklen Modus zu aktivieren.

Gibt es Probleme?

Falls Ihre App nicht korrekt ausgeführt wird, nutzen Sie den Code unter dem folgenden Link, um das Problem zu beheben.

6. Adaptives Design hinzufügen

Mit Flutter können Sie Apps erstellen, die fast überall ausgeführt werden können. Das bedeutet aber nicht, dass alle Apps überall gleich verhalten müssen. Nutzer erwarten mittlerweile unterschiedliche Verhaltensweisen und Funktionen von verschiedenen Plattformen.

Material bietet Pakete, die die Arbeit mit adaptiven Layouts erleichtern. Sie finden diese Flutter-Pakete auf GitHub.

Beachten Sie beim Erstellen einer plattformübergreifenden, adaptiven Anwendung die folgenden Plattformunterschiede:

  • Eingabemethode: Maus, Touch oder Gamepad
  • Schriftgröße, Geräteausrichtung und Betrachtungsabstand
  • Bildschirmgröße und Formfaktor: Smartphone, Tablet, faltbar, Computer, Web

a3c16fc17be25f6c.png Die Datei lib/src/shared/views/adaptive_navigation.dart enthält eine Navigationsklasse, in der Sie eine Liste von Zielen und Inhalten zum Rendern des Texts angeben können. Da Sie dieses Layout auf mehreren Bildschirmen verwenden, gibt es ein gemeinsames Basislayout, das an jedes untergeordnete Element übergeben wird. Navigationsstreifen eignen sich gut für Desktop- und große Bildschirme. Sie gestalten das Layout jedoch für Mobilgeräte, da auf Mobilgeräten stattdessen eine untere Navigationsleiste angezeigt wird.

lib/src/shared/views/adaptive_navigation.dart

import 'package:flutter/material.dart';

class AdaptiveNavigation extends StatelessWidget {
  const AdaptiveNavigation({
    super.key,
    required this.destinations,
    required this.selectedIndex,
    required this.onDestinationSelected,
    required super.child,
  });

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

a8487a3c4d7890c9.png

Nicht alle Bildschirme haben die gleiche Größe. Wenn Sie versuchen würden, die Desktop-Version Ihrer App auf Ihrem Telefon anzuzeigen, müssen Sie die Augen zusammenkneifen und zoomen, um alles zu sehen. Sie möchten, dass Ihre App das Aussehen an den Bildschirm ändert, auf dem sie angezeigt wird. Mit responsivem Webdesign sorgst du dafür, dass deine App auf Bildschirmen jeder Größe gut aussieht.

Damit Ihre App responsiv wird, fügen Sie einige adaptive Haltepunkte ein (nicht zu verwechseln mit Haltepunkten zur Fehlerbehebung). Diese Haltepunkte geben die Bildschirmgrößen an, bei denen das Layout Ihrer App geändert werden soll.

Auf kleineren Bildschirmen wird nicht so viel angezeigt wie auf einem größeren Bildschirm, ohne dass der Inhalt verkleinert wird. Damit die App nicht wie eine verkürzte Desktop-App aussieht, erstellen Sie ein separates Layout für Mobilgeräte, bei dem die Inhalte mithilfe von Tabs unterteilt werden. Dadurch wirkt die App auf Mobilgeräten besser nativ.

Die folgenden Erweiterungsmethoden (im MyArtist-Projekt in lib/src/shared/extensions.dart definiert) sind ein guter Ausgangspunkt, wenn du optimierte Layouts für verschiedene Ziele entwickelst.

lib/src/shared/extensions.dart

extension BreakpointUtils on BoxConstraints {
  bool get isTablet => maxWidth > 730;
  bool get isDesktop => maxWidth > 1200;
  bool get isMobile => !isTablet && !isDesktop;
}

Ein Bildschirm, der in der längsten Richtung mehr als 730 Pixel, aber kleiner als 1.200 Pixel ist, wird als Tablet betrachtet. Alles, was größer als 1.200 Pixel ist, wird als Desktopversion betrachtet. Wenn ein Gerät weder ein Tablet noch ein Computer ist, wird es als Mobilgerät betrachtet. Weitere Informationen zu adaptiven Haltepunkten finden Sie unter material.io. Sie können das Paket adaptive_breakpoints verwenden.

Das responsive Layout des Startbildschirms verwendet AdaptiveContainer und AdaptiveColumn basierend auf dem 12-Spalten-Raster mithilfe der Pakete adaptive_components und adaptive_breakpoints, um ein responsives Rasterlayout in Material Design zu implementieren.

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.pngEin adaptives Layout benötigt zwei Layouts: eines für Mobilgeräte und ein responsives Layout für größere Bildschirme. LayoutBuilder gibt derzeit nur ein Desktop-Layout zurück. Erstellen Sie in lib/src/features/home/view/home_screen.dart das Layout für Mobilgeräte als TabBar und TabBarView mit 4 Tabs.

lib/src/features/home/view/home_screen.dart

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({super.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,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
     },
   );
 }
}

377cfdda63a9de54.png

Gibt es Probleme?

Falls Ihre App nicht korrekt ausgeführt wird, nutzen Sie den Code unter dem folgenden Link, um das Problem zu beheben.

Leerzeichen verwenden

Leerräume sind ein wichtiges visuelles Tool für Ihre App, das eine organisatorische Unterbrechung zwischen den Abschnitten schafft.

Es ist besser, zu viel Leerraum zu lassen als zu wenig. Es ist besser, mehr Leerraum hinzuzufügen, als die Größe der Schriftart oder der visuellen Elemente zu verringern, um mehr Platz zu schaffen.

Für Menschen mit Sehbeeinträchtigungen kann mangelnder Leerraum eine echte Herausforderung sein. Zu viel Leerraum kann einen Mangel an Kohärenz aufweisen und Ihre Benutzeroberfläche wirkt unübersichtlich. Sehen Sie sich beispielsweise die folgenden Screenshots an:

7f5e3514a7ee1750.png

d5144a50f5b4142c.png

Als Nächstes fügen Sie dem Startbildschirm Leerzeichen hinzu, um ihm mehr Platz zu geben. Anschließend werden Sie das Layout weiter optimieren, um die Abstände zu verfeinern.

a3c16fc17be25f6c.png Umschließt ein Widget mit einem Padding-Objekt, um ein Leerzeichen um das Widget hinzuzufügen. Erhöhen Sie die Padding-Werte, die sich derzeit in lib/src/features/home/view/home_screen.dart befinden, auf 35:

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 Aktualisieren Sie die App im Hot. Es sollte wie zuvor aussehen, allerdings mit mehr Leerraum zwischen den Widgets. Der zusätzliche Abstand sieht besser aus, aber das Markierungsbanner oben ist immer noch zu nah an den Rändern.

a3c16fc17be25f6c.png Ändern Sie in lib/src/features/home/view/home_highlight.dart den Abstand des Banners auf 35:

lib/src/features/home/view/home_highlight.dart

class HomeHighlight extends StatelessWidget {
  const HomeHighlight({super.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 Aktualisieren Sie die App im Hot. Die beiden Playlists unten haben kein Leerzeichen dazwischen und scheinen daher zur selben Tabelle zu gehören. Das ist nicht der Fall und Sie werden das Problem als Nächstes beheben.

df1d9af97d039cc8.png

a3c16fc17be25f6c.png Füge Leerzeichen zwischen den Playlists ein, indem du ein Größen-Widget in das Row-Element einfügst, das die Playlists enthält. Fügen Sie in lib/src/features/home/view/home_screen.dart einen SizedBox mit einer Breite von 35 hinzu:

lib/src/features/home/view/home_screen.dart

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 Aktualisieren Sie die App im Hot. Die App sollte so aussehen:

d8b2a3d47736dbab.png

Jetzt gibt es viel Platz für den Inhalt des Startbildschirms, aber die Bereiche sehen zu getrennt aus und es gibt keine Verbindung zwischen den einzelnen Bereichen.

a3c16fc17be25f6c.png Bisher haben Sie den gesamten Innenabstand (horizontal und vertikal) für die Widgets auf dem Startbildschirm mit EdgeInsets.all(35) auf 35 festgelegt. Sie können den Abstand für die einzelnen Ränder aber auch separat festlegen. Passen Sie den Abstand an, damit er sich besser in den Raum einfügt.

  • EdgeInsets.LTRB() legt links, oben, rechts und unten einzeln fest
  • EdgeInsets.symmetric() legt den Abstand für vertikal (oben und unten) als gleichwertig und für horizontal (links und rechts) entsprechend fest.
  • Mit EdgeInsets.only() werden nur die angegebenen Kanten festgelegt.
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 Legen Sie in lib/src/features/home/view/home_highlight.dart für den linken und rechten Abstand des Banners 35 sowie für den Abstand oben und unten auf 5 fest:

lib/src/features/home/view/home_highlight.dart

class HomeHighlight extends StatelessWidget {
  const HomeHighlight({super.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 Aktualisieren Sie die App im Hot. Das Layout und die Abstände sehen viel besser aus! Fügen Sie als letzten Schliff Bewegung und Animation hinzu.

7f5e3514a7ee1750.png

Gibt es Probleme?

Falls Ihre App nicht korrekt ausgeführt wird, nutzen Sie den Code unter dem folgenden Link, um das Problem zu beheben.

7. Bewegung und Animation hinzufügen

Bewegung und Animation sind großartige Möglichkeiten, um Bewegung und Energie einzuführen und Feedback zu geben, wenn Nutzende mit der App interagieren.

Zwischen Bildschirmen animieren

Die ThemeProvider definiert eine PageTransitionsTheme mit Bildschirmübergangsanimationen für mobile Plattformen (iOS, Android). Desktop-Nutzer erhalten bereits Feedback durch Maus- oder Touchpad-Klicks, sodass keine Animation beim Seitenübergang erforderlich ist.

Flutter bietet die Bildschirmübergangsanimationen, die Sie für Ihre App basierend auf der Zielplattform konfigurieren können, wie in lib/src/shared/providers/theme.dart dargestellt:

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 Übergeben Sie PageTransitionsTheme sowohl an das helle als auch das dunkle Design in lib/src/shared/providers/theme.dart.

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

Ohne Animationen unter iOS

Mit Animation auf iOS-Geräten

Gibt es Probleme?

Falls Ihre App nicht korrekt ausgeführt wird, nutzen Sie den Code unter dem folgenden Link, um das Problem zu beheben.

Hover-Zustände hinzufügen

Eine Möglichkeit, eine Desktop-App mit Bewegung zu versehen, sind die Hover-Status. Dabei ändert sich der Status eines Widgets (z. B. Farbe, Form oder Inhalt), wenn der Nutzer den Mauszeiger darauf bewegt.

Standardmäßig gibt die Klasse _OutlinedCardState, die für die Playlist-Kacheln mit der Bezeichnung „Zuletzt abgespielt“ verwendet wird, ein MouseRegion zurück. Dadurch wird der Cursor-Pfeil in einen Zeiger, wenn der Mauszeiger darauf bewegt wird. Du kannst aber auch zusätzliches visuelles Feedback hinzufügen.

a3c16fc17be25f6c.png Öffnen Sie lib/src/shared/views/outlined_card.dart und ersetzen Sie den Inhalt durch die folgende Implementierung, um den Status _hovered einzuführen.

lib/src/shared/views/outlined_card.dart

import 'package:flutter/material.dart';

class OutlinedCard extends StatefulWidget {
  const OutlinedCard({
    super.key,
    required this.child,
    this.clickable = true,
  });
  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 Lade die App neu und bewege dann den Mauszeiger auf eine der zuletzt abgespielten Playlist-Kacheln.

OutlinedCard ändert die Deckkraft und rundet die Ecken ab.

a3c16fc17be25f6c.png Animieren Sie zum Schluss mit dem in lib/src/shared/views/hoverable_song_play_button.dart definierten HoverableSongPlayButton-Widget die Titelnummer in einer Playlist zu einer Wiedergabeschaltfläche. Fügen Sie in lib/src/features/playlists/view/playlist_songs.dart das Center-Widget, das die Nummer des Titels enthält, mit HoverableSongPlayButton ein:

lib/src/features/playlists/view/playlist_songs.dart

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.pngLade die App neu und bewege dann den Mauszeiger in der Playlist Top-Titel heute oder in der Playlist Neuerscheinungen über die Nummer des Titels.

Die Nummer wird zu einer Wiedergabe-Schaltfläche animiert. Wenn Sie darauf klicken, wird der Song abgespielt.

Den endgültigen Projektcode finden Sie auf GitHub.

8. Glückwunsch!

Du hast dieses Codelab abgeschlossen. Sie haben gelernt, dass es viele kleine Änderungen gibt, die Sie in eine App integrieren können, um sie ansprechender, zugänglicher, besser lokalisieren und besser für verschiedene Plattformen zu gestalten. Zu diesen Techniken gehören unter anderem:

  • Typografie: Text ist mehr als nur ein Kommunikationstool. Verwenden Sie die Art der Textdarstellung, um eine positive Wirkung auf die Nutzenden zu haben. und Wahrnehmung Ihrer App.
  • Thematik: Richten Sie ein Designsystem ein, das Sie zuverlässig verwenden können, ohne für jedes Widget Designentscheidungen treffen zu müssen.
  • Anpassungsfähigkeit: Berücksichtigen Sie das Gerät und die Plattform, auf denen der Nutzer Ihre App ausführt, sowie deren Funktionen. Berücksichtigen Sie die Bildschirmgröße und die Darstellung Ihrer App.
  • Bewegung und Animation: Das Hinzufügen von Bewegung zu Ihrer App verleiht der User Experience mehr Energie und bietet den Nutzenden in praktischer Weise Feedback.

Mit ein paar kleinen Anpassungen kann Ihre App von langweilig zu schön werden:

Vorher

1e67c60667821082.png

Nachher

Nächste Schritte

Wir hoffen, du hast mehr über das Erstellen ansprechender Apps in Flutter erfahren.

Falls Sie die hier genannten Tipps oder Tricks anwenden oder eigene Tipp haben, würden wir uns freuen, von Ihnen zu hören! Kontaktiere uns auf Twitter unter @rodydavis und @khanhnwin.

Vielleicht finden Sie auch die folgenden Ressourcen hilfreich.

Designs

Adaptive und reaktionsschnelle Ressourcen:

Allgemeine Designressourcen:

Vernetzen Sie sich außerdem mit der Flutter-Community.

Mach weiter und gestalte die App-Welt schön!