استخدِم تطبيق Flutter كالملل لإنشاء مظهر جميل.

1. مقدمة

Flutter هو مجموعة أدوات واجهة المستخدم من Google لإنشاء تطبيقات رائعة ومُجمَّعة إلى رموز أصلية للأجهزة الجوّالة والويب وأجهزة الكمبيوتر المكتبي، وذلك من خلال قاعدة رموز برمجية واحدة. يعمل Flutter مع الرموز البرمجية الحالية ويستفيد منه المطوّرون والمؤسسات في جميع أنحاء العالم، وهو مجاني ومفتوح المصدر.

في هذا الدرس التطبيقي حول الترميز، يمكنك تحسين تطبيق الموسيقى في Flutter، ما يجعله مملّاً ورائعًا. لتحقيق ذلك، يستخدم هذا الدرس التطبيقي حول الترميز الأدوات وواجهات برمجة التطبيقات التي تم تقديمها في المادة 3.

ما ستتعرَّف عليه

  • طريقة كتابة تطبيق Flutter سهل الاستخدام ورائع على جميع الأنظمة الأساسية
  • كيفية تصميم نص في تطبيقك للتأكد من أنه يضيف إلى تجربة المستخدم.
  • تعرَّف على كيفية اختيار الألوان المناسبة وتخصيص التطبيقات المصغّرة وإنشاء المظهر الخاص بك واستخدام "الوضع الداكن" بسرعة وسهولة.
  • كيفية إنشاء تطبيقات تكيّفية عبر الأنظمة الأساسية.
  • كيفية إنشاء تطبيقات تبدو جيدة على أي شاشة.
  • طريقة إضافة الحركات إلى تطبيق Flutter لجعله مميزًا

المتطلبات الأساسية:

يفترض هذا الدرس التطبيقي حول الترميز أنّ لديك بعض الخبرة في استخدام Flutter. إذا لم يكن الأمر كذلك، فقد ترغب في معرفة الأساسيات أولاً. الروابط التالية مفيدة:

ما الذي ستقوم ببنائه

يرشدك هذا الدرس التطبيقي حول الترميز خلال عملية إنشاء الشاشة الرئيسية لتطبيق يُعرف باسم 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) واستخدام أدواته لتشغيل التطبيق.

a3c16fc17be25f6c.png تشغيل التطبيق

اكتمال عملية النقل بنجاح يجب أن يكون رمز إجراء التفعيل للشاشة الرئيسية لتطبيق MyArtist قيد التشغيل. من المفترض أن تظهر لك شاشة MyArtist الرئيسية. تظهر الإعلانات على الكمبيوتر المكتبي على الرغم من أنّ الأجهزة الجوّالة هي... ليس رائعًا. لسبب واحد، إنه لا يحترم الدرجة الأولى. لا تقلق، ستصلح هذه المشكلة.

1e67c60667821082.png d1139cde225de452.png

التعرّف على الرمز

بعد ذلك، قم بجولة في التعليمات البرمجية.

افتح 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 في تطبيق إجراء التفعيل علامات تبويب لكل مسار رئيسي، إلا أنّ الرموز البادئة متطابقة:

86c5f73b3aa5fd35.png

وهذا غير مفيد لأن المستخدم لا يزال عليه قراءة نص كل علامة تبويب. ابدأ بإضافة إشارات مرئية حتى يتمكن المستخدم من إلقاء نظرة سريعة على الأيقونات البادئة للعثور على علامة التبويب المطلوبة. ويساعد هذا أيضًا في الترجمة وسهولة الوصول.

a3c16fc17be25f6c.png في 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',
  ),
];

23278e4f4610fbf4.png

هل مِن مشاكل؟

إذا كان تطبيقك لا يعمل بشكل صحيح، ابحث عن الأخطاء الإملائية. إذا لزم الأمر، يمكنك استخدام الرمز المتوفّر في الروابط التالية للعودة إلى المسار الصحيح.

اختيار الخطوط بعناية

