Mache deine Flutter-App langweilig zu schön

Ihre Flutter-App von langweilig zu schön

Informationen zu diesem Codelab

subjectZuletzt aktualisiert: Juni 24, 2025
account_circleVerfasst von The Flutter Team

1. Einführung

Flutter ist das UI-Toolkit von Google, mit dem ansprechende, nativ kompilierte Anwendungen für Mobilgeräte, Web und Computer auf einer gemeinsamen Codebasis erstellt werden können. Flutter funktioniert mit 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, damit sie nicht mehr langweilig aussieht. Dazu werden in diesem Codelab Tools und APIs verwendet, die in Material 3 vorgestellt wurden.

Lerninhalte

  • So erstellen Sie eine Flutter-App, die plattformübergreifend nutzbar und ansprechend ist.
  • Wie Sie Text in Ihrer App so gestalten, dass er die Nutzerfreundlichkeit erhöht.
  • Sie erfahren, wie Sie die richtigen Farben auswählen, Widgets anpassen, Ihr eigenes Design erstellen und schnell den dunklen Modus implementieren.
  • Informationen zum Erstellen plattformübergreifender adaptiver Apps.
  • Apps entwickeln, die auf jedem Display gut aussehen
  • So verleihen Sie Ihrer Flutter-App Bewegung und machen sie so richtig lebendig.

Vorbereitung

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

Aufgaben

In diesem Codelab erfahren Sie, wie Sie den Startbildschirm für eine Anwendung namens MyArtist erstellen, einer Musikplayer-App, mit der sich Fans über ihre Lieblingskünstler auf dem Laufenden halten können. Darin wird erläutert, wie Sie Ihr App-Design so anpassen können, dass es auf allen Plattformen gut aussieht.

In den folgenden Videos wird gezeigt, wie die App am Ende dieses Codelabs funktioniert:

Was möchten Sie in diesem Codelab lernen?

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 einem der folgenden Geräte ausführen:

  • Ein physisches Android- oder iOS-Gerät, das mit Ihrem Computer verbunden und auf den Entwicklermodus gesetzt ist.
  • Der iOS-Simulator (erfordert die Installation der Xcode-Tools).
  • Der Android-Emulator (erfordert die Einrichtung in Android Studio).
  • Einen Browser (für die Fehlerbehebung ist Chrome erforderlich)
  • Als Windows-, Linux- oder macOS-Desktopanwendung Sie müssen die Entwicklung auf der Plattform durchführen, auf der Sie die Bereitstellung planen. Wenn Sie also eine Windows-Desktopanwendung entwickeln möchten, müssen Sie die Entwicklung unter Windows durchführen, um auf die entsprechende Build-Kette zugreifen zu 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/

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

flutter run

Fertig! Der Auslösercode für den Startbildschirm von MyArtist sollte ausgeführt werden. Der Startbildschirm von MyArtist sollte angezeigt werden. Auf dem Computer sieht es gut aus, aber auf Mobilgeräten… Nicht so gut. Zum einen wird die Notch nicht berücksichtigt. Keine Sorge, Sie werden das Problem beheben.

1e67c60667821082.pngd1139cde225de452.png

Code ansehen

Sehen wir uns als Nächstes den Code an.

Öffnen Sie lib/src/features/home/view/home_screen.dart. Diese Datei enthält Folgendes:

lib/src/features/home/view/home_screen.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 '../../../utils/adaptive_components.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) {
       
return Scaffold(
         
body: SingleChildScrollView(
           
child: AdaptiveColumn(
             
children: [
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Padding(
                   
padding: const EdgeInsets.all(2),
                   
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),
                       
child: Text(
                         
'Recently played',
                         
style: context.headlineSmall,
                       
),
                     
),
                     
HomeRecent(playlists: playlists),
                   
],
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Padding(
                   
padding: const EdgeInsets.all(2),
                   
child: Row(
                     
crossAxisAlignment: CrossAxisAlignment.start,
                     
children: [
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
child: Text(
                                 
'Top Songs Today',
                                 
style: context.titleLarge,
                               
),
                             
),
                             
LayoutBuilder(
                               
builder: (context, constraints) =>
                                   
PlaylistSongs(
                                     
playlist: topSongs,
                                     
constraints: constraints,
                                   
),
                             
),
                           
],
                         
),
                       
),
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
child: Text(
                                 
'New Releases',
                                 
style: context.titleLarge,
                               
),
                             
),
                             
