מידע על Codelab זה
1. מבוא
Flutter היא ערכת הכלים של Google לבניית ממשק משתמש, שמאפשרת ליצור אפליקציות יפות ומתורגמות באופן מקורי לנייד, לאינטרנט ולמחשבים ממקור קוד יחיד. Flutter פועלת עם קוד קיים, מפתחים וארגונים ברחבי העולם משתמשים בה, והיא קוד פתוח זמין בחינם.
בקודלאב הזה תלמדו איך לשפר אפליקציית מוזיקה ב-Flutter, ולהפוך אותה משעממת ליפיפית. כדי לעשות זאת, ב-codelab הזה נעשה שימוש בכלים ובממשקי API שהוצגו ב-Material 3.
מה תלמדו
- איך לכתוב אפליקציית Flutter שתהיה שימושית ומרהיבה בכל הפלטפורמות.
- איך מעצבים טקסט באפליקציה כדי לוודא שהוא תורם לחוויית המשתמש.
- איך בוחרים את הצבעים המתאימים, מתאימים אישית ווידג'טים, יוצרים עיצוב משלכם ומטמיעים במהירות את מצב האפלה.
- איך יוצרים אפליקציות דינמיות בפלטפורמות שונות.
- איך יוצרים אפליקציות שנראות טוב בכל מסך.
- איך מוסיפים תנועה לאפליקציה ב-Flutter כדי להפוך אותה למושכת יותר.
דרישות מוקדמות
אנחנו יוצאים מנקודת הנחה שיש לכם ניסיון מסוים ב-Flutter. אם לא, מומלץ קודם ללמוד את היסודות. הקישורים הבאים יכולים לעזור:
- בניית ממשקי משתמש באמצעות Flutter
- כדאי לנסות את Codelab האפליקציה הראשונה שלכם ב-Flutter
מה תפַתחו
בקודלאב הזה תלמדו איך ליצור את מסך הבית של אפליקציה בשם MyArtist
. זוהי אפליקציית נגן מוזיקה שבה מעריצים יכולים להתעדכן לגבי האומן האהוב עליהם. מוסבר בו איך לשנות את עיצוב האפליקציה כך שייראה יפה בכל הפלטפורמות.
בסרטונים הבאים אפשר לראות איך האפליקציה פועלת בסיום הקודלאב:
מה היית רוצה ללמוד מהקודלאב הזה?
2. הגדרת סביבת הפיתוח ב-Flutter
כדי להשלים את שיעור ה-Lab הזה, תצטרכו שני תוכנות – Flutter SDK ועורך.
אפשר להריץ את הקודלאב בכל אחד מהמכשירים הבאים:
- מכשיר Android או iOS פיזי שמחובר למחשב ומוגדר למצב פיתוח.
- סימולטור iOS (נדרשת התקנה של הכלים של Xcode).
- Android Emulator (נדרשת הגדרה ב-Android Studio).
- דפדפן (נדרש דפדפן Chrome לניפוי באגים).
- כאפליקציית מחשב ל-Windows, ל-Linux או ל-macOS. עליכם לפתח בפלטפורמה שבה אתם מתכננים לפרוס. לכן, אם רוצים לפתח אפליקציה למחשב עם Windows, צריך לפתח ב-Windows כדי לגשת לרשת ה-build המתאימה. יש דרישות ספציפיות למערכות הפעלה שפורטו באתר docs.flutter.dev/desktop.
3. הורדת האפליקציה למתחילים ב-Codelab
יצירת עותקים (cloning) מ-GitHub
כדי להעתיק (clone) את סדנת הקוד הזו מ-GitHub, מריצים את הפקודות הבאות:
git clone https://github.com/flutter/codelabs.git cd codelabs/boring_to_beautiful/step_01/
כדי לוודא שהכול פועל, מריצים את אפליקציית Flutter כאפליקציית מחשב כמו שמתואר בהמשך. לחלופין, אפשר לפתוח את הפרויקט הזה בסביבת הפיתוח המשולבת (IDE) ולהשתמש בכלים שלה כדי להריץ את האפליקציה.
flutter run
הצלחת! קוד ההתחלה של מסך הבית של MyArtist אמור לפעול. אמור להופיע מסך הבית של MyArtist. האתר נראה טוב במחשב, אבל בנייד... לא טוב. בין היתר, הוא לא מתייחס למגרעת. אל דאגה, אפשר לפתור את זה.
סיור בקוד
בשלב הבא נבצע סיור בקוד.
פותחים את הקובץ lib/src/features/home/view/home_screen.dart
, שמכיל את הפרטים הבאים:
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,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
הקובץ הזה מייבא את material.dart
ומטמיע ווידג'ט עם מצב באמצעות שתי כיתות:
- ההצהרה
import
מאפשרת להשתמש ב-Material Components. - הכיתה
HomeScreen
מייצגת את כל הדף שמוצג. - שיטת
build()
של הכיתה_HomeScreenState
יוצרת את הבסיס של עץ הווידג'טים, שמשפיע על האופן שבו נוצרים כל הווידג'טים בממשק המשתמש.
4. שימוש בטיפוגרפיה
טקסט נמצא בכל מקום. טקסט הוא דרך שימושית לתקשר עם המשתמש. האם האפליקציה שלכם אמורה להיות ידידותית ומהנה, או אולי אמינה ומקצועית? יש סיבה לכך שאפליקציית הבנקאות המועדפת עליכם לא משתמשת ב-Comic Sans. אופן הצגת הטקסט משפיע על הרושם הראשוני של המשתמשים לגבי האפליקציה. ריכזנו כאן כמה דרכים לשימוש בטקסט בצורה מושכלת יותר.
הסברים ויזואליים יעילים יותר מהסברים מילוליים
כשהדבר אפשרי, 'הראו' במקום 'אמרו'. לדוגמה, ב-NavigationRail
באפליקציית ההתחלה יש כרטיסיות לכל מסלול ראשי, אבל הסמלים המובילים זהים:
הפתרון הזה לא מועיל כי המשתמש עדיין צריך לקרוא את הטקסט בכל כרטיסייה. קודם כל, מוסיפים רמזים חזותיים כדי שהמשתמש יוכל להעיף מבט מהיר על הסמלים המובילים ולמצוא את הכרטיסייה הרצויה. כך גם קל יותר לבצע לוקליזציה ולהתאים את האתר לנגישות.
בקטע lib/src/shared/router.dart
, מוסיפים סמלים מובילים ייחודיים לכל יעד ניווט (דף הבית, פלייליסט ואנשים):
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',
),
];
בעיות?
אם האפליקציה לא פועלת כמו שצריך, כדאי לחפש שגיאות הקלדה. אם צריך, אפשר להשתמש בקוד שמופיע בקישורים הבאים כדי לחזור לדרך הנכונה.
בחירת גופנים בצורה מושכלת
הגופנים קובעים את האופי של האפליקציה, ולכן חשוב לבחור את הגופן הנכון. כשבוחרים גופן, כדאי להביא בחשבון כמה דברים:
- Sans-serif או serif: לגופנים מסוג serif יש קווים דקורטיביים או "זנבות" בסוף האותיות, והם נחשבים לפורמליים יותר. בגופנים ללא serif אין קווים דקורטיביים, והם נתפסים בדרך כלל כפחות רשמיים.
- גופנים באותיות גדולות בלבד: שימוש באותיות גדולות בלבד מתאים כדי למשוך תשומת לב לכמויות קטנות של טקסט (למשל כותרות), אבל אם משתמשים בהן יותר מדי, הן עלולות להיחשב כצעקה שגורמת למשתמש להתעלם מהן לגמרי.
- אותיות רישיות בכותרות או אותיות רישיות בתחילת משפט: כשאתם מוסיפים כותרות או תוויות, כדאי להחליט איך להשתמש באותיות רישיות: אותיות רישיות בכותרות, שבהן האות הראשונה בכל מילה היא אות רישית ("This Is a Title Case Title"), הן רשמיות יותר. אותיות רישיות בתחילת משפט, שבהן רק שמות עצם ומילה ראשונה בטקסט מודגשים באותיות רישיות ('This is a sentence case title'), הן פחות רשמיות ויותר מדוברות.
- מרווחי גופן (מרווח בין כל אות), אורך שורה (רוחב הטקסט המלא במסך) וגובה שורה (גובה כל שורה של טקסט): אם מרווחי הגופן גדולים מדי או קטנים מדי, או אם אורך השורה או גובה השורה גדולים מדי או קטנים מדי, קשה יותר לקרוא את האפליקציה. לדוגמה, יכול להיות שיהיה קשה לשמור על המיקום בקריאה של קטע טקסט גדול בלי הפסקות.
בהתאם לכך, כדאי להיכנס אל Google Fonts ולבחור גופן ללא serif, כמו Montserrat, כי אפליקציית המוזיקה אמורה להיות שובבה ומהנה.
משורת הפקודה, שולפים את החבילה google_fonts
. הפעולה הזו מעדכנת גם את הקובץ pubspec.yaml
כדי להוסיף את הגופנים כיחסי תלות של האפליקציה.
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>
ב-lib/src/shared/extensions.dart
, מייבאים את החבילה החדשה:
lib/src/shared/extensions.dart
import 'package:google_fonts/google_fonts.dart'; // Add this line.
הגדרת TextTheme:
של Montserrat
TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line
מבצעים טעינה מחדש בזמן ריצה (hot reload) של כדי להפעיל את השינויים. (משתמשים בלחצן בסביבת הפיתוח המשולבת, או מזינים
r
בשורת הפקודה כדי לבצע טעינה מחדש בזמן אמת):
אמורים להופיע הסמלים החדשים של NavigationRail
עם טקסט שמוצג בגופן Montserrat.
בעיות?
אם האפליקציה לא פועלת כמו שצריך, כדאי לחפש שגיאות הקלדה. אם צריך, אפשר להשתמש בקוד שמופיע בקישורים הבאים כדי לחזור לדרך הנכונה.
5. הגדרת העיצוב
עיצוב לפי נושאים עוזר ליצור עיצוב מובנה ואחיד באפליקציה, על ידי הגדרת מערכת של צבעים וסגנונות טקסט. בעזרת נושאים אפשר להטמיע ממשק משתמש במהירות בלי להדאיג לגבי פרטים קטנים, כמו ציון הצבע המדויק של כל ווידג'ט.
מפתחי Flutter בדרך כלל יוצרים רכיבים עם עיצוב מותאם אישית באחת משתי דרכים:
- ליצור ווידג'טים מותאמים אישית לכל אחד עם עיצוב משלו.
- יצירת עיצובים ברמת ההיקף לווידג'טים שמוגדרים כברירת מחדל.
בדוגמה הזו נעשה שימוש בספק עיצוב שנמצא ב-lib/src/shared/providers/theme.dart
כדי ליצור ווידג'טים וצבעים בעיצוב עקבי לאורך האפליקציה:
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);
}
}
כדי להשתמש בספק, יוצרים מכונה ומעבירים אותה לאובייקט העיצוב ברמת ההיקף ב-MaterialApp
, שנמצא ב-lib/src/shared/app.dart
. הוא יועבר בירושה לכל אובייקטי Theme
בתצוגת עץ:
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,
);
},
),
)),
),
);
}
}
עכשיו, אחרי שהגדרתם את העיצוב, תוכלו לבחור צבעים לאפליקציה.
לפעמים קשה לבחור את קבוצת הצבעים הנכונה. יכול להיות שיש לכם רעיון לגבי הצבע הראשי, אבל סביר להניח שתרצו שהאפליקציה תכלול יותר מצבע אחד. איזה צבע צריך להיות לטקסט? שם? תוכן? קישורים? מה לגבי צבע הרקע? הכלי ליצירת עיצוב ב-Material הוא כלי מבוסס-אינטרנט (שנוסף ב-Material 3) שעוזר לכם לבחור קבוצה של צבעים משלימים לאפליקציה.
כדי לבחור צבע מקור לאפליקציה, פותחים את Material Theme Builder ומעיינים בצבעים שונים לממשק המשתמש. חשוב לבחור צבע שמתאים לאסתטיקה של המותג או להעדפה האישית שלכם.
אחרי שיוצרים עיצוב, לוחצים לחיצה ימנית על בועת הצבע ראשי – נפתח תיבת דו-שיח שמכילה את הערך הקסדצימלי של הצבע הראשי. מעתיקים את הערך הזה. (אפשר גם להגדיר את הצבע באמצעות תיבת הדו-שיח הזו).
מעבירים את הערך הקסדצימלי של הצבע העיקרי לספק העיצוב. לדוגמה, צבע הקסדצימלי #00cbe6
מצוין כ-Color(0xff00cbe6)
. הפונקציה ThemeProvider
יוצרת ThemeData
שמכיל את קבוצת הצבעים המשלימים שצפיתם בהם ב-Material Theme Builder:
final settings = ValueNotifier(ThemeSettings(
sourceColor: Color(0xff00cbe6), // Replace this color
themeMode: ThemeMode.system,
));
מפעילים מחדש את האפליקציה. אחרי שמוסיפים את הצבע הראשי, האפליקציה מתחילה להיראות יותר דרמטית. כדי לגשת לכל הצבעים החדשים, צריך להפנות לעיצוב בהקשר ולשלוף את ColorScheme
:
final colors = Theme.of(context).colorScheme;
כדי להשתמש בצבע מסוים, צריך לגשת לתפקיד צבע ב-colorScheme
. עוברים אל lib/src/shared/views/outlined_card.dart
ומוסיפים ל-OutlinedCard
גבול:
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,
),
);
}
}
בגרסה השלישית של Material הוספנו תפקידים צבעוניים מעודנים שמשתלבים זה בזה, וניתן להשתמש בהם בכל ממשק המשתמש כדי להוסיף שכבות חדשות של ביטוי. התפקידים החדשים לפי צבעים כוללים:
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
בנוסף, אסימוני עיצוב חדשים תומכים גם בעיצוב בהיר וגם בעיצוב כהה:
אפשר להשתמש בתפקידי הצבעים האלה כדי להקצות משמעות והדגשה לחלקים שונים בממשק המשתמש. גם אם רכיב מסוים לא בולט, עדיין אפשר להשתמש בצבע דינמי כדי להבליט אותו.
המשתמש יכול להגדיר את הבהירות של האפליקציה בהגדרות המערכת של המכשיר. ב-lib/src/shared/app.dart
, כשהמכשיר מוגדר למצב כהה, מחזירים את העיצוב הכהה ואת מצב העיצוב ל-MaterialApp
.
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,
);
כדי להפעיל את מצב האפלה, לוחצים על סמל הירח בפינה השמאלית העליונה.
בעיות?
אם האפליקציה לא פועלת כמו שצריך, אפשר להשתמש בקוד שמופיע בקישור הבא כדי להמשיך כרגיל.
6. הוספת עיצוב רספונסיבי
אפשר ליצור אפליקציות ב-Flutter שפועלות כמעט בכל מקום, אבל זה לא אומר שכל אפליקציה אמורה להתנהג באופן זהה בכל מקום. המשתמשים מצפים לתכונות ולהתנהגויות שונות בפלטפורמות שונות.
חבילת Material כוללת חבילות שבעזרתן קל יותר לעבוד עם פריסות מותאמות. אפשר למצוא את חבילות Flutter האלה ב-GitHub.
כשאתם מפתחים אפליקציה רספונסיבית לפלטפורמות שונות, חשוב לזכור את ההבדלים הבאים בין הפלטפורמות:
- שיטת קלט: עכבר, מגע או בקר משחקים (גיימפאד)
- גודל הגופן, כיוון המכשיר ומרחק הצפייה
- גודל המסך וצורת המכשיר: טלפון, טאבלט, מכשיר מתקפל, מחשב, אינטרנט
קובץ lib/src/shared/views/adaptive_navigation.dart
מכיל סיווג ניווט שבו אפשר לספק רשימה של יעדים ותוכן לעיבוד הגוף. מכיוון שמשתמשים בפריסה הזו במספר מסכים, יש פריסה בסיסית משותפת שאפשר להעביר לכל צאצא. פסי הניווט מתאימים למחשבים ולמסכים גדולים, אבל כדי שהפריסה תהיה ידידותית לניידים, כדאי להציג סרגל ניווט תחתון בניידים במקום זאת.
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.
},
);
}
}
לא כל המסכים באותו גודל. אם תנסו להציג את גרסת המחשב של האפליקציה בטלפון, תצטרכו לשלב בין צפייה בעיניים עקומות לבין שינוי מרחק התצוגה כדי לראות את כל הפרטים. אתם רוצים לשנות את המראה של האפליקציה בהתאם למסך שבו היא מוצגת. בעזרת עיצוב רספונסיבי, אתם יכולים לוודא שהאפליקציה תיראה נהדר במסכים בגדלים שונים.
כדי שהאפליקציה תהיה רספונסיבית, מומלץ להוסיף כמה נקודות עצירה דינמיות (לא להתבלבל עם נקודות עצירה לניפוי באגים). נקודות העצירה האלה מציינות את גדלי המסכים שבהם הפריסה של האפליקציה צריכה להשתנות.
במסכים קטנים יותר אי אפשר להציג כמות תוכן גדולה כמו במסכים גדולים יותר, בלי לכווץ את התוכן. כדי שהאפליקציה לא תיראה כמו אפליקציה למחשב שהתכווץ, כדאי ליצור פריסה נפרדת לנייד שמשתמשת בכרטיסיות כדי לפצל את התוכן. כך האפליקציה נראית יותר 'מקומית' בנייד.
שיטות התוספים הבאות (שמוגדרות בפרויקט MyArtist
ב-lib/src/shared/extensions.dart
) הן נקודת התחלה טובה כשאתם מעצבים פריסות שמותאמות ליעדים שונים.
lib/src/shared/extensions.dart
extension BreakpointUtils on BoxConstraints {
bool get isTablet => maxWidth > 730;
bool get isDesktop => maxWidth > 1200;
bool get isMobile => !isTablet && !isDesktop;
}
מסך גדול מ-730 פיקסלים (בכיוון הארוך ביותר), אבל קטן מ-1,200 פיקסלים, נחשב לטאבלט. כל תמונה גדולה מ-1,200 פיקסלים נחשבת לתמונה למחשב. אם המכשיר הוא לא טאבלט ולא מחשב, הוא נחשב למכשיר נייד. מידע נוסף על נקודות עצירה מותאמות ב-material.io
בפריסה הרספונסיבית של מסך הבית נעשה שימוש ב-AdaptiveContainer
וב-AdaptiveColumn
על סמך רשת של 12 עמודות.
כדי ליצור פריסה דינמית, צריך ליצור שתי פריסות: אחת לנייד ופרסה רספונסיבית למסכים גדולים יותר. בשלב הזה, LayoutBuilder
מחזירה פריסת מחשב. ב-lib/src/features/home/view/home_screen.dart
, יוצרים את הפריסה לנייד כ-TabBar
ו-TabBarView
עם 4 כרטיסיות.
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,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
בעיות?
אם האפליקציה לא פועלת כמו שצריך, אפשר להשתמש בקוד שמופיע בקישור הבא כדי להמשיך כרגיל.
7. שימוש ברווחים לבנים
רווחים לבנים הם כלי חזותי חשוב באפליקציה, והם יוצרים הפסקה ארגונית בין קטעים.
עדיף שיהיה יותר מדי שטח לבן מאשר מעט מדי. מומלץ להוסיף יותר רווחים לבנים במקום להקטין את גודל הגופן או את הרכיבים החזותיים כדי שיתאימו למרחב.
חוסר במרחב לבן יכול להקשות על אנשים עם בעיות ראייה. יותר מדי רווח לבן עלול לגרום לחוסר הרמוניה ולכך שהממשק המשתמש ייראה לא מאורגן. לדוגמה, צילום המסך הבא:
בשלב הבא תוסיפו למסך הבית שטח ללא תוכן כדי שיהיה בו יותר מקום. לאחר מכן תוכלו לשנות את הפריסה כדי לשפר את המרווחים.
כדי להוסיף רווח לבן מסביב לווידג'ט, אפשר לעטוף אותו באובייקט Padding
. מגדילים את כל ערכי המילוי ב-lib/src/features/home/view/home_screen.dart
ל-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,
),
),
],
),
),
],
),
),
),
],
),
),
);
מעלים מחדש את האפליקציה. היא אמורה להיראות כמו קודם, אבל עם יותר מרווח בין הווידג'טים. הרווח הנוסף נראה טוב יותר, אבל באנר ה-Highlight בחלק העליון עדיין קרוב מדי לקצוות.
ב-lib/src/features/home/view/home_highlight.dart
, משנים את הערך של הרווח בבאנר ל-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')),
),
),
),
],
);
}
}
מעלים מחדש את האפליקציה. בין שני הפלייליסטים בתחתית המסך אין רווח, כך שהם נראים כאילו הם שייכים לאותה טבלה. זה לא המצב, וזה מה שצריך לתקן בשלב הבא.
כדי להוסיף רווחים בין הפלייליסטים, מוסיפים ווידג'ט בגודל מתאים ל-Row
שמכיל אותם. ב-lib/src/features/home/view/home_screen.dart
, מוסיפים SizedBox
ברוחב 35:
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,
),
),
],
),
),
],
),
),
),
מעלים מחדש את האפליקציה. האפליקציה אמורה להיראות כך:
עכשיו יש הרבה מקום לתוכן במסך הבית, אבל הכל נראה מופרד מדי ואין הרמוניה בין הקטעים.
עד עכשיו הגדרתם את כל השוליים (האופקיים והאנכיים) של הווידג'טים במסך הבית ל-35 באמצעות EdgeInsets.all(35)
, אבל אפשר גם להגדיר את השוליים של כל אחד מהקצוות בנפרד. מתאימים אישית את הריפוד כך שיתאים למרחב המשותף.
EdgeInsets.LTRB()
מגדיר את הצדדים השמאלי, העליון, הימני והתחתון בנפרדEdgeInsets.symmetric()
מגדיר את המרווח הפנימי האנכי (למעלה ולמטה) כזהה, ואת המרווח הפנימי האופקי (ימינה ושמאלה) כזההEdgeInsets.only()
מגדיר רק את הקצוות שצוינו.
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,
),
),
],
),
),
],
),
),
),
],
),
),
);
ב-lib/src/features/home/view/home_highlight.dart
, מגדירים את המרווח הפנימי בצד ימין ובצד ימין של הבאנר ל-35, ואת המרווח הפנימי בחלק העליון ובחלק התחתון ל-5:
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')),
),
),
),
],
);
}
}
מעמיסים מחדש את האפליקציה. הפריסה והמרווחים נראים הרבה יותר טוב! כדי לסיים, מוסיפים תנועה ואנימציה.
בעיות?
אם האפליקציה לא פועלת כמו שצריך, אפשר להשתמש בקוד שמופיע בקישור הבא כדי להמשיך כרגיל.
8. הוספת תנועה ואנימציה
תנועה ואנימציה הן דרכים מצוינות להוסיף תנועה ואנרגיה, ולספק משוב כשהמשתמש מבצע פעולה באפליקציה.
אנימציה בין מסכים
ה-ThemeProvider
מגדיר PageTransitionsTheme
עם אנימציות של מעברים בין מסכים לפלטפורמות לנייד (iOS, Android). משתמשים במחשב כבר מקבלים משוב מהקליק שלהם בעכבר או במשטח המגע, ולכן אין צורך באנימציה של מעבר בין דפים.
ב-Flutter יש אנימציות של מעברים בין מסכים שאפשר להגדיר לאפליקציה על סמך פלטפורמת היעד, כפי שמתואר בקטע lib/src/shared/providers/theme.dart
:
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(),
},
);
מעבירים את PageTransitionsTheme
גם לעיצוב הבהיר וגם לעיצוב הכהה ב-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,
);
}
ללא אנימציה ב-iOS
עם אנימציה ב-iOS
בעיות?
אם האפליקציה לא פועלת כמו שצריך, אפשר להשתמש בקוד שמופיע בקישור הבא כדי להמשיך כרגיל.
9. הוספת מצבי מעבר
אחת הדרכים להוסיף תנועה לאפליקציה למחשב היא באמצעות מצבי מעוף, שבהם הווידג'ט משנה את המצב שלו (למשל צבע, צורה או תוכן) כשהמשתמש מעביר את הסמן מעליו.
כברירת מחדל, הכיתה _OutlinedCardState
(שמשמשת לאריחי הפלייליסט 'הושמעו לאחרונה') מחזירה MouseRegion
– שממיר את החץ של הסמן למצב 'צבעוני' כשמעבירים מעליו את העכבר – אבל אפשר להוסיף משוב חזותי נוסף.
פותחים את הקובץ lib/src/shared/views/outlined_card.dart
ומחליפים את התוכן שלו בהטמעה הבאה כדי להוסיף את המצב _hovered
.
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,
),
),
);
}
}
מעלים מחדש את האפליקציה ומעבירים את העכבר מעל אחת מהכרטיסיות של הפלייליסטים שהופעלו לאחרונה.
ה-OutlinedCard
משנה את רמת השקיפות ומעגל את הפינות.
לסיום, הופכים את מספר השיר בפלייליסט ללחצן הפעלה באמצעות הווידג'ט HoverableSongPlayButton
שמוגדר ב-lib/src/shared/views/hoverable_song_play_button.dart
. ב-lib/src/features/playlists/view/playlist_songs.dart
, עוטפים את הווידג'ט Center
(שמכיל את מספר השיר) ב-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())),
],
),
מעבירים את העכבר מעל מספר השיר במצעד השירים המובילים היום או בפלייליסט השירים החדשים, ומפעילים מחדש את האפליקציה.
המספר הופך ללחצן הפעלה, שמפעיל את השיר כשלוחצים עליו.
10. מעולה!
סיימת את ה-Codelab הזה! למדתם שיש הרבה שינויים קטנים שאפשר לשלב באפליקציה כדי להפוך אותה ליפה יותר, וגם לנגישה יותר, ניתנת יותר ללוקליזציה ומתאימה יותר למספר פלטפורמות. השיטות האלה כוללות, בין היתר:
- טיפוגרפיה: טקסט הוא יותר מכלי תקשורת. האופן שבו הטקסט מוצג יכול להשפיע באופן חיובי על חוויית המשתמש ועל התפיסה שלו לגבי האפליקציה.
- עיצוב לפי נושא: יצירת מערכת עיצוב שאפשר להשתמש בה בצורה מהימנה בלי שתצטרכו לקבל החלטות עיצוב לגבי כל ווידג'ט.
- התאמה אישית: חשוב להביא בחשבון את המכשיר והפלטפורמה שבהם המשתמש מפעיל את האפליקציה ואת היכולות שלהם. חשוב להביא בחשבון את גודל המסך ואת אופן הצגת האפליקציה.
- תנועה ואנימציה: הוספת תנועה לאפליקציה מוסיפה אנרגיה לחוויית המשתמש, ומבחינה מעשית יותר, מספקת למשתמשים משוב.
בעזרת כמה שינויים קטנים, האפליקציה שלכם יכולה להפוך ממשעממת ליפה:
לפני
אחרי
השלבים הבאים
אנחנו מקווים שלמדתם עוד על פיתוח אפליקציות יפות ב-Flutter.
אם תשתמשו באחד מהטיפים או הטריקים שצוינו כאן (או אם יש לכם טיפ משלכם לשתף), נשמח לשמוע מכם! אפשר ליצור איתנו קשר ב-Twitter @rodydavis ו-@khanhnwin.
מקורות המידע הבאים עשויים לעזור לך.
בחירת עיצוב
- Material Theme Builder (כלי)
משאבים מותאמים רספונסיביים:
- פיענוח Flutter במודעות דינמיות לעומת רספונסיביות (סרטון)
- פריסות דינמיות (סרטון מ-The Boring Flutter Development Show)
- יצירת אפליקציות רספונסיביות ומותאמות (flutter.dev)
- רכיבי Material מותאמים ל-Flutter (ספרייה ב-GitHub)
- 5 דברים שאפשר לעשות כדי להכין את האפליקציה למסכים גדולים (סרטון מ-Google I/O 2021)
מקורות מידע כלליים בנושא עיצוב:
- הדברים הקטנים: איך הופכים למעצבים-מפתחים מהסוג האגדי (סרטון מ-Flutter Engage)
- Material Design 3 למכשירים מתקפלים (material.io)
בנוסף, אתם יכולים ליצור קשר עם קהילת Flutter.
קדימה, תהיו יפים!