1. مقدمه
Flutter جعبه ابزار UI گوگل برای ساخت برنامه های زیبا و بومی کامپایل شده برای موبایل، وب و دسکتاپ از یک پایگاه کد واحد است. Flutter با کدهای موجود کار می کند، توسط توسعه دهندگان و سازمان ها در سراسر جهان استفاده می شود و رایگان و متن باز است.
در این کد لبه، شما یک برنامه موسیقی Flutter را ارتقا می دهید و آن را از خسته کننده به زیبا تبدیل می کنید. برای انجام این کار، این نرم افزار کد از ابزارها و API های معرفی شده در Material 3 استفاده می کند.
چیزی که یاد خواهید گرفت
- چگونه یک برنامه فلاتر بنویسیم که در همه پلتفرم ها قابل استفاده و زیبا باشد.
- چگونه متن را در برنامه خود طراحی کنید تا مطمئن شوید که به تجربه کاربر اضافه می کند.
- چگونه رنگهای مناسب را انتخاب کنید، ویجتها را سفارشی کنید، تم خود را بسازید، و به سرعت و به راحتی حالت تاریک را اجرا کنید.
- نحوه ساخت اپلیکیشن های تطبیقی چند پلتفرمی
- چگونه برنامه هایی بسازیم که در هر صفحه ای ظاهر خوبی داشته باشند.
- چگونه به برنامه Flutter خود حرکت اضافه کنید تا واقعاً پاپ شود.
پیش نیازها:
این کد لبه فرض می کند که شما تجربه فلاتر دارید. اگر نه، ممکن است بخواهید ابتدا اصول اولیه را یاد بگیرید. لینک های زیر مفید هستند:
- از چارچوب ویجت فلاتر بازدید کنید
- برنامه Write Your First Flutter، قسمت 1 Codelab را امتحان کنید
چیزی که خواهی ساخت
این لبه کد شما را در ساخت صفحه اصلی برای برنامهای به نام MyArtist راهنمایی میکند، یک برنامه پخش موسیقی که در آن طرفداران میتوانند با هنرمندان مورد علاقه خود بهروز باشند. در مورد اینکه چگونه می توانید طراحی اپلیکیشن خود را تغییر دهید تا در همه پلتفرم ها زیبا به نظر برسد، بحث می شود.
ویدیوهای زیر نحوه عملکرد برنامه را در تکمیل این کد لبه نشان می دهد:
دوست دارید از این کد لبه چه چیزی یاد بگیرید؟
2. محیط توسعه Flutter خود را تنظیم کنید
برای تکمیل این آزمایشگاه به دو نرم افزار نیاز دارید - Flutter SDK و یک ویرایشگر .
شما می توانید کدلب را با استفاده از هر یک از این دستگاه ها اجرا کنید:
- یک دستگاه فیزیکی Android یا iOS که به رایانه شما متصل شده و روی حالت Developer تنظیم شده است.
- شبیه ساز iOS (نیاز به نصب ابزار Xcode دارد).
- شبیه ساز اندروید (نیاز به نصب در Android Studio دارد).
- یک مرورگر (Chrome برای اشکال زدایی لازم است).
- به عنوان یک برنامه دسکتاپ Windows ، Linux ، یا macOS . شما باید روی پلتفرمی که قصد استقرار در آن را دارید توسعه دهید. بنابراین، اگر می خواهید یک برنامه دسکتاپ ویندوز توسعه دهید، باید در ویندوز توسعه دهید تا به زنجیره ساخت مناسب دسترسی داشته باشید. الزامات خاص سیستم عامل وجود دارد که به طور مفصل در docs.flutter.dev/desktop پوشش داده شده است.
3. اپلیکیشن شروع کد لبه را دریافت کنید
آن را از GitHub کلون کنید
برای شبیه سازی این کد لبه از GitHub، دستورات زیر را اجرا کنید:
git clone https://github.com/flutter/codelabs.git cd codelabs/boring_to_beautiful/step_01/
برای اطمینان از اینکه همه چیز کار می کند، برنامه Flutter را به عنوان یک برنامه دسکتاپ مانند تصویر زیر اجرا کنید. از طرف دیگر، این پروژه را در IDE خود باز کنید و از ابزار آن برای اجرای برنامه استفاده کنید.
موفقیت! کد شروع برای صفحه اصلی MyArtist باید در حال اجرا باشد. باید صفحه اصلی MyArtist را ببینید. روی دسکتاپ خوب به نظر می رسد، اما موبایل... عالی نیست. برای یک چیز، آن را به شکاف احترام نمی گذارد. نگران نباش اینو درست میکنی
کد را بگردید
در مرحله بعد، کد را مرور کنید.
lib/src/features/home/view/home_screen.dart
را باز کنید که شامل موارد زیر است:
lib/src/features/home/view/home_screen.dart
import 'package:adaptive_components/adaptive_components.dart';
import 'package:flutter/material.dart';
import '../../../shared/classes/classes.dart';
import '../../../shared/extensions.dart';
import '../../../shared/providers/providers.dart';
import '../../../shared/views/views.dart';
import '../../playlists/view/playlist_songs.dart';
import 'view.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
final PlaylistsProvider playlistProvider = PlaylistsProvider();
final List<Playlist> playlists = playlistProvider.playlists;
final Playlist topSongs = playlistProvider.topSongs;
final Playlist newReleases = playlistProvider.newReleases;
final ArtistsProvider artistsProvider = ArtistsProvider();
final List<Artist> artists = artistsProvider.artists;
return LayoutBuilder(
builder: (context, constraints) {
// Add conditional mobile layout
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2), // Modify this line
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(2), // Modify this line
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2), // Modify this line
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.all(2), // Modify this line
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
// Add spacer between tables
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.all(2), // Modify this line
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
این فایل material.dart
را وارد می کند و یک ویجت stateful را با استفاده از دو کلاس پیاده سازی می کند:
- بیانیه
import
مولفه های مواد را در دسترس قرار می دهد. - کلاس
HomeScreen
کل صفحه نمایش داده شده را نشان می دهد. - متد
build()
کلاس_HomeScreenState
ریشه درخت ویجت را ایجاد می کند که بر نحوه ایجاد همه ویجت ها در UI تأثیر می گذارد.
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 : فونتهای سریف دارای خطوط تزیینی یا "دم" در انتهای حروف هستند و بیشتر رسمی تلقی میشوند. فونتهای Sans-serif خطوط تزیینی ندارند و بیشتر غیررسمیتر تلقی میشوند. sans serif بزرگ T و serif بزرگ T
- فونت های تمام حروف بزرگ : استفاده از تمام حروف برای جلب توجه به مقدار کمی از متن مناسب است (به تیترها فکر کنید)، اما در صورت استفاده بیش از حد، می توان آن را به عنوان فریاد تلقی کرد که باعث می شود کاربر به طور کامل آن را نادیده بگیرد.
- حروف کوچک عنوان یا جمله : هنگام اضافه کردن عنوان یا برچسب، نحوه استفاده از حروف بزرگ را در نظر بگیرید: حروف عنوان ، که در آن حرف اول هر کلمه بزرگ می شود ("این عنوان یک عنوان عنوان است")، رسمی تر است. جمله ای که فقط اسم های خاص را با حروف بزرگ می نویسد و اولین کلمه در متن ("این عنوان مورد جمله است") بیشتر محاوره ای و غیر رسمی است.
- کرنینگ (فاصله بین هر حرف)، طول خط (عرض متن کامل در سراسر صفحه) و ارتفاع خط (بلندی هر خط از متن) : کم یا زیاد بودن هر یک از این موارد باعث می شود برنامه شما کمتر قابل خواندن باشد. به عنوان مثال، هنگام خواندن یک متن بزرگ و بدون شکستگی، به راحتی می توانید جایگاه خود را از دست بدهید.
با در نظر گرفتن این موضوع، به Google Fonts بروید و یک فونت sans-serif مانند Montserrat انتخاب کنید، زیرا برنامه موسیقی برای بازیگوش و سرگرم کننده در نظر گرفته شده است.
از خط فرمان، بسته google_fonts
را بکشید. این همچنین فایل pubspec را به روز می کند تا فونت ها را به عنوان وابستگی برنامه اضافه کند.
$ flutter pub add google_fonts
macos/Runner/DebugProfile.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<!-- Make sure these lines are present from here... -->
<key>com.apple.security.network.client</key>
<true/>
<!-- To here. -->
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
در lib/src/shared/extensions.dart
، بسته جدید را وارد کنید:
lib/src/shared/extensions.dart
import 'package:google_fonts/google_fonts.dart'; // Add this line.
TextTheme:
TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line
بارگذاری مجدد داغ برای فعال کردن تغییرات (از دکمه در IDE خود استفاده کنید یا از خط فرمان، r
برای بارگذاری مجدد داغ وارد کنید.):
شما باید آیکون های NavigationRail
جدید به همراه متن نمایش داده شده با فونت Montserrat را ببینید.
مشکلات؟
اگر برنامه شما به درستی اجرا نمی شود، به دنبال اشتباهات تایپی باشید. در صورت نیاز، از کد موجود در لینک های زیر برای بازگشت به مسیر استفاده کنید.
5. موضوع را تنظیم کنید
تم ها با مشخص کردن مجموعه ای از رنگ ها و سبک های متن، به ایجاد طراحی ساختاریافته و یکنواختی به برنامه کمک می کنند. تم ها شما را قادر می سازند تا به سرعت یک رابط کاربری را بدون نیاز به استرس بر روی جزئیات جزئی مانند تعیین رنگ دقیق برای هر ویجت پیاده سازی کنید.
توسعه دهندگان فلاتر معمولاً مؤلفه هایی با موضوع سفارشی به یکی از دو روش ایجاد می کنند:
- ویجتهای سفارشی جداگانه ایجاد کنید که هر کدام موضوع خاص خود را دارند.
- برای ویجتهای پیشفرض تمهای محدودهای ایجاد کنید.
این مثال از یک ارائهدهنده تم واقع در 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.value, settings.value.sourceColor.value));
}
Color source(Color? target) {
Color source = settings.value.sourceColor;
if (target != null) {
source = blend(target);
}
return source;
}
ColorScheme colors(Brightness brightness, Color? targetColor) {
final dynamicPrimary = brightness == Brightness.light
? lightDynamic?.primary
: darkDynamic?.primary;
return ColorScheme.fromSeed(
seedColor: dynamicPrimary ?? source(targetColor),
brightness: brightness,
);
}
ShapeBorder get shapeMedium => RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
);
CardTheme cardTheme() {
return CardTheme(
elevation: 0,
shape: shapeMedium,
clipBehavior: Clip.antiAlias,
);
}
ListTileThemeData listTileTheme(ColorScheme colors) {
return ListTileThemeData(
shape: shapeMedium,
selectedColor: colors.secondary,
);
}
AppBarTheme appBarTheme(ColorScheme colors) {
return AppBarTheme(
elevation: 0,
backgroundColor: colors.surface,
foregroundColor: colors.onSurface,
);
}
TabBarTheme tabBarTheme(ColorScheme colors) {
return TabBarTheme(
labelColor: colors.secondary,
unselectedLabelColor: colors.onSurfaceVariant,
indicator: BoxDecoration(
border: Border(
bottom: BorderSide(
color: colors.secondary,
width: 2,
),
),
),
);
}
BottomAppBarTheme bottomAppBarTheme(ColorScheme colors) {
return BottomAppBarTheme(
color: colors.surface,
elevation: 0,
);
}
BottomNavigationBarThemeData bottomNavigationBarTheme(ColorScheme colors) {
return BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
backgroundColor: colors.surfaceContainerHighest,
selectedItemColor: colors.onSurface,
unselectedItemColor: colors.onSurfaceVariant,
elevation: 0,
landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
);
}
NavigationRailThemeData navigationRailTheme(ColorScheme colors) {
return const NavigationRailThemeData();
}
DrawerThemeData drawerTheme(ColorScheme colors) {
return DrawerThemeData(
backgroundColor: colors.surface,
);
}
ThemeData light([Color? targetColor]) {
final _colors = colors(Brightness.light, targetColor);
return ThemeData.light().copyWith(
pageTransitionsTheme: pageTransitionsTheme,
colorScheme: _colors,
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(_colors),
bottomAppBarTheme: bottomAppBarTheme(_colors),
bottomNavigationBarTheme: bottomNavigationBarTheme(_colors),
navigationRailTheme: navigationRailTheme(_colors),
tabBarTheme: tabBarTheme(_colors),
drawerTheme: drawerTheme(_colors),
scaffoldBackgroundColor: _colors.background,
useMaterial3: true,
);
}
ThemeData dark([Color? targetColor]) {
final _colors = colors(Brightness.dark, targetColor);
return ThemeData.dark().copyWith(
pageTransitionsTheme: pageTransitionsTheme,
colorScheme: _colors,
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(_colors),
bottomAppBarTheme: bottomAppBarTheme(_colors),
bottomNavigationBarTheme: bottomNavigationBarTheme(_colors),
navigationRailTheme: navigationRailTheme(_colors),
tabBarTheme: tabBarTheme(_colors),
drawerTheme: drawerTheme(_colors),
scaffoldBackgroundColor: _colors.background,
useMaterial3: true,
);
}
ThemeMode themeMode() {
return settings.value.themeMode;
}
ThemeData theme(BuildContext context, [Color? targetColor]) {
final brightness = MediaQuery.of(context).platformBrightness;
return brightness == Brightness.light
? light(targetColor)
: dark(targetColor);
}
static ThemeProvider of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ThemeProvider>()!;
}
@override
bool updateShouldNotify(covariant ThemeProvider oldWidget) {
return oldWidget.settings != settings;
}
}
class ThemeSettings {
ThemeSettings({
required this.sourceColor,
required this.themeMode,
});
final Color sourceColor;
final ThemeMode themeMode;
}
Color randomColor() {
return Color(Random().nextInt(0xFFFFFFFF));
}
// Custom Colors
const linkColor = CustomColor(
name: 'Link Color',
color: Color(0xFF00B0FF),
);
class CustomColor {
const CustomColor({
required this.name,
required this.color,
this.blend = true,
});
final String name;
final Color color;
final bool blend;
Color value(ThemeProvider provider) {
return provider.custom(this);
}
}
برای استفاده از ارائهدهنده، یک نمونه ایجاد کنید و آن را به شی موضوع محدودهای در 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 Theme Builder یک ابزار مبتنی بر وب (معرفی شده در 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(
child: widget.child,
// Add from here...
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
),
// ... To here.
),
);
}
}
مواد 3 نقشهای رنگی متفاوتی را معرفی میکند که مکمل یکدیگر هستند و میتوانند در سرتاسر UI برای افزودن لایههای جدید بیان استفاده شوند. این نقش های رنگی جدید عبارتند از:
-
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 super.child,
});
final List<NavigationDestination> destinations;
final int selectedIndex;
final void Function(int index) onDestinationSelected;
final Widget child;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, dimens) {
// Tablet Layout
if (dimens.maxWidth >= 600) { // Add this line
return Scaffold(
body: Row(
children: [
NavigationRail(
extended: dimens.maxWidth >= 800,
minExtendedWidth: 180,
destinations: destinations
.map((e) => NavigationRailDestination(
icon: e.icon,
label: Text(e.label),
))
.toList(),
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
),
Expanded(child: child),
],
),
);
} // Add this line
// Mobile Layout
// Add from here...
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
destinations: destinations,
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
),
);
// ... To here.
},
);
}
}
اندازه همه صفحه نمایش ها یکسان نیست. اگر سعی کردید نسخه دسکتاپ برنامه خود را روی تلفن خود نمایش دهید، باید ترکیبی از چشمک زدن و زوم کردن را انجام دهید تا همه چیز را ببینید. میخواهید برنامهتان بر اساس صفحهای که در آن نمایش داده میشود، ظاهر آن را تغییر دهد. با طراحی واکنشگرا، مطمئن میشوید که برنامهتان روی صفحهنمایشهای هر اندازه عالی به نظر میرسد.
برای اینکه برنامهتان پاسخگو باشد، چند نقطه شکست تطبیقی را معرفی کنید (نباید با نقاط شکست اشکالزدایی اشتباه شود). این نقاط شکست اندازههای صفحهای را که برنامه شما باید طرحبندی خود را تغییر دهد، مشخص میکند.
صفحه نمایش های کوچکتر نمی توانند به اندازه نمایشگرهای بزرگتر بدون کوچک کردن محتوا نمایش دهند. برای اینکه برنامه شبیه یک برنامه دسکتاپ کوچک شده به نظر نرسد، یک طرحبندی جداگانه برای موبایل ایجاد کنید که از برگهها برای تجزیه محتوا استفاده میکند. این به اپلیکیشن حس بومی تری در موبایل می دهد.
روشهای توسعه زیر (تعریف شده در پروژه 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 پیکسل (در طولانی ترین جهت)، اما کوچکتر از 1200 پیکسل، تبلت محسوب می شود. هر چیزی که بزرگتر از 1200 پیکسل باشد دسکتاپ در نظر گرفته می شود. اگر دستگاهی نه تبلت باشد و نه دسکتاپ، موبایل محسوب می شود. میتوانید درباره نقاط شکست تطبیقی در material.io اطلاعات بیشتری کسب کنید. ممکن است از بسته adaptive_breakpoints استفاده کنید.
طرح پاسخگوی صفحه اصلی از AdaptiveContainer
و AdaptiveColumn
بر اساس شبکه 12 ستونی با استفاده از بسته های adaptive_components و adaptive_breakpoints برای پیاده سازی یک طرح شبکه پاسخگو در طراحی متریال استفاده می کند.
return LayoutBuilder(
builder: (context, constraints) {
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 40,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 20,
),
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(15),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 25),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
یک چیدمان تطبیقی به دو طرح نیاز دارد: یکی برای موبایل و یک چیدمان پاسخگو برای صفحه نمایش های بزرگتر. LayoutBuilder
در حال حاضر فقط یک طرح دسکتاپ را برمی گرداند. در lib/src/features/home/view/home_screen.dart
طرح بندی موبایل را به صورت TabBar
و TabBarView
با 4 تب بسازید.
lib/src/features/home/view/home_screen.dart
import 'package:adaptive_components/adaptive_components.dart';
import 'package:flutter/material.dart';
import '../../../shared/classes/classes.dart';
import '../../../shared/extensions.dart';
import '../../../shared/providers/providers.dart';
import '../../../shared/views/views.dart';
import '../../playlists/view/playlist_songs.dart';
import 'view.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
final PlaylistsProvider playlistProvider = PlaylistsProvider();
final List<Playlist> playlists = playlistProvider.playlists;
final Playlist topSongs = playlistProvider.topSongs;
final Playlist newReleases = playlistProvider.newReleases;
final ArtistsProvider artistsProvider = ArtistsProvider();
final List<Artist> artists = artistsProvider.artists;
return LayoutBuilder(
builder: (context, constraints) {
// Add from here...
if (constraints.isMobile) {
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
centerTitle: false,
title: const Text('Good morning'),
actions: const [BrightnessToggle()],
bottom: const TabBar(
isScrollable: true,
tabs: [
Tab(text: 'Home'),
Tab(text: 'Recently Played'),
Tab(text: 'New Releases'),
Tab(text: 'Top Songs'),
],
),
),
body: LayoutBuilder(
builder: (context, constraints) => TabBarView(
children: [
SingleChildScrollView(
child: Column(
children: [
const HomeHighlight(),
HomeArtists(
artists: artists,
constraints: constraints,
),
],
),
),
HomeRecent(
playlists: playlists,
axis: Axis.vertical,
),
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
],
),
),
),
);
}
// ... To here.
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 40,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 20,
),
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(15),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 25),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
مشکلات؟
اگر برنامه شما به درستی اجرا نمیشود، از کد موجود در پیوند زیر استفاده کنید تا به مسیر خود بازگردید.
از فضای خالی استفاده کنید
فضای سفید یک ابزار بصری مهم برای برنامه شما است که یک وقفه سازمانی بین بخش ها ایجاد می کند.
بهتر است فضای خالی بیش از حد وجود داشته باشد تا اینکه کافی نباشد. افزودن فضای خالی بیشتر به کاهش اندازه فونت یا عناصر بصری برای جا دادن بیشتر در فضا ترجیح داده می شود.
کمبود فضای خالی می تواند برای کسانی که مشکلات بینایی دارند مشکل باشد. فضای خالی بیش از حد ممکن است فاقد انسجام باشد و باعث شود که رابط کاربری شما به خوبی سازماندهی نشده به نظر برسد. برای مثال، اسکرین شات های زیر را ببینید:
در مرحله بعد، فضای خالی را به صفحه اصلی اضافه می کنید تا فضای بیشتری به آن بدهید. سپس طرح بندی را برای تنظیم دقیق فاصله ها تغییر می دهید.
یک ویجت را با یک شیء Padding
بپیچید تا فضای خالی اطراف آن ویجت اضافه شود. تمام مقادیر padding موجود در lib/src/features/home/view/home_screen.dart
به 35 افزایش دهید:
lib/src/features/home/view/home_screen.dart
Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.all(35), // Modify this line
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) => PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
// Add spacer between tables
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.all(35), // Modify this line
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) => PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
برنامه را دوباره بارگیری کنید. باید مانند قبل به نظر برسد، اما با فضای خالی بیشتر بین ویجت ها. پد اضافی بهتر به نظر می رسد، اما بنر برجسته در بالا هنوز خیلی به لبه ها نزدیک است.
در lib/src/features/home/view/home_highlight.dart
، بالشتک روی بنر را به 35 تغییر دهید:
lib/src/features/home/view/home_highlight.dart
class HomeHighlight extends StatelessWidget {
const HomeHighlight({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Clickable(
child: SizedBox(
height: 275,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(
'assets/images/news/concert.jpeg',
fit: BoxFit.cover,
),
),
),
onTap: () => launch('https://docs.flutter.dev'),
),
),
),
],
);
}
}
برنامه را دوباره بارگیری کنید. دو لیست پخش در پایین هیچ فضای خالی بین خود ندارند، بنابراین به نظر می رسد که متعلق به یک جدول هستند. اینطور نیست و بعداً آن را برطرف خواهید کرد.
با قرار دادن ویجت اندازه در Row
که حاوی آنهاست، فضای خالی بین لیست های پخش اضافه کنید. در lib/src/features/home/view/home_screen.dart
، یک SizedBox
با عرض 35 اضافه کنید:
lib/src/features/home/view/home_screen.dart
Padding(
padding: const EdgeInsets.all(35),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
],
),
),
const SizedBox(width: 35), // Add this line
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
],
),
),
],
),
),
برنامه را دوباره بارگیری کنید. برنامه باید به شکل زیر باشد:
اکنون فضای زیادی برای محتویات صفحه اصلی وجود دارد، اما همه چیز بسیار جدا به نظر می رسد و هیچ انسجامی بین بخش ها وجود ندارد.
تا کنون، با EdgeInsets.all(35)
همه بالشتکها (افقی و عمودی) را برای ویجتها در صفحه اصلی روی 35 تنظیم کردهاید، اما میتوانید برای هر یک از لبهها نیز بهطور مستقل، padding را تنظیم کنید. بالشتک را به گونه ای سفارشی کنید که مناسب تر با فضا باشد.
-
EdgeInsets.LTRB()
چپ، بالا، راست و پایین را به صورت جداگانه تنظیم می کند -
EdgeInsets.symmetric()
padding را برای عمودی (بالا و پایین) معادل و افقی (چپ و راست) را معادل تنظیم می کند. -
EdgeInsets.only()
فقط لبه های مشخص شده را تنظیم می کند.
Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 25, 20, 10), // Modify this line
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
), // Modify this line
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(
playlists: playlists,
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(15), // Modify this line
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 8, bottom: 8), // Modify this line
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 25),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 8, bottom: 8), // Modify this line
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
در 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 this line
padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 5),
child: Clickable(
child: SizedBox(
height: 275,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(
'assets/images/news/concert.jpeg',
fit: BoxFit.cover,
),
),
),
onTap: () => launch('https://docs.flutter.dev'),
),
),
),
],
);
}
}
برنامه را دوباره بارگیری کنید. طرح و فاصله بسیار بهتر به نظر می رسد! برای لمس نهایی، کمی حرکت و انیمیشن اضافه کنید.
مشکلات؟
اگر برنامه شما به درستی اجرا نمیشود، از کد موجود در پیوند زیر استفاده کنید تا به مسیر خود بازگردید.
7. اضافه کردن حرکت و انیمیشن
حرکت و انیمیشن راههای عالی برای معرفی حرکت و انرژی و ارائه بازخورد در هنگام تعامل کاربر با برنامه هستند.
متحرک سازی بین صفحه نمایش
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(),
},
);
در lib/src/shared/providers/theme.dart، PageTransitionsTheme
به تم های روشن و تاریک منتقل کنید.
lib/src/shared/providers/theme.dart
ThemeData light([Color? targetColor]) {
final _colors = colors(Brightness.light, targetColor);
return ThemeData.light().copyWith(
pageTransitionsTheme: pageTransitionsTheme, // Add this line
colorScheme: ColorScheme.fromSeed(
seedColor: source(targetColor),
brightness: Brightness.light,
),
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(),
tabBarTheme: tabBarTheme(_colors),
scaffoldBackgroundColor: _colors.background,
);
}
ThemeData dark([Color? targetColor]) {
final _colors = colors(Brightness.dark, targetColor);
return ThemeData.dark().copyWith(
pageTransitionsTheme: pageTransitionsTheme, // Add this line
colorScheme: ColorScheme.fromSeed(
seedColor: source(targetColor),
brightness: Brightness.dark,
),
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(),
tabBarTheme: tabBarTheme(_colors),
scaffoldBackgroundColor: _colors.background,
);
}
بدون انیمیشن در iOS
با انیمیشن در iOS
مشکلات؟
اگر برنامه شما به درستی اجرا نمیشود، از کد موجود در پیوند زیر استفاده کنید تا به مسیر خود بازگردید.
حالت های شناور را اضافه کنید
یکی از راههای افزودن حرکت به برنامه دسکتاپ، حالتهای شناور است، جایی که ویجت حالت خود را تغییر میدهد (مانند رنگ، شکل یا محتوا)، زمانی که کاربر مکاننما را روی آن میبرد.
بهطور پیشفرض، کلاس _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.withOpacity(
_hovered ? 0.12 : 0,
),
borderRadius: borderRadius,
),
child: TweenAnimationBuilder<BorderRadius>(
duration: kThemeAnimationDuration,
curve: animationCurve,
tween: Tween(begin: BorderRadius.zero, end: borderRadius),
builder: (context, borderRadius, child) => ClipRRect(
clipBehavior: Clip.antiAlias,
borderRadius: borderRadius,
child: child,
),
child: widget.child,
),
),
);
}
}
برنامه را دوباره بارگیری کنید و سپس روی یکی از کاشیهای لیست پخش اخیراً پخش شده نگه دارید.
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
HoverableSongPlayButton( // Add this line
hoverMode: HoverMode.overlay, // Add this line
song: playlist.songs[index], // Add this line
child: Center( // Modify this line
child: Text(
(index + 1).toString(),
textAlign: TextAlign.center,
),
),
), // Add this line
برنامه را دوباره بارگیری کنید و سپس مکان نما را روی شماره آهنگ در لیست پخش آهنگ های برتر امروز یا لیست پخش جدید نگه دارید.
عدد به یک دکمه پخش تبدیل می شود که با کلیک روی آن آهنگ را پخش می کند.
کد نهایی پروژه را در GitHub ببینید.
8. تبریک می گویم!
شما این کد را تکمیل کردید! شما آموخته اید که تغییرات کوچک زیادی وجود دارد که می توانید آنها را در یک برنامه ادغام کنید تا آن را زیباتر، و همچنین در دسترس تر، بومی سازی تر و برای پلتفرم های مختلف مناسب تر کنید. این تکنیک ها شامل، اما محدود به موارد زیر نیستند:
- تایپوگرافی: متن چیزی بیش از یک ابزار ارتباطی است. از روشی که متن نمایش داده می شود برای ایجاد تأثیر مثبت بر تجربه و درک کاربران از برنامه خود استفاده کنید.
- قالب بندی: یک سیستم طراحی ایجاد کنید که بتوانید بدون نیاز به تصمیم گیری در مورد طراحی برای هر ویجت به طور قابل اعتماد از آن استفاده کنید.
- سازگاری: دستگاه و پلتفرمی که کاربر برنامه شما را روی آن اجرا می کند و قابلیت های آن را در نظر بگیرید. اندازه صفحه و نحوه نمایش برنامه شما را در نظر بگیرید.
- حرکت و انیمیشن: افزودن حرکت به برنامه شما به تجربه کاربر انرژی میافزاید و عملاً بازخوردی را برای کاربران فراهم میکند.
با چند ترفند کوچک برنامه شما می تواند از خسته کننده به زیبا تبدیل شود:
قبل از
بعد از
مراحل بعدی
امیدواریم در مورد ساخت اپلیکیشن های زیبا در Flutter اطلاعات بیشتری کسب کرده باشید!
اگر از هر یک از نکات یا ترفندهای ذکر شده در اینجا استفاده می کنید (یا نکته ای از خودتان برای به اشتراک گذاشتن دارید)، خوشحال می شویم که از شما بشنویم! با ما در توییتر در @rodydavis و @khanhnwin تماس بگیرید!
همچنین ممکن است منابع زیر برای شما مفید باشد.
موضوع بندی
- سازنده تم مواد (ابزار)
منابع تطبیقی و پاسخگو:
- رمزگشایی فلاتر در تطبیق در مقابل پاسخگو (ویدئو)
- طرحبندیهای تطبیقی (ویدیویی از برنامه توسعه فلوتر خسته کننده)
- ایجاد برنامه های پاسخگو و سازگار (flutter.dev)
- اجزای مواد تطبیقی برای فلاتر (کتابخانه در GitHub)
- 5 کاری که می توانید برای آماده سازی برنامه خود برای نمایشگرهای بزرگ انجام دهید (ویدئو از Google I/O 2021)
منابع طراحی عمومی:
- چیزهای کوچک: تبدیل شدن به طراح-توسعهدهنده اسطورهای (ویدئو از Flutter Engage)
- Material Design 3 برای دستگاههای تاشو (material.io)
همچنین، با انجمن Flutter ارتباط برقرار کنید !
به جلو بروید و دنیای برنامه را زیبا کنید!