LayoutBuilder(
                               
builder: (context, constraints) =>
                                   
PlaylistSongs(
                                     
playlist: newReleases,
                                     
constraints: constraints,
                                   
),
                             
),
                           
],
                         
),
                       
),
                     
],
                   
),
                 
),
               
),
             
],
           
),
         
),
       
);
     
},
   
);
 
}
}

In dieser Datei wird material.dart importiert und ein zustandsabhängiges Widget mit zwei Klassen implementiert:

  • Mit der import-Anweisung werden die Material Components verfügbar gemacht.
  • Die HomeScreen-Klasse steht für die gesamte angezeigte Seite.
  • Mit der Methode build() der Klasse _HomeScreenState wird der Stamm des Widget-Baums erstellt, was sich auf die Erstellung aller Widgets in der Benutzeroberfläche auswirkt.

4. Vorteile der Typografie nutzen

Text ist überall. Text ist eine nützliche Möglichkeit, mit Nutzern zu kommunizieren. Soll Ihre App freundlich und unterhaltsam oder vielleicht vertrauenswürdig und professionell wirken? Es gibt einen Grund, warum Ihre bevorzugte Banking-App nicht Comic Sans verwendet. Die Art und Weise, wie Text präsentiert wird, prägt den ersten Eindruck, den Nutzer von Ihrer App erhalten. Im Folgenden finden Sie einige Möglichkeiten, Text sinnvoller zu verwenden.

Zeigen, nicht erzählen

Zeigen Sie nach Möglichkeit, anstatt zu erzählen. Die NavigationRail in der Start-App hat beispielsweise Tabs für jede Hauptroute, die vorangestellten Symbole sind jedoch identisch:

86c5f73b3aa5fd35.png

Das ist nicht hilfreich, da der Nutzer trotzdem den Text auf jedem Tab lesen muss. Fügen Sie zuerst visuelle Hinweise hinzu, damit Nutzer schnell einen Blick auf die wichtigsten Symbole werfen können, um den gewünschten Tab zu finden. Das hilft auch bei der Lokalisierung und Barrierefreiheit.

Füge in lib/src/shared/router.dart unterschiedliche Symbole für die einzelnen Navigationsziele (Startseite, Playlist und Personen) 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

Probleme?

Wenn Ihre App nicht richtig funktioniert, suchen Sie nach Tippfehlern. Wenn nötig, kannst du den Code unter den folgenden Links verwenden, um wieder auf den richtigen Weg zu kommen.

Schriftarten mit Bedacht auswählen

Schriftarten prägen die Persönlichkeit Ihrer Anwendung. Daher ist die Auswahl der richtigen Schriftart entscheidend. Bei der Auswahl einer Schriftart sollten Sie Folgendes beachten:

  • Serifenlos oder Serifen: Serifenschriften haben dekorative Striche oder „Schwänze“ am Ende der Buchstaben und werden als formeller wahrgenommen. Serifenlose Schriftarten haben keine dekorativen Striche und werden in der Regel als weniger formell wahrgenommen. Ein großes „T“ in einer serifenlosen und einer Serifenschrift
  • Schriftarten in Großbuchstaben: Die Verwendung von Großbuchstaben eignet sich, um auf kleine Textmengen aufmerksam zu machen (z. B. in Überschriften). Bei übermäßigem Einsatz kann es jedoch so wirken, als würden Sie schreien, was dazu führt, dass Nutzer den Text ignorieren.
  • Erster Buchstabe groß oder Erster Buchstabe im Satz groß: Überlegen Sie sich, wie Sie Großbuchstaben verwenden, wenn Sie Titel oder Labels hinzufügen: Die Großschreibung, bei der der erste Buchstabe jedes Wortes großgeschrieben wird („Dieser Titel ist in Großbuchstaben geschrieben“), wirkt formeller. Bei der Regulären Groß- und Kleinschreibung werden nur Eigennamen und das erste Wort im Text großgeschrieben („Das ist ein Titel mit regulärer Groß- und Kleinschreibung“). Diese Schreibweise ist eher ungezwungen und informell.
  • Kerning (Abstand zwischen den einzelnen Buchstaben), Zeilenlänge (Breite des gesamten Texts auf dem Bildschirm) und Zeilenhöhe (Höhe der einzelnen Textzeilen): Zu viel oder zu wenig davon macht Ihre App weniger lesbar. Es kann beispielsweise schwierig sein, beim Lesen eines großen, zusammenhängenden Textblocks den Überblick zu behalten.

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