تحدد الخطوط طبيعة تطبيقك، لذا فإن اختيار الخط المناسب أمر بالغ الأهمية. عند تحديد خط، إليك بعض الأشياء التي يجب مراعاتها:

  • Sans-serif أو serif: خطوط Serif بها خطوط زخرفية أو "ذيل" في نهاية الخطابات ويُنظر إليها على أنها أكثر رسمية. لا تحتوي خطوط Sans-serif على خطوط زخرفية وتميل إلى اعتبارها أكثر رسمية. 34bf54e4cad90101.png حرف T كبير بدون ذنابة وحرف T كبير
  • الأحرف الكبيرة (في حال الكتابة بلغة أجنبية): من المناسب استخدام أحرف كبيرة بالكامل لجذب الانتباه إلى كميّات صغيرة من النص (مثلاً في العناوين الرئيسية)، ولكن عند الإفراط في استخدام الخطوط، قد يتم اعتبارها صياحًا مما يجعل المستخدم يتجاهلها تمامًا.
  • حالة أحرف العنوان أو حالة الجملة: عند إضافة عناوين أو تصنيفات، عليك مراعاة كيفية استخدام الأحرف الكبيرة: حالة العنوان، حيث تتم كتابة الحرف الأول من كل كلمة ("This Is a Title case Title") بأكثر رسمية. إنّ حالة أحرف الجملة التي لا تضمّن سوى الأسماء المناسبة والكلمة الأولى في النص ("This is a sentence case title") هي أكثر تحاورية وغير رسمية.
  • الأحرف (التباعد بين كل حرف) وطول السطر (عرض النص الكامل على الشاشة) وارتفاع السطر (طول كل سطر من النص): يؤدي استخدام الكثير أو القليل جدًا من أي مما يلي إلى جعل تطبيقك أقل قابلية للقراءة. على سبيل المثال، من السهل أن تفقد المكان عند قراءة جزء نصي كبير وغير منقطع.

مع أخذ ذلك في الاعتبار، يمكنك الانتقال إلى Google Fonts واختيار خط sans-serif، مثل Montserrat، لأنّ تطبيق الموسيقى يهدف إلى أن يكون ممتعًا ومرحًا.

a3c16fc17be25f6c.png من سطر الأوامر، اسحب حزمة 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>

a3c16fc17be25f6c.png في lib/src/shared/extensions.dart، استورِد الحزمة الجديدة:

lib/src/shared/extensions.dart

import 'package:google_fonts/google_fonts.dart';  // Add this line.

a3c16fc17be25f6c.png تعيين جبل مونتسرات TextTheme:

TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line

a3c16fc17be25f6c.png إعادة التحميل السريع 7f9a9e103c7b5e5.png لتفعيل التغييرات. (استخدِم الزرّ في بيئة التطوير المتكاملة (IDE) أو من سطر الأوامر، أدخِل r لإعادة التحميل بشكل سريع).

1e67c60667821082.png

من المفترض أن تظهر رموز 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);
 }
}

a3c16fc17be25f6c.pngلاستخدام الموفِّر، يمكنك إنشاء مثيل وتمريره إلى كائن المظهر ذي النطاق الخاص في 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) تساعدك في اختيار مجموعة من الألوان التكميلية لتطبيقك.

a3c16fc17be25f6c.pngلاختيار لون مصدر للتطبيق، افتح Material Theme Builder، واستكشِف ألوانًا مختلفة لواجهة المستخدم. من المهم تحديد لون يناسب الجمالية للعلامة التجارية و/أو تفضيلاتك الشخصية.

بعد إنشاء مظهر، انقر بزر الماوس الأيمن على فقاعة اللون الأساسي، وسيؤدي ذلك إلى فتح مربّع حوار يحتوي على القيمة السداسية العشرية للون الأساسي. انسخ هذه القيمة. (يمكنك أيضًا ضبط اللون باستخدام مربع الحوار هذا).

a3c16fc17be25f6c.pngتمرير القيمة السداسية العشرية للون الأساسي إلى موفّر المظهر. على سبيل المثال، يتم تحديد اللون الست عشري #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;

a3c16fc17be25f6c.pngلاستخدام لون معيّن، يمكنك الوصول إلى دور اللون على 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

بالإضافة إلى ذلك، تتوافق الرموز المميّزة الجديدة للتصميم مع المظهرَين الفاتح والداكن:

7b51703ed96196a4.png

يمكن استخدام أدوار الألوان هذه لتعيين المعنى والتوكيد على أجزاء مختلفة من واجهة المستخدم. حتى إذا لم يكن أحد المكونات بارزًا، لا يزال بإمكانه الاستفادة من اللون الديناميكي.

a3c16fc17be25f6c.png يمكن للمستخدم ضبط سطوع التطبيق من خلال إعدادات نظام الجهاز. عند ضبط الجهاز على الوضع الداكن في 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.

ضع في اعتبارك الاختلافات التالية على نظام التشغيل عند إنشاء تطبيق تكيُّفي من عدّة أنظمة أساسية:

  • طريقة إرسال البيانات: الماوس أو اللمس أو جهاز التحكّم في الألعاب
  • حجم الخط واتجاه الجهاز ومسافة العرض
  • حجم الشاشة وشكلها: الهاتف، والجهاز اللوحي، والأجهزة القابلة للطي، والكمبيوتر المكتبي، والويب

