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:
- Sehen Sie sich das Flutter-Widget-Framework an.
- Codelab Write Your First Flutter App, Teil 1 ausprobieren
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">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.
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!
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:
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.
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',
),
];
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. 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.
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>
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 festlegen TextTheme:
TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line
Hot Refresh , 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.
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);
}
}
Wenn 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.
Um 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.
Ü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;
Wenn 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:
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.
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
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.
},
);
}
}
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,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
Ein 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,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
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:
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.
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,
),
),
],
),
),
],
),
),
),
],
),
),
);
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.
Ä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'),
),
),
),
],
);
}
}
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.
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,
),
],
),
),
],
),
),
Aktualisieren Sie die App im Hot. Die App sollte so aussehen:
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.
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 festEdgeInsets.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,
),
),
],
),
),
],
),
),
),
],
),
),
);
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'),
),
),
),
],
);
}
}
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.
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(),
},
);
Ü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.
Ö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,
),
),
);
}
}
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.
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
Lade 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
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
- Material Theme Builder (Tool)
Adaptive und reaktionsschnelle Ressourcen:
- Video: Flutter bei adaptiven vs. responsiv decodieren
- Adaptive Layouts (Video von The Boring Flutter Development Show)
- Responsive und adaptive Apps erstellen (flutter.dev)
- Adaptive Material components for Flutter (Bibliothek auf GitHub)
- Fünf Dinge, die du tun kannst, um deine App für große Bildschirme vorzubereiten (Video von der Google I/O 2021)
Allgemeine Designressourcen:
- The small items: Becoming the mythical Designer-developer (Video von Flutter Engage)
- Material Design 3 für faltbare Geräte (material.io)
Vernetzen Sie sich außerdem mit der Flutter-Community.
Mach weiter und gestalte die App-Welt schön!