Rufen Sie das google_fonts-Paket über die Befehlszeile ab. Dadurch wird auch die Datei pubspec.yaml aktualisiert, um die Schriftarten als App-Abhängigkeit hinzuzufügen.

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" "https://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/>
        <key>com.apple.security.network.server</key>
        <true/>
        <!-- Make sure the following two lines are present -->
        <key>com.apple.security.network.client</key>
        <true/>
</dict>
</plist>

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.

Montserrat-TextTheme: festlegen

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

Führen Sie einen Hot Reload von 7f9a9e103c7b5e5.png aus, um die Änderungen zu aktivieren. Verwenden Sie dazu die Schaltfläche in Ihrer IDE oder geben Sie in der Befehlszeile r ein.

1e67c60667821082.png

Die neuen NavigationRail-Symbole und der Text sollten in der Schriftart Montserrat angezeigt werden.

Probleme?

Wenn Ihre App nicht richtig funktioniert, suchen Sie nach Tippfehlern. Wenn nötig, kannst du den Code unter den folgenden Links verwenden, um wieder auf den richtigen Weg zu kommen.

5. Design festlegen

Mithilfe von Themen können Sie einer App ein strukturiertes Design und Einheitlichkeit verleihen, indem Sie ein festgelegtes System von Farben und Textstilen festlegen. Mithilfe von Themen können Sie schnell eine Benutzeroberfläche implementieren, ohne sich um Kleinigkeiten wie die genaue Farbe für jedes einzelne Widget kümmern zu müssen.

Flutter-Entwickler erstellen benutzerdefinierte Komponenten mit einem benutzerdefinierten Design in der Regel auf eine von zwei Arten:

  • Erstellen Sie individuelle benutzerdefinierte Widgets mit jeweils eigenem Design.
  • Erstellen Sie Designs für Standard-Widgets.

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

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.toARGB32(),
       