a3c16fc17be25f6c.png يحتوي ملف 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.
      },
    );
  }
}

a8487a3c4d7890c9.png

ليست جميع الشاشات بنفس الحجم. إذا حاولت عرض إصدار سطح المكتب من تطبيقك على هاتفك، عليك القيام بمزيج من النظر والتكبير لرؤية كل شيء. تريد من تطبيقك تغيير مظهره استنادًا إلى الشاشة التي يظهر فيها. بفضل التصميم سريع الاستجابة، يمكنك ضمان ظهور تطبيقك بشكل رائع على الشاشات من جميع الأحجام.

لجعل تطبيقك متجاوبًا، قدِّم بعض نقاط التوقف التكيُّفية (يجب عدم الخلط بينها وبين نقاط الإيقاف لتصحيح الأخطاء). تحدِّد نقاط الإيقاف هذه أحجام الشاشة التي يجب أن يغيّر التطبيق تنسيقها فيها.

لا يمكن عرض الشاشات الأصغر حجمًا بحجم الشاشات الأكبر بدون تصغير المحتوى. لمنع التطبيق من أن يبدو كتطبيق متوافق مع أجهزة الكمبيوتر المكتبي تم تصغيره، يمكنك إنشاء تنسيق منفصل للأجهزة الجوّالة يستخدم علامات التبويب لتقسيم المحتوى. ويمنح ذلك التطبيق طابعًا مدمجًا مع المحتوى على الأجهزة الجوّالة.

تُعدّ طرق الإضافات التالية (المحدّدة في مشروع 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,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );

a3c16fc17be25f6c.pngيحتاج التنسيق التكيُّفي إلى تنسيقَين: أحدهما للأجهزة الجوّالة والآخر للشاشات الأكبر حجمًا. تعرض 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,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
     },
   );
 }
}

377cfdda63a9de54.png

هل مِن مشاكل؟

إذا كان تطبيقك لا يعمل بشكل صحيح، يمكنك استخدام الرمز الوارد في الرابط التالي للعودة إلى المسار الصحيح.

استخدام مسافة بيضاء

المسافات البيضاء هي أداة مرئية مهمة لتطبيقك، حيث تؤدي إلى فاصل تنظيمي بين الأقسام.

من الأفضل أن تكون لديك مسافات بيضاء كثيرة جدًا وليس كافية. يُفضل إضافة المزيد من المسافات البيضاء على تقليل حجم الخط أو العناصر المرئية لتلائم بشكل أكبر المساحة.

يمكن أن يكون عدم وجود مسافات بيضاء صعبًا على الأشخاص الذين يعانون من مشكلات في الرؤية. يمكن أن تفتقر المسافة البيضاء الكبيرة جدًا إلى التماسك وتجعل واجهة المستخدم تبدو منظمة بشكل سيئ. على سبيل المثال، يمكنك الاطّلاع على لقطات الشاشة التالية:

7f5e3514a7ee1750.png

d5144a50f5b4142c.png

بعد ذلك، ستضيف مسافة بيضاء إلى الشاشة الرئيسية لمنحها مساحة أكبر. ويمكنك بعد ذلك تعديل التصميم لضبط التباعد بدقة.

a3c16fc17be25f6c.png التفاف التطبيق المصغّر مع عنصر 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,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );

a3c16fc17be25f6c.png من المهم إعادة تحميل التطبيق. من المفترض أن تبدو مماثلة كما كانت من قبل، ولكن مع مزيد من المسافات البيضاء بين الأدوات. تبدو المساحة المتروكة الإضافية أفضل، إلا أن بانر التمييز في الأعلى لا يزال قريبًا جدًا من الحواف.

a3c16fc17be25f6c.png في 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'),
            ),
          ),
        ),
      ],
    );
  }
}

a3c16fc17be25f6c.png من المهم إعادة تحميل التطبيق. لا تتضمن قائمتا التشغيل في الأسفل مسافة بيضاء، لذا يبدو أنهما ينتميان إلى الجدول نفسه. ليس الأمر كذلك، وستصلحون ذلك بعد ذلك.

df1d9af97d039cc8.png

a3c16fc17be25f6c.png أضِف مسافة بيضاء بين قوائم التشغيل من خلال إدراج تطبيق مصغّر للحجم في 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,
            ),
          ],
        ),
      ),
    ],
  ),
),

