لمحة عن هذا الدرس التطبيقي حول الترميز
1. مقدمة
Flutter هي مجموعة أدوات واجهة المستخدم من Google لإنشاء تطبيقات محلية ومميّزة للأجهزة الجوّالة والويب وأجهزة الكمبيوتر المكتبي من خلال قاعدة رموز برمجية واحدة. يعمل Flutter مع الرموز البرمجية الحالية، ويستخدمه المطوّرون والمؤسسات في جميع أنحاء العالم، وهو مجاني ومفتوح المصدر.
في هذا الدرس التطبيقي حول الترميز، يمكنك تحسين تطبيق موسيقى في Flutter، ما يجعله أكثر روعة. ولتحقيق ذلك، يستخدم هذا الدرس التطبيقي الأدوات وواجهات برمجة التطبيقات التي تم تقديمها في Material 3.
المُعطيات
- كيفية كتابة تطبيق Flutter سهل الاستخدام وجميل على جميع المنصات
- كيفية تصميم النصوص في تطبيقك للتأكّد من أنّها تساهم في تحسين تجربة المستخدم
- كيفية اختيار الألوان المناسبة وتخصيص التطبيقات المصغّرة وإنشاء مظهرك الخاص وتطبيق الوضع الداكن بسرعة
- كيفية إنشاء تطبيقات قابلة للتكيّف على جميع المنصات
- كيفية إنشاء تطبيقات تبدو رائعة على أي شاشة
- كيفية إضافة حركة إلى تطبيق Flutter لإضفاء لمسة مميزة عليه
المتطلبات الأساسية
يفترض هذا الدرس التطبيقي حول الترميز أنّ لديك بعض الخبرة في استخدام Flutter. إذا لم تكن كذلك، ننصحك بمعرفة الأساسيات أولاً. يمكنك الاطّلاع على الروابط التالية للحصول على مزيد من المعلومات:
- اطّلِع على مقالة إنشاء واجهات مستخدم باستخدام Flutter.
- جرِّب الدرس التطبيقي تطبيقك الأول باستخدام Flutter.
ما ستُنشئه
يرشدك هذا الدرس التطبيقي إلى كيفية إنشاء الشاشة الرئيسية لتطبيق يُسمى MyArtist
، وهو تطبيق مشغّل موسيقى يتيح للمعجبين الاطّلاع على آخر أخبار الفنّانين المفضّلين لديهم. ويناقش هذا الدليل كيفية تعديل تصميم تطبيقك ليبدو جميلًا على جميع المنصات.
توضّح الفيديوهات التالية طريقة عمل التطبيق عند الانتهاء من هذا الدليل التعليمي:
ما الذي تريد تعلّمه من هذا الدليل التعليمي حول البرمجة؟
2. إعداد بيئة تطوير Flutter
تحتاج إلى برنامجَين لإكمال هذا الدرس التطبيقي، وهما حزمة تطوير البرامج (SDK) من Flutter ومحرِّر.
يمكنك تشغيل ورشة التعلم البرمجي باستخدام أيّ من الأجهزة التالية:
- جهاز Android أو iOS متصل بالكمبيوتر ومفعَّل فيه وضع المطوّر
- محاكي iOS (يتطلب تثبيت أدوات Xcode)
- محاكي Android (يتطلب الإعداد في "استوديو Android")
- متصفّح (يجب استخدام Chrome لتصحيح الأخطاء)
- كتطبيق سطح مكتب Windows أو Linux أو macOS يجب إجراء عملية التطوير على المنصة التي تريد نشر التطبيق عليها. لذلك، إذا أردت تطوير تطبيق مخصّص لأجهزة الكمبيوتر المكتبي التي تعمل بنظام التشغيل Windows، عليك إجراء عملية التطوير على نظام التشغيل Windows للوصول إلى سلسلة الإنشاء المناسبة. هناك متطلبات خاصة بنظام التشغيل يتم تناولها بالتفصيل على docs.flutter.dev/desktop.
3. الحصول على تطبيق Codelab Starter
استنساخه من GitHub
لنسخ هذا الدليل التعليمي من 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. - تمثّل فئة
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',
),
];
هل لديك مشاكل؟
إذا كان تطبيقك لا يعمل بشكل صحيح، ابحث عن أخطاء إملائية. إذا لزم الأمر، يمكنك استخدام الرمز المتوفّر في الروابط التالية للعودة إلى المسار الصحيح.
اختيار الخطوط بعناية
تحدّد الخطوط شخصية تطبيقك، لذا من المهم اختيار الخط المناسب. عند اختيار خط، إليك بعض النقاط التي يجب مراعاتها:
- الخطّ المُسطّح أو الخطّ المُسنّد: تحتوي الخطوط المُسنّدة على خطوط زخرفية أو "ذيول" في نهاية الأحرف، ويُنظر إليها على أنّها أكثر رسمية. لا تحتوي الخطوط غير المزوّدة بزخارف على الخطوط الزخرفية، ويُنظر إليها عادةً على أنّها أكثر رسمية.
- الخطوط التي تستخدم أحرفًا كبيرة بالكامل: إنّ استخدام أحرف كبيرة بالكامل مناسب لجذب الانتباه إلى أجزاء صغيرة من النص (مثل العناوين)، ولكن عند الإفراط في استخدامه، قد يُنظر إليه على أنّه صراخ، ما يدفع المستخدم إلى تجاهله تمامًا.
- حالة أحرف العنوان أو حالة أحرف الجملة: عند إضافة عناوين أو تصنيفات، ننصحك بالتفكير في كيفية استخدام الأحرف الكبيرة: حالة أحرف العنوان، حيث يتم كتابة الحرف الأول من كل كلمة بحرف كبير ("This Is a Title Case Title")، هي أكثر رسمية. حالة أحرف الجملة، التي تستخدم الأحرف الكبيرة في أسماء الأعلام والكلمة الأولى فقط في النص ("This is a sentence case title")، هي أكثر أسلوبًا حواريًا وغير رسمي.
- مقياس التداخل (المسافة بين كل حرف) وطول السطر (عرض النص الكامل على الشاشة) وارتفاع السطر (ارتفاع كل سطر من النص): إنّ زيادة أيٍّ من هذه العناصر أو نقصانه يجعل تطبيقك أقل سهولة في القراءة. على سبيل المثال، قد يكون من الصعب تذكر الموضع الذي وصلت إليه عند قراءة كتلة كبيرة من النص غير المنقطع.
مع أخذ ذلك في الاعتبار، انتقِل إلى "خطوط Google" واختَر خطًا بدون خطوط مُسنّنة، مثل Montserrat، لأنّ تطبيق الموسيقى مخصّص للترفيه والمرح.
من سطر الأوامر، استخدِم الأمر pull لتحميل حزمة 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.
ضبط Montserrat TextTheme:
TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line
أعِد تحميل فورًا لتفعيل التغييرات. (استخدِم الزر في بيئة تطوير البرامج المتكاملة أو أدخِل
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 Design واستكشِف ألوانًا مختلفة لواجهة المستخدم. من المهم اختيار لون يناسب المظهر الجمالي للعلامة التجارية أو يناسب ذوقك الشخصي.
بعد إنشاء مظهر، انقر بزر الماوس الأيمن على فقاعة اللون الأساسي، سيؤدي ذلك إلى فتح مربّع حوار يحتوي على القيمة الست عشرية للّون الأساسي. انسخ هذه القيمة. (يمكنك أيضًا ضبط اللون باستخدام مربّع الحوار هذا).
نقْل القيمة السداسية العشرية للّون الأساسي إلى مقدّم المظهر على سبيل المثال، يتم تحديد اللون الست عشري #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 3 أدوارًا دقيقة للألوان تكمل بعضها البعض ويمكن استخدامها في جميع أنحاء واجهة المستخدم لإضافة طبقات جديدة من التعبير. تشمل أدوار الألوان الجديدة ما يلي:
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 بكسل (في أطول اتجاه)، ولكنّها أصغر من 1200 بكسل، شاشة جهاز لوحي. ويُعتبر أيّ حجم أكبر من 1200 بكسل كمبيوتر مكتبي. إذا لم يكن الجهاز جهازًا لوحيًا أو كمبيوترًا مكتبيًا، يتم اعتباره جهازًا جوّالاً. يمكنك الاطّلاع على مزيد من المعلومات عن نقاط التوقف التكيُّفية على 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,
),
),
],
),
),
],
),
),
),
],
),
),
);
أعِد تحميل التطبيق بسرعة. من المفترض أن يبدو كما كان من قبل، ولكن مع مساحة بيضاء أكبر بين التطبيقات المصغّرة. يبدو الشريط الإضافي للتباعد أفضل، ولكنّ بانر "أهم التفاصيل" في أعلى الصفحة لا يزال قريبًا جدًا من الحواف.
في 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())),
],
),
أعِد تحميل التطبيق بسرعة، ثم مرِّر مؤشر الماوس فوق رقم الأغنية في قائمة أبرز الأغاني اليوم أو قائمة الإصدارات الجديدة.
يتحول الرقم إلى صورة متحركة لزر تشغيل يشغّل الأغنية عند النقر عليه.
اطّلِع على رمز المشروع النهائي على GitHub.
10. تهانينا!
لقد أكملت هذا الدليل التعليمي حول البرمجة. لقد تعرّفت على العديد من التغييرات الصغيرة التي يمكنك دمجها في التطبيق لجعله أكثر جمالًا، وأكثر سهولة في الاستخدام وقابلية للترجمة والأقلمة، وأكثر ملاءمةً لمنصّات متعددة. وتشمل هذه الأساليب، على سبيل المثال لا الحصر:
- أسلوب الخط: النص هو أكثر من مجرد أداة للتواصل. استخدِم طريقة عرض النص لترك تأثير إيجابي على تجربة المستخدمين ونظرتهم إلى تطبيقك.
- المظاهر: يمكنك إنشاء نظام تصميم يمكنك استخدامه بشكل موثوق بدون الحاجة إلى اتخاذ قرارات تصميمية لكل تطبيق مصغّر.
- التكيّف: يجب مراعاة الجهاز والنظام الأساسي الذي يستخدمهما المستخدم لتشغيل تطبيقك وقدراتهما. يجب مراعاة حجم الشاشة وطريقة عرض تطبيقك.
- الحركة والرسوم المتحركة: تضيف الحركة إلى تطبيقك طاقة إلى تجربة المستخدم، كما توفّر ملاحظات للمستخدمين بشكل عملي أكثر.
من خلال إجراء بعض التعديلات الصغيرة، يمكنك تحويل تطبيقك من تطبيق ممل إلى تطبيق جميل:
قبل
بعد
الخطوات التالية
نأمل أن تكون قد تعرّفت على مزيد من المعلومات حول إنشاء تطبيقات رائعة في Flutter.
إذا طبّقت أيًا من النصائح أو الحيل المذكورة هنا (أو إذا كانت لديك نصيحة تريد مشاركتها)، يسعدنا معرفة رأيك. يمكنك التواصل معنا على Twitter على @rodydavis و@khanhnwin.
يمكنك أيضًا الاطّلاع على المراجع التالية.
المظهر
- Material Theme Builder (أداة)
المراجع التكيُّفية وتلك السريعة الاستجابة:
- فك ترميز Flutter على Adaptive vs Responsive (فيديو)
- التنسيقات التكيُّفية (فيديو من The Boring Flutter Development Show)
- إنشاء تطبيقات متجاوبة وقابلة للتكيّف (flutter.dev)
- مكوّنات Material المتوافقة مع Flutter (مكتبة على GitHub)
- 5 إجراءات يمكنك اتّخاذها لإعداد تطبيقك للأجهزة التي تتضمّن شاشات كبيرة (فيديو من مؤتمر Google I/O لعام 2021)
مراجع التصميم العامة:
- الأشياء الصغيرة: أن تصبح مصمّمًا ومطوّرًا في آنٍ واحد (فيديو من Flutter Engage)
- Material Design 3 للأجهزة القابلة للطي (material.io)
يمكنك أيضًا التواصل مع منتدى Flutter.
نتمنّى لك التوفيق في إنشاء تطبيقات جميلة.