settings.value.sourceColor.toARGB32(),
     
),
   
);
 
}

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

 
CardThemeData cardTheme() {
   
return CardThemeData(
     
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,
   
);
 
}

 
TabBarThemeData tabBarTheme(ColorScheme colors) {
   
return TabBarThemeData(
     
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 colorScheme = colors(Brightness.light, targetColor);
   
return ThemeData.light().copyWith(
     
colorScheme: colorScheme,
     
appBarTheme: appBarTheme(colorScheme),
     
cardTheme: cardTheme(),
     
listTileTheme: listTileTheme(colorScheme),
     
bottomAppBarTheme: bottomAppBarTheme(colorScheme),
     
bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
     
navigationRailTheme: navigationRailTheme(colorScheme),
     
tabBarTheme: tabBarTheme(colorScheme),
     
drawerTheme: drawerTheme(colorScheme),
     
scaffoldBackgroundColor: colorScheme.surface,
   
);
 
}

 
ThemeData dark([Color? targetColor]) {
   
final colorScheme = colors(Brightness.dark, targetColor);
   
return ThemeData.dark().copyWith(
     
colorScheme: colorScheme,
     
appBarTheme: appBarTheme(colorScheme),
     
cardTheme: cardTheme(),
     
listTileTheme: listTileTheme(colorScheme),
     
bottomAppBarTheme: bottomAppBarTheme(colorScheme),
     
bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
     
navigationRailTheme: navigationRailTheme(colorScheme),
     
tabBarTheme: tabBarTheme(colorScheme),
     
drawerTheme: drawerTheme(colorScheme),
     
scaffoldBackgroundColor: colorScheme.surface,
   
);
 
}

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

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

Wenn Sie den Anbieter verwenden möchten, erstellen Sie eine Instanz und übergeben Sie sie an das thematische Objekt mit Bereichsbeschränkung in MaterialApp in 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 Farben für die Anwendung aus.

Die Auswahl der richtigen Farben kann schwierig sein. Sie haben vielleicht eine Vorstellung von der Hauptfarbe, aber wahrscheinlich möchten Sie, dass Ihre App mehr als nur eine Farbe hat. Welche Farbe sollte der Text haben? Titel? Inhalt? Links? Wie sieht es mit der Hintergrundfarbe aus? Der Material Theme Builder ist ein webbasiertes Tool, das in Material 3 eingeführt wurde und Ihnen hilft, eine Reihe von Komplementärfarben für Ihre App auszuwählen.

Wenn Sie eine Quellfarbe für die Anwendung auswählen möchten, öffnen Sie den Material Theme Builder und sehen Sie sich verschiedene Farben für die Benutzeroberfläche an. Es ist wichtig, eine Farbe auszuwählen, die zur Ästhetik der Marke oder zu Ihren persönlichen Vorlieben passt.

Nachdem Sie ein Design erstellt haben, klicken Sie mit der rechten Maustaste auf das Primär-Farbfeld. Daraufhin wird ein Dialogfeld mit dem Hexadezimalwert der Primärfarbe geöffnet. Kopieren Sie diesen Wert. Sie können die Farbe auch über dieses Dialogfeld festlegen.

Übergeben Sie den Hexadezimalwert der Primärfarbe an den Designanbieter. Die Hexadezimalfarbe #00cbe6 wird beispielsweise als Color(0xff00cbe6) angegeben. Die ThemeProvider generiert eine ThemeData mit den Komplementärfarben, die Sie in Material Theme Builder in der Vorschau angesehen haben:

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

Starten Sie die App neu. Mit der primären Farbe wirkt die App ausdrucksvoller. Wenn Sie auf alle neuen Farben zugreifen möchten, müssen Sie im Kontext auf das Design verweisen und die ColorScheme abrufen:

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

Wenn Sie eine bestimmte Farbe verwenden möchten, greifen Sie auf eine Farbrolle auf der colorScheme zu. Gehen Sie zu lib/src/shared/views/outlined_card.dart und fügen Sie dem OutlinedCard einen Rahmen hinzu:

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(
        // Add from here...
        decoration: BoxDecoration(
          border: Border.all(
            color: Theme.of(context).colorScheme.outline,
            width: 1,
          ),
        ),
        // ... To here.
        child: widget.child,
      ),
    );
  }
}

In Material 3 werden nuancierte Farbrollen eingeführt, die sich ergänzen und in der gesamten Benutzeroberfläche verwendet werden können, um neue Ausdrucksebenen hinzuzufügen. 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 Design-Tokens sowohl das helle als auch das dunkle Design:

7b51703ed96196a4.png

Mit diesen Farbrollen können Sie verschiedenen Teilen der Benutzeroberfläche Bedeutung und Betonung verleihen. Auch wenn eine Komponente nicht auffällig ist, kann sie von dynamischen Farben profitieren.

Der Nutzer kann die Helligkeit der App in den Systemeinstellungen des Geräts festlegen. Wenn das Gerät in lib/src/shared/app.dart auf den dunklen Modus eingestellt ist, muss das MaterialApp ein dunkles Design und einen Designmodus zurückgeben.

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.

Probleme?

Wenn Ihre App nicht richtig funktioniert, können Sie den Code unter dem folgenden Link verwenden, um das Problem zu beheben.

6. Adaptives Design hinzufügen

Mit Flutter können Sie Apps entwickeln, die fast überall ausgeführt werden können. Das bedeutet jedoch nicht, dass jede App überall gleich funktionieren muss. Nutzer erwarten von verschiedenen Plattformen unterschiedliche Verhaltensweisen und Funktionen.

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, Touchbedienung oder Gamepad
  • Schriftgröße, Geräteausrichtung und Betrachtungsabstand
  • Bildschirmgröße und Formfaktor: Smartphone, Tablet, faltbares Gerät, Computer, Web