a3c16fc17be25f6c.png من المهم إعادة تحميل التطبيق. من المفترض أن يظهر التطبيق على النحو التالي:

d8b2a3d47736dbab.png

هناك الآن مساحة كبيرة لمحتوى الشاشة الرئيسية، ولكن يبدو أن كل شيء مفصول جدًا ولا يوجد ترابط بين الأقسام.

a3c16fc17be25f6c.png حتى الآن، تم ضبط كل المساحة المتروكة (أفقية وعمودية) للتطبيقات المصغّرة على الشاشة الرئيسية على 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,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );

a3c16fc17be25f6c.png في 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'),
            ),
          ),
        ),
      ],
    );
  }
}

a3c16fc17be25f6c.png من المهم إعادة تحميل التطبيق. يبدو التنسيق والتباعد أفضل بكثير! للحصول على اللمسة الأخيرة، أضف بعض الحركة والرسوم المتحركة.

7f5e3514a7ee1750.png

هل مِن مشاكل؟

إذا كان تطبيقك لا يعمل بشكل صحيح، يمكنك استخدام الرمز الوارد في الرابط التالي للعودة إلى المسار الصحيح.

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(),
  },
);

a3c16fc17be25f6c.png مرِّر 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 الذي يحوّل سهم المؤشر إلى مؤشر عند التمرير، ولكن يمكنك إضافة المزيد من الملاحظات المرئية.

a3c16fc17be25f6c.png افتح 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,
        ),
      ),
    );
  }
}

a3c16fc17be25f6c.png أعِد تحميل التطبيق مباشرةً، ثم مرِّر مؤشر الماوس فوق أحد أقسام قوائم التشغيل التي تم تشغيلها مؤخرًا.

يمكن لـ OutlinedCard تغيير التعتيم وتقريب الزوايا.

a3c16fc17be25f6c.png أخيرًا، يمكنك تحريك رقم الأغنية في قائمة تشغيل وتحويلها إلى زر تشغيل باستخدام تطبيق "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

a3c16fc17be25f6c.pngعليك إعادة تحميل التطبيق ثم تمرير المؤشر فوق رقم الأغنية في قائمة التشغيل أبرز الأغاني اليوم أو الإصدارات الجديدة.

ينتقل الرقم إلى الزر تشغيل لتشغيل الأغنية عند النقر عليها.

اطّلِع على الرمز البرمجي النهائي للمشروع على GitHub.

8. تهانينا!

لقد أكملت هذا الدرس التطبيقي حول الترميز. لقد علمت أن هناك العديد من التغييرات الصغيرة التي يمكنك دمجها في أي تطبيق لجعله أكثر جمالاً وتسهيل الوصول إليه وأقلمته وأكثر ملاءمة للعديد من الأنظمة الأساسية. ومن بين هذه الأساليب، على سبيل المثال لا الحصر:

  • أسلوب الخط: يعد النص أكثر من مجرد أداة للتواصل. استخدِم طريقة عرض النص لإحداث تأثير إيجابي لدى المستخدمين التطبيق وتصوره.
  • التصميم: أنشئ نظام تصميم يمكنك استخدامه بشكل موثوق به دون الحاجة إلى اتخاذ قرارات تصميم لكل أداة.
  • التكيّف: يجب الأخذ بعين الاعتبار الجهاز والنظام الأساسي الذي يشغِّل المستخدم تطبيقك عليه وإمكانياته. فكِّر في حجم الشاشة وطريقة عرض تطبيقك.
  • الحركة والرسوم المتحركة: تؤدي إضافة الحركة إلى تطبيقك إلى إضافة المزيد من الطاقة إلى تجربة المستخدم، ومن الناحية العملية، توفر ملاحظات للمستخدمين.

من خلال بعض التعديلات البسيطة، يمكنك تغيير مظهر تطبيقك المملّ:

قبل

1e67c60667821082.png

بعد

الخطوات التالية

نأمل أن تكون قد تعلّمت المزيد حول إنشاء تطبيقات رائعة في Flutter.

إذا طبقت أيًا من النصائح أو الحيل المذكورة هنا (أو كانت لديك نصيحة خاصة بك لمشاركتها)، يسرّنا أن نسمع منك. يمكنك التواصل معنا على Twitter على @rodydavis و@khanhnwin.

ويمكنك أيضًا الاطّلاع على المراجع التالية المفيدة.

المظهر

الموارد التكيُّفية والسريعة الاستجابة:

موارد التصميم العامة:

ويمكنك أيضًا التواصل مع منتدى Flutter.

انطلِق واجعل عالم التطبيق جميلاً!