Informationen zu diesem Codelab
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:
- Weitere Informationen finden Sie unter Benutzeroberflächen mit Flutter erstellen.
- Codelab Meine erste Flutter-App ausprobieren
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.
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:
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',
),
];
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.
- 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 aus, um die Änderungen zu aktivieren. Verwenden Sie dazu die Schaltfläche in Ihrer IDE oder geben Sie in der Befehlszeile
r
ein.
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:
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.
},
);
}
}
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,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
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:
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.
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:
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.
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.
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
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
- Material Theme Builder (Tool)
Anpassbare und responsive Ressourcen:
- Decoding Flutter on Adaptive vs Responsive (Video)
- Adaptive Layouts (Video von The Boring Flutter Development Show)
- Responsive und adaptive Apps erstellen (flutter.dev)
- Adaptive Material Components for Flutter (Bibliothek auf GitHub)
- 5 Tipps zur Vorbereitung Ihrer App auf große Bildschirme (Video von der Google I/O 2021)
Allgemeine Designressourcen:
- The little things: Becoming the mythical designer-developer (Video von Flutter Engage)
- Material Design 3 für faltbare Geräte (material.io)
Treten Sie auch der Flutter-Community bei.
Viel Spaß beim Entwerfen von Apps!