Die Datei lib/src/shared/views/adaptive_navigation.dart enthält eine Navigationsklasse, in der Sie eine Liste von Zielen und Inhalten angeben können, um den Textkörper zu rendern. Da Sie dieses Layout auf mehreren Bildschirmen verwenden, gibt es ein gemeinsames Basislayout, das an jedes untergeordnete Element übergeben wird. Navigationsleisten eignen sich gut für Computer und große Bildschirme. Sie können das Layout jedoch für Mobilgeräte optimieren, indem Sie stattdessen eine Navigationsleiste unten anzeigen.

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 this.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) {
       
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 dieselbe Größe. Wenn Sie versucht hätten, die Desktopversion Ihrer App auf Ihrem Smartphone anzuzeigen, müssten Sie die Augen zusammenkneifen und heranzoomen, um alles zu sehen. Sie möchten, dass sich das Aussehen Ihrer App je nach Bildschirm ändert, auf dem sie angezeigt wird. Mit responsivem Design sorgen Sie dafür, dass Ihre App auf Bildschirmen jeder Größe gut zur Geltung kommt.

Um Ihre App responsiv zu gestalten, sollten Sie einige adaptive Breakpoints einfügen (nicht zu verwechseln mit Breakpoints für das Debuggen). Mit diesen Grenzwerten wird festgelegt, bei welchen Bildschirmgrößen das Layout Ihrer App geändert werden soll.

Auf kleineren Bildschirmen können nicht so viele Inhalte wie auf größeren Bildschirmen angezeigt werden, ohne dass die Inhalte verkleinert werden. Damit die App nicht wie eine verkleinerte Desktop-Anwendung aussieht, erstellen Sie ein separates Layout für Mobilgeräte, in dem die Inhalte mithilfe von Tabs unterteilt werden. Dadurch wirkt die App auf Mobilgeräten nativer.

Die folgenden Erweiterungsmethoden (definiert im MyArtist-Projekt in lib/src/shared/extensions.dart) sind ein guter Ausgangspunkt für die Gestaltung optimierter Layouts für verschiedene Ziele.

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 größer als 730 Pixel (in der längsten Richtung) und kleiner als 1.200 Pixel ist, gilt als Tablet. Alles, was größer als 1.200 Pixel ist, wird als Desktop-Computer betrachtet. Wenn es sich bei einem Gerät weder um ein Tablet noch um einen Computer handelt, wird es als Mobilgerät betrachtet. Weitere Informationen zu adaptiven Breakpoints finden Sie auf material.io.

Das responsive Layout des Startbildschirms verwendet AdaptiveContainer und AdaptiveColumn basierend auf dem 12-Spalten-Raster.

Für ein adaptives Layout sind zwei Layouts erforderlich: eines für Mobilgeräte und ein responsives Layout für größere Bildschirme. An dieser Stelle gibt das LayoutBuilder ein Desktop-Layout zurück. Erstellen Sie in lib/src/features/home/view/home_screen.dart das mobile Layout als TabBar und TabBarView mit 4 Tabs.

lib/src/features/home/view/home_screen.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 '../../../utils/adaptive_components.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.all(2),
                   
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),
                       
child: Text(
                         
'Recently played',
                         
style: context.headlineSmall,
                       
),
                     
),
                     
HomeRecent(playlists: playlists),
                   
],
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Padding(
                   
padding: const EdgeInsets.all(2),
                   
child: Row(
                     
crossAxisAlignment: CrossAxisAlignment.start,
                     
children: [
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
child: Text(
                                 
'Top Songs Today',
                                 
style: context.titleLarge,
                               
),
                             
),
                             
LayoutBuilder(
                               
builder: (context, constraints) =>
                                   
PlaylistSongs(
                                     
playlist: topSongs,
                                     
constraints: constraints,
                                   
),
                             
),
                           
],
                         
),
                       
),
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
child: Text(
                                 
'New Releases',
                                 
style: context.titleLarge,
                               
),
                             
),
                             
LayoutBuilder(
                               
builder: (context, constraints) =>
                                   
PlaylistSongs(
                                     
playlist: newReleases,
                                     
constraints: constraints,
                                   
),
                             
),
                           
],
                         
),
                       
),
                     
],
                   
),
                 
),
               
),
             
],
           
),
         
),
       
);
     
},
   
);
 
}
}

