1. مقدمة
Flutter هو مجموعة أدوات واجهة المستخدم من Google لإنشاء تطبيقات رائعة ومُجمَّعة إلى رموز أصلية للأجهزة الجوّالة والويب وأجهزة الكمبيوتر المكتبي، وذلك من خلال قاعدة رموز برمجية واحدة. يعمل Flutter مع الرموز البرمجية الحالية ويستفيد منه المطوّرون والمؤسسات في جميع أنحاء العالم، وهو مجاني ومفتوح المصدر.
في هذا الدرس التطبيقي حول الترميز، يمكنك تحسين تطبيق الموسيقى في Flutter، ما يجعله مملّاً ورائعًا. لتحقيق ذلك، يستخدم هذا الدرس التطبيقي حول الترميز الأدوات وواجهات برمجة التطبيقات التي تم تقديمها في المادة 3.
ما ستتعرَّف عليه
- طريقة كتابة تطبيق Flutter سهل الاستخدام ورائع على جميع الأنظمة الأساسية
- كيفية تصميم نص في تطبيقك للتأكد من أنه يضيف إلى تجربة المستخدم.
- تعرَّف على كيفية اختيار الألوان المناسبة وتخصيص التطبيقات المصغّرة وإنشاء المظهر الخاص بك واستخدام "الوضع الداكن" بسرعة وسهولة.
- كيفية إنشاء تطبيقات تكيّفية عبر الأنظمة الأساسية.
- كيفية إنشاء تطبيقات تبدو جيدة على أي شاشة.
- طريقة إضافة الحركات إلى تطبيق Flutter لجعله مميزًا
المتطلبات الأساسية:
يفترض هذا الدرس التطبيقي حول الترميز أنّ لديك بعض الخبرة في استخدام Flutter. إذا لم يكن الأمر كذلك، فقد ترغب في معرفة الأساسيات أولاً. الروابط التالية مفيدة:
- جولة حول إطار عمل Flutter Widget
- جرِّب الدرس التطبيقي كتابة أول تطبيق Flutter، الجزء 1 حول الترميز
ما الذي ستقوم ببنائه
يرشدك هذا الدرس التطبيقي حول الترميز خلال عملية إنشاء الشاشة الرئيسية لتطبيق يُعرف باسم MyArtist، وهو تطبيق مشغّل موسيقى يتيح للمعجبين البقاء على اطّلاع على آخر أخبار الفنانين المفضّلين لديهم. حيث يناقش كيفية تعديل تصميم تطبيقك ليبدو جميلاً عبر الأنظمة الأساسية.
توضّح الفيديوهات التالية طريقة عمل التطبيق عند الانتهاء من هذا الدرس التطبيقي حول الترميز:
ما الذي تريد تعلّمه من هذا الدرس التطبيقي حول الترميز؟
2. إعداد بيئة تطوير Flutter
لإكمال هذا التمرين، تحتاج إلى برنامجَين، وهما Flutter SDK ومحرِّر.
يمكنك تشغيل الدرس التطبيقي حول الترميز باستخدام أي من الأجهزة التالية:
- جهاز Android أو iOS فعلي متصل بجهاز الكمبيوتر وتم ضبطه على "وضع مطور البرامج".
- محاكي iOS (يتطلب تثبيت أدوات Xcode).
- محاكي Android (يتطلب عملية إعداد في "استوديو Android").
- متصفّح (يجب توفُّر متصفّح Chrome لتصحيح الأخطاء)
- كتطبيق سطح المكتب الذي يعمل بنظام التشغيل Windows أو Linux أو macOS. يجب إجراء تطوير على النظام الأساسي الذي تخطّط لنشر الإعلان عليه. لذا، إذا كنت ترغب في تطوير تطبيق سطح مكتب Windows، ينبغي لك تطويره على Windows للوصول إلى سلسلة الإصدار المناسبة. هناك متطلبات خاصة بنظام التشغيل تم تناولها بالتفصيل على 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
وينفّذ أداة ذات حالة باستخدام فئتَين:
- تتيح عبارة
import
استخدام Material Components. - تمثّل الفئة
HomeScreen
الصفحة المعروضة بالكامل. - تنشئ الطريقة
build()
للفئة_HomeScreenState
جذر شجرة التطبيقات المصغّرة، ما يؤثر في كيفية إنشاء جميع التطبيقات المصغّرة في واجهة المستخدم.
4. الاستفادة من أسلوب الخط
النص متوفّر في كل مكان يعد النص طريقة مفيدة للتواصل مع المستخدم. هل يهدف تطبيقك إلى أن يكون ودودًا وممتعًا، أو ربما جديرًا بالثقة واحترافيًا؟ هناك سبب لعدم استخدام تطبيق Comic Sans المصرفي المفضّل لديك. وتشكّل طريقة عرض النص الانطباع الأول للمستخدم عن تطبيقك. فيما يلي بعض الطرق لاستخدام النص بعناية.
مشاهدة المحتوى بدلاً من الإخبار
حيثما أمكن، قم بـ "عرض" بدلاً من "إخبار". على سبيل المثال، يتضمّن NavigationRail
في تطبيق إجراء التفعيل علامات تبويب لكل مسار رئيسي، إلا أنّ الرموز البادئة متطابقة:
وهذا غير مفيد لأن المستخدم لا يزال عليه قراءة نص كل علامة تبويب. ابدأ بإضافة إشارات مرئية حتى يتمكن المستخدم من إلقاء نظرة سريعة على الأيقونات البادئة للعثور على علامة التبويب المطلوبة. ويساعد هذا أيضًا في الترجمة وسهولة الوصول.
في lib/src/shared/router.dart
، أضِف رموزًا بادئة مميزة لكل وجهة تنقُّل (الصفحة الرئيسية وقائمة التشغيل والمستخدمين):
lib/src/shared/router.dart
const List<NavigationDestination> destinations = [
NavigationDestination(
label: 'Home',
icon: Icon(Icons.home), // Modify this line
route: '/',
),
NavigationDestination(
label: 'Playlists',
icon: Icon(Icons.playlist_add_check), // Modify this line
route: '/playlists',
),
NavigationDestination(
label: 'Artists',
icon: Icon(Icons.people), // Modify this line
route: '/artists',
),
];
هل مِن مشاكل؟
إذا كان تطبيقك لا يعمل بشكل صحيح، ابحث عن الأخطاء الإملائية. إذا لزم الأمر، يمكنك استخدام الرمز المتوفّر في الروابط التالية للعودة إلى المسار الصحيح.
اختيار الخطوط بعناية
تحدد الخطوط طبيعة تطبيقك، لذا فإن اختيار الخط المناسب أمر بالغ الأهمية. عند تحديد خط، إليك بعض الأشياء التي يجب مراعاتها:
- Sans-serif أو serif: خطوط Serif بها خطوط زخرفية أو "ذيل" في نهاية الخطابات ويُنظر إليها على أنها أكثر رسمية. لا تحتوي خطوط Sans-serif على خطوط زخرفية وتميل إلى اعتبارها أكثر رسمية. حرف T كبير بدون ذنابة وحرف T كبير
- الأحرف الكبيرة (في حال الكتابة بلغة أجنبية): من المناسب استخدام أحرف كبيرة بالكامل لجذب الانتباه إلى كميّات صغيرة من النص (مثلاً في العناوين الرئيسية)، ولكن عند الإفراط في استخدام الخطوط، قد يتم اعتبارها صياحًا مما يجعل المستخدم يتجاهلها تمامًا.
- حالة أحرف العنوان أو حالة الجملة: عند إضافة عناوين أو تصنيفات، عليك مراعاة كيفية استخدام الأحرف الكبيرة: حالة العنوان، حيث تتم كتابة الحرف الأول من كل كلمة ("This Is a Title case Title") بأكثر رسمية. إنّ حالة أحرف الجملة التي لا تضمّن سوى الأسماء المناسبة والكلمة الأولى في النص ("This is a sentence case title") هي أكثر تحاورية وغير رسمية.
- الأحرف (التباعد بين كل حرف) وطول السطر (عرض النص الكامل على الشاشة) وارتفاع السطر (طول كل سطر من النص): يؤدي استخدام الكثير أو القليل جدًا من أي مما يلي إلى جعل تطبيقك أقل قابلية للقراءة. على سبيل المثال، من السهل أن تفقد المكان عند قراءة جزء نصي كبير وغير منقطع.
مع أخذ ذلك في الاعتبار، يمكنك الانتقال إلى 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
الجديدة إلى جانب النص المعروض بخط "مونتسرات".
هل مِن مشاكل؟
إذا كان تطبيقك لا يعمل بشكل صحيح، ابحث عن الأخطاء الإملائية. إذا لزم الأمر، يمكنك استخدام الرمز المتوفّر في الروابط التالية للعودة إلى المسار الصحيح.
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.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 أدوارًا دقيقة للألوان تتكامل مع بعضها البعض ويمكن استخدامها في جميع أنحاء واجهة المستخدم لإضافة طبقات تعبير جديدة. تتضمن أدوار الألوان الجديدة هذه ما يلي:
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 لتنفيذ تنسيق شبكة سريع الاستجابة في Material Design.
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
لإضافة مسافة بيضاء حوله. زيادة كل قيم المساحة المتروكة حاليًا في 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,
),
],
),
),
],
),
),
من المهم إعادة تحميل التطبيق. من المفترض أن يظهر التطبيق على النحو التالي:
هناك الآن مساحة كبيرة لمحتوى الشاشة الرئيسية، ولكن يبدو أن كل شيء مفصول جدًا ولا يوجد ترابط بين الأقسام.
حتى الآن، تم ضبط كل المساحة المتروكة (أفقية وعمودية) للتطبيقات المصغّرة على الشاشة الرئيسية على 35 باستخدام EdgeInsets.all(35)
، ولكن يمكنك أيضًا ضبط المساحة المتروكة لكل حافة على حدة. يمكنك تخصيص المساحة المتروكة لتناسب المساحة بشكل أفضل.
- ضبط
EdgeInsets.LTRB()
على اليسار واليمين وأعلى اليمين وأسفل بشكل فردي - تضبط
EdgeInsets.symmetric()
المساحة المتروكة (أعلى وأسفل) مكافئة وأفقية (لليسار واليمين) مكافئة. - تضبط
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(),
},
);
مرِّر PageTransitionsTheme
إلى كلٍّ من المظهرَين الفاتح والداكن في lib/src/shared/providers/theme.dart.
lib/src/shared/providers/theme.dart
ThemeData light([Color? targetColor]) {
final _colors = colors(Brightness.light, targetColor);
return ThemeData.light().copyWith(
pageTransitionsTheme: pageTransitionsTheme, // Add this line
colorScheme: ColorScheme.fromSeed(
seedColor: source(targetColor),
brightness: Brightness.light,
),
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(),
tabBarTheme: tabBarTheme(_colors),
scaffoldBackgroundColor: _colors.background,
);
}
ThemeData dark([Color? targetColor]) {
final _colors = colors(Brightness.dark, targetColor);
return ThemeData.dark().copyWith(
pageTransitionsTheme: pageTransitionsTheme, // Add this line
colorScheme: ColorScheme.fromSeed(
seedColor: source(targetColor),
brightness: Brightness.dark,
),
appBarTheme: appBarTheme(_colors),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(),
tabBarTheme: tabBarTheme(_colors),
scaffoldBackgroundColor: _colors.background,
);
}
بدون صور متحركة على 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.
إذا طبقت أيًا من النصائح أو الحيل المذكورة هنا (أو كانت لديك نصيحة خاصة بك لمشاركتها)، يسرّنا أن نسمع منك. يمكنك التواصل معنا على Twitter على @rodydavis و@khanhnwin.
ويمكنك أيضًا الاطّلاع على المراجع التالية المفيدة.
المظهر
- أداة إنشاء مظاهر المواد (أداة)
الموارد التكيُّفية والسريعة الاستجابة:
- فك ترميز Flutter باستخدام الميزات التكيّفية أو المتجاوبة مع مختلف الأجهزة (فيديو)
- التنسيقات التكيُّفية (فيديو من برنامج The Boring Flutter Development Show)
- إنشاء تطبيقات متجاوبة وتكيّفية (flutter.dev)
- مكوّنات المواد التكيُّفية في Flutter (مكتبة على GitHub)
- 5 إجراءات يمكنك اتّخاذها لإعداد تطبيقك للشاشات الكبيرة (فيديو من مؤتمر Google I/O لعام 2021)
موارد التصميم العامة:
- الجوانب الصغيرة: الوصول إلى المطوّرين الأسطوريين (فيديو من Flutter Engage)
- التصميم المتعدد الأبعاد 3 للأجهزة القابلة للطي (material.io)
ويمكنك أيضًا التواصل مع منتدى Flutter.
انطلِق واجعل عالم التطبيق جميلاً!