377cfdda63a9de54.png

Probleme?

Wenn Ihre App nicht richtig funktioniert, können Sie den Code unter dem folgenden Link verwenden, um das Problem zu beheben.

7. Verwenden Sie Leerzeichen.

Weißraum ist ein wichtiges visuelles Tool für Ihre App, da er eine organisatorische Pause zwischen den Abschnitten schafft.

Es ist besser, zu viel Weißraum zu haben als zu wenig. Es ist besser, mehr Weißraum hinzuzufügen, als die Schrift oder visuellen Elemente zu verkleinern, damit sie in den verfügbaren Bereich passen.

Ein Mangel an Weißraum kann für Menschen mit Sehproblemen eine Herausforderung sein. Zu viel Weißraum kann zu einem Mangel an Zusammenhalt führen und Ihre Benutzeroberfläche unübersichtlich wirken lassen. Beispiele:

7f5e3514a7ee1750.png

d5144a50f5b4142c.png

Als Nächstes fügen Sie dem Startbildschirm Weißraum hinzu, um mehr Platz zu schaffen. Anschließend passen Sie das Layout weiter an, um den Abstand zu optimieren.

Um ein Widget mit einem Padding-Objekt zu umschließen, um um das Widget herum Weißraum zu schaffen. Erhöhen Sie alle Werte für den Abstand in lib/src/features/home/view/home_screen.dart auf 35:

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

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

Führen Sie einen Hot-Reload der App durch. Sie sollte jetzt genauso aussehen wie zuvor, aber mit mehr Weißraum zwischen den Widgets. Das zusätzliche Abstandselement sieht besser aus, aber das Highlights-Banner oben ist immer noch zu nah an den Rändern.

Ändern Sie in lib/src/features/home/view/home_highlight.dart den Abstand des Banners in 15:

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(15),                   // 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: () => launchUrl(Uri.parse('https://docs.flutter.dev')),
            ),
          ),
        ),
      ],
    );
  }
}

Führen Sie einen Hot-Reload der App durch. Zwischen den beiden Playlists unten gibt es kein Leerzeichen, sodass es so aussieht, als würden sie zu derselben Tabelle gehören. Das ist nicht der Fall und Sie werden das als Nächstes beheben.

df1d9af97d039cc8.png

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

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

AdaptiveContainer(
  columnSpan: 12,
  child: 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,
                ),
              ),
              LayoutBuilder(
                builder: (context, constraints) =>
                    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,
                ),
              ),
              LayoutBuilder(
                builder: (context, constraints) =>
                    PlaylistSongs(
                      playlist: newReleases,
                      constraints: constraints,
                    ),
              ),
            ],
          ),
        ),
      ],
    ),
  ),
),

Führen Sie einen Hot-Reload der App durch. Die App sollte dann so aussehen:

d8b2a3d47736dbab.png

Jetzt gibt es viel Platz für die Inhalte des Startbildschirms, aber alles wirkt zu getrennt und die Abschnitte wirken nicht zusammenhängend.

Bisher haben Sie mit EdgeInsets.all(35) den gesamten Abstand (horizontal und vertikal) für die Widgets auf dem Startbildschirm auf 35 festgelegt. Sie können den Abstand aber auch für jede Kante unabhängig festlegen. Passen Sie den Abstand an den Bereich an.

  • Mit EdgeInsets.LTRB() werden links, oben, rechts und unten einzeln festgelegt.
  • Mit EdgeInsets.symmetric() wird der vertikale (oben und unten) und der horizontale (links und rechts) Abstand gleichmäßig festgelegt.
  • EdgeInsets.only() legt nur die angegebenen Kanten fest.

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

return 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(             // Modify from here...
                  horizontal: 15,
                  vertical: 10,
                ),                                               // To here.
                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(          // Modify from here...
                          left: 8,
                          bottom: 8,
                        ),                                       // To here.
                        child: Text(
                          'Top Songs Today',
                          style: context.titleLarge,
                        ),
                      ),
                      LayoutBuilder(
                        builder: (context, constraints) =>
                            PlaylistSongs(
                              playlist: topSongs,
                              constraints: constraints,
                            ),
                      ),
                    ],
                  ),
                ),
                const SizedBox(width: 25),                       // Modify this line
                Flexible(
                  flex: 10,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Padding(
                        padding: const EdgeInsets.only(          // Modify from here...
                          left: 8,
                          bottom: 8,
                        ),                                       // To here.
                        child: Text(
                          'New Releases',
                          style: context.titleLarge,
                        ),
                      ),
                      LayoutBuilder(
                        builder: (context, constraints) =>
                            PlaylistSongs(
                              playlist: newReleases,
                              constraints: constraints,
                            ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    ),
  ),
);

Legen Sie in lib/src/features/home/view/home_highlight.dart den linken und rechten Abstand des Banners auf 35 und den oberen und unteren Abstand 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 the following 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: () => launchUrl(Uri.parse('https://docs.flutter.dev')),
            ),
          ),
        ),
      ],
    );
  }
}

Führen Sie einen Hot Reload der App durch. Das Layout und die Abstände sehen jetzt viel besser aus. Zum Schluss noch etwas Bewegung und Animation hinzufügen.

7f5e3514a7ee1750.png

Probleme?

Wenn Ihre App nicht richtig funktioniert, können Sie den Code unter dem folgenden Link verwenden, um das Problem zu beheben.

8. Bewegung und Animation hinzufügen

Bewegung und Animation sind eine gute Möglichkeit, Bewegung und Energie zu vermitteln und Feedback zu geben, wenn Nutzer mit der App interagieren.

Zwischen Bildschirmen animieren

Mit dem ThemeProvider wird ein PageTransitionsTheme mit Bildschirmübergangsanimationen für mobile Plattformen (iOS, Android) definiert. Nutzer auf dem Computer erhalten bereits Feedback durch das Klicken mit der Maus oder dem Touchpad. Eine Seitenübergangsanimation ist daher nicht erforderlich.

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

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

Übergeben Sie die PageTransitionsTheme sowohl an das helle als auch an das dunkle Design in lib/src/shared/providers/theme.dart.

lib/src/shared/providers/theme.dart

ThemeData light([Color? targetColor]) {
  final colorScheme = colors(Brightness.light, targetColor);
  return ThemeData.light().copyWith(
    pageTransitionsTheme: pageTransitionsTheme,                     // Add this line
    colorScheme: colorScheme,
    appBarTheme: appBarTheme(colorScheme),
    cardTheme: cardTheme(),
    listTileTheme: listTileTheme(colorScheme),
    bottomAppBarTheme: bottomAppBarTheme(colorScheme),
    bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
    navigationRailTheme: navigationRailTheme(colorScheme),
    tabBarTheme: tabBarTheme(colorScheme),
    drawerTheme: drawerTheme(colorScheme),
    scaffoldBackgroundColor: colorScheme.surface,
  );
}

ThemeData dark([Color? targetColor]) {
  final colorScheme = colors(Brightness.dark, targetColor);
  return ThemeData.dark().copyWith(
    pageTransitionsTheme: pageTransitionsTheme,                     // Add this line
    colorScheme: colorScheme,
    appBarTheme: appBarTheme(colorScheme),
    cardTheme: cardTheme(),
    listTileTheme: listTileTheme(colorScheme),
    bottomAppBarTheme: bottomAppBarTheme(colorScheme),
    bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
    navigationRailTheme: navigationRailTheme(colorScheme),
    tabBarTheme: tabBarTheme(colorScheme),
    drawerTheme: drawerTheme(colorScheme),
    scaffoldBackgroundColor: colorScheme.surface,
  );
}

Ohne Animation auf iOS-Geräten

Mit Animation auf iOS-Geräten

Probleme?

Wenn Ihre App nicht richtig funktioniert, können Sie den Code unter dem folgenden Link verwenden, um das Problem zu beheben.

9. Mouseover-Einblendungen hinzufügen

Eine Möglichkeit, einer Desktop-Anwendung Bewegung zu verleihen, sind Mouseover-Zustände, bei denen ein Widget seinen Status (z. B. Farbe, Form oder Inhalt) ändert, wenn der Nutzer den Mauszeiger darauf bewegt.

Standardmäßig gibt die Klasse _OutlinedCardState (für die Playlist-Kacheln „Zuletzt abgespielt“) ein MouseRegion zurück, wodurch der Cursorpfeil beim Bewegen des Mauszeigers zu einem Zeiger wird. Du kannst aber auch mehr visuelles Feedback hinzufügen.

Öffnen Sie lib/src/shared/views/outlined_card.dart und ersetzen Sie den Inhalt durch die folgende Implementierung, um einen _hovered-Status 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.withAlpha(_hovered ? 30 : 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,
       
),
     
),
   
);
 
}
}

Führe einen Hot Reload der App durch und bewege den Mauszeiger auf eine der Kacheln der zuletzt abgespielten Playlists.

Mit dem OutlinedCard wird die Deckkraft geändert und die Ecken werden abgerundet.

Animieren Sie abschließend die Songnummer in einer Playlist mithilfe des in lib/src/shared/views/hoverable_song_play_button.dart definierten HoverableSongPlayButton-Widgets zu einer Wiedergabetaste. Umschließen Sie in lib/src/features/playlists/view/playlist_songs.dart das Center-Widget (das die Songnummer enthält) in einem HoverableSongPlayButton:

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

rowBuilder: (context, index) => DataRow.byIndex(
  index: index,
  cells: [
    DataCell(
      HoverableSongPlayButton(                                      // Modify from here...
        hoverMode: HoverMode.overlay,
        song: playlist.songs[index],
        child: Center(
          child: Text(
            (index + 1).toString(),
            textAlign: TextAlign.center,
          ),
        ),
      ),                                                            // To here.
    ),
    DataCell(
      Row(
        children: [
          Padding(
            padding: const EdgeInsets.all(2),
            child: ClippedImage(playlist.songs[index].image.image),
          ),
          const SizedBox(width: 10),
          Expanded(child: Text(playlist.songs[index].title)),
        ],
      ),
    ),
    DataCell(Text(playlist.songs[index].length.toHumanizedString())),
  ],
),

Lade die App neu und bewege den Mauszeiger auf die Songnummer in der Playlist Top-Songs des Tages oder Neuveröffentlichungen.

Die Zahl wird zu einer Wiedergabeschaltfläche, über die der Titel abgespielt wird, wenn du darauf klickst.

Den Code des finalen Projekts auf GitHub ansehen

10. Glückwunsch!

Sie haben dieses Codelab abgeschlossen. Sie haben gelernt, dass es viele kleine Änderungen gibt, die Sie in eine App einbinden können, um sie schöner, barrierefreier, leichter lokalisierbar und für mehrere Plattformen geeigneter zu machen. Dazu gehören unter anderem:

  • Typografie: Text ist mehr als nur ein Kommunikationsmittel. Die Art und Weise, wie Text angezeigt wird, sollte sich positiv auf die Nutzerfreundlichkeit und die Wahrnehmung Ihrer App auswirken.
  • Designthemen: Erstellen Sie ein Designsystem, das Sie zuverlässig verwenden können, ohne für jedes Widget Designentscheidungen treffen zu müssen.
  • Adaptivität: Berücksichtigen Sie das Gerät und die Plattform, auf denen der Nutzer Ihre App ausführt, und deren Funktionen. Berücksichtigen Sie die Bildschirmgröße und die Darstellung Ihrer App.
  • Bewegung und Animation: Wenn Sie Ihrer App Bewegung hinzufügen, wird die Nutzererfahrung lebendiger und Nutzer erhalten praktisches Feedback.

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

Vorher

1e67c60667821082.png

Nachher

Nächste Schritte

Wir hoffen, dass Sie mehr über die Entwicklung ansprechender Apps in Flutter erfahren haben.

Wenn du einen der hier genannten Tipps oder Tricks ausprobierst (oder einen eigenen Tipp hast, den du teilen möchtest), würden wir uns sehr über eine Nachricht von dir freuen. Du kannst uns auf Twitter unter @rodydavis und @khanhnwin erreichen.

Vielleicht finden Sie auch die folgenden Ressourcen nützlich:

Designs

Anpassbare und responsive Ressourcen:

Allgemeine Designressourcen:

Treten Sie auch der Flutter-Community bei.

Viel Spaß beim Entwerfen von Apps!