برنامه Flutter خود را از خسته کننده به زیبا تبدیل کنید

1. مقدمه

Flutter جعبه ابزار UI گوگل برای ساخت برنامه های زیبا و بومی کامپایل شده برای موبایل، وب و دسکتاپ از یک پایگاه کد واحد است. Flutter با کدهای موجود کار می کند، توسط توسعه دهندگان و سازمان ها در سراسر جهان استفاده می شود و رایگان و متن باز است.

در این کد لبه، شما یک برنامه موسیقی Flutter را ارتقا می دهید و آن را از خسته کننده به زیبا تبدیل می کنید. برای انجام این کار، این نرم افزار کد از ابزارها و API های معرفی شده در Material 3 استفاده می کند.

چیزی که یاد خواهید گرفت

  • چگونه یک برنامه فلاتر بنویسیم که در همه پلتفرم ها قابل استفاده و زیبا باشد.
  • چگونه متن را در برنامه خود طراحی کنید تا مطمئن شوید که به تجربه کاربر اضافه می کند.
  • چگونه رنگ‌های مناسب را انتخاب کنید، ویجت‌ها را سفارشی کنید، تم خود را بسازید، و به سرعت و به راحتی حالت تاریک را اجرا کنید.
  • نحوه ساخت اپلیکیشن های تطبیقی ​​چند پلتفرمی
  • چگونه برنامه هایی بسازیم که در هر صفحه ای ظاهر خوبی داشته باشند.
  • چگونه به برنامه Flutter خود حرکت اضافه کنید تا واقعاً پاپ شود.

پیش نیازها:

این کد لبه فرض می کند که شما تجربه فلاتر دارید. اگر نه، ممکن است بخواهید ابتدا اصول اولیه را یاد بگیرید. لینک های زیر مفید هستند:

چیزی که خواهی ساخت

این لبه کد شما را در ساخت صفحه اصلی برای برنامه‌ای به نام MyArtist راهنمایی می‌کند، یک برنامه پخش موسیقی که در آن طرفداران می‌توانند با هنرمندان مورد علاقه خود به‌روز باشند. در مورد اینکه چگونه می توانید طراحی اپلیکیشن خود را تغییر دهید تا در همه پلتفرم ها زیبا به نظر برسد، بحث می شود.

ویدیوهای زیر نحوه عملکرد برنامه را در تکمیل این کد لبه نشان می دهد:

دوست دارید از این کد لبه چه چیزی یاد بگیرید؟

من با موضوع جدید هستم و می خواهم یک مرور کلی خوب داشته باشم. من چیزی در مورد این موضوع می دانم، اما می خواهم یک تجدید نظر کنم. من به دنبال کدی برای استفاده در پروژه خود هستم. من به دنبال توضیح یک چیز خاص هستم.

2. محیط توسعه Flutter خود را تنظیم کنید

برای تکمیل این آزمایشگاه به دو نرم افزار نیاز دارید - Flutter SDK و یک ویرایشگر .

شما می توانید کدلب را با استفاده از هر یک از این دستگاه ها اجرا کنید:

  • یک دستگاه فیزیکی Android یا iOS که به رایانه شما متصل شده و روی حالت Developer تنظیم شده است.
  • شبیه ساز iOS (نیاز به نصب ابزار Xcode دارد).
  • شبیه ساز اندروید (نیاز به نصب در Android Studio دارد).
  • یک مرورگر (Chrome برای اشکال زدایی لازم است).
  • به عنوان یک برنامه دسکتاپ Windows ، Linux ، یا macOS . شما باید روی پلتفرمی که قصد استقرار در آن را دارید توسعه دهید. بنابراین، اگر می خواهید یک برنامه دسکتاپ ویندوز توسعه دهید، باید در ویندوز توسعه دهید تا به زنجیره ساخت مناسب دسترسی داشته باشید. الزامات خاص سیستم عامل وجود دارد که به طور مفصل در docs.flutter.dev/desktop پوشش داده شده است.

3. اپلیکیشن شروع کد لبه را دریافت کنید

آن را از GitHub کلون کنید

برای شبیه سازی این کد لبه از GitHub، دستورات زیر را اجرا کنید:

git clone https://github.com/flutter/codelabs.git
cd codelabs/boring_to_beautiful/step_01/

برای اطمینان از اینکه همه چیز کار می کند، برنامه Flutter را به عنوان یک برنامه دسکتاپ مانند تصویر زیر اجرا کنید. از طرف دیگر، این پروژه را در IDE خود باز کنید و از ابزار آن برای اجرای برنامه استفاده کنید.

a3c16fc17be25f6c.png برنامه را اجرا کنید.

موفقیت! کد شروع برای صفحه اصلی MyArtist باید در حال اجرا باشد. باید صفحه اصلی MyArtist را ببینید. روی دسکتاپ خوب به نظر می رسد، اما موبایل... عالی نیست. برای یک چیز، آن را به شکاف احترام نمی گذارد. نگران نباش اینو درست میکنی

1e67c60667821082.pngd1139cde225de452.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 را وارد می کند و یک ویجت stateful را با استفاده از دو کلاس پیاده سازی می کند:

  • بیانیه import مولفه های مواد را در دسترس قرار می دهد.
  • کلاس HomeScreen کل صفحه نمایش داده شده را نشان می دهد.
  • متد build() کلاس _HomeScreenState ریشه درخت ویجت را ایجاد می کند که بر نحوه ایجاد همه ویجت ها در UI تأثیر می گذارد.

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 : فونت‌های سریف دارای خطوط تزیینی یا "دم" در انتهای حروف هستند و بیشتر رسمی تلقی می‌شوند. فونت‌های Sans-serif خطوط تزیینی ندارند و بیشتر غیررسمی‌تر تلقی می‌شوند. 34bf54e4cad90101.png sans serif بزرگ T و serif بزرگ T
  • فونت های تمام حروف بزرگ : استفاده از تمام حروف برای جلب توجه به مقدار کمی از متن مناسب است (به تیترها فکر کنید)، اما در صورت استفاده بیش از حد، می توان آن را به عنوان فریاد تلقی کرد که باعث می شود کاربر به طور کامل آن را نادیده بگیرد.
  • حروف کوچک عنوان یا جمله : هنگام اضافه کردن عنوان یا برچسب، نحوه استفاده از حروف بزرگ را در نظر بگیرید: حروف عنوان ، که در آن حرف اول هر کلمه بزرگ می شود ("این عنوان یک عنوان عنوان است")، رسمی تر است. جمله ای که فقط اسم های خاص را با حروف بزرگ می نویسد و اولین کلمه در متن ("این عنوان مورد جمله است") بیشتر محاوره ای و غیر رسمی است.
  • کرنینگ (فاصله بین هر حرف)، طول خط (عرض متن کامل در سراسر صفحه) و ارتفاع خط (بلندی هر خط از متن) : کم یا زیاد بودن هر یک از این موارد باعث می شود برنامه شما کمتر قابل خواندن باشد. به عنوان مثال، هنگام خواندن یک متن بزرگ و بدون شکستگی، به راحتی می توانید جایگاه خود را از دست بدهید.

با در نظر گرفتن این موضوع، به 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 جدید به همراه متن نمایش داده شده با فونت Montserrat را ببینید.

مشکلات؟

اگر برنامه شما به درستی اجرا نمی شود، به دنبال اشتباهات تایپی باشید. در صورت نیاز، از کد موجود در لینک های زیر برای بازگشت به مسیر استفاده کنید.

5. موضوع را تنظیم کنید

تم ها با مشخص کردن مجموعه ای از رنگ ها و سبک های متن، به ایجاد طراحی ساختاریافته و یکنواختی به برنامه کمک می کنند. تم ها شما را قادر می سازند تا به سرعت یک رابط کاربری را بدون نیاز به استرس بر روی جزئیات جزئی مانند تعیین رنگ دقیق برای هر ویجت پیاده سازی کنید.

توسعه دهندگان فلاتر معمولاً مؤلفه هایی با موضوع سفارشی به یکی از دو روش ایجاد می کنند:

  • ویجت‌های سفارشی جداگانه ایجاد کنید که هر کدام موضوع خاص خود را دارند.
  • برای ویجت‌های پیش‌فرض تم‌های محدوده‌ای ایجاد کنید.

این مثال از یک ارائه‌دهنده تم واقع در lib/src/shared/providers/theme.dart استفاده می‌کند تا ویجت‌ها و رنگ‌هایی با مضمون ثابت در سراسر برنامه ایجاد کند:

lib/src/shared/providers/theme.dart

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:material_color_utilities/material_color_utilities.dart';

class NoAnimationPageTransitionsBuilder extends PageTransitionsBuilder {
 const NoAnimationPageTransitionsBuilder();

 @override
 Widget buildTransitions<T>(
   PageRoute<T> route,
   BuildContext context,
   Animation<double> animation,
   Animation<double> secondaryAnimation,
   Widget child,
 ) {
   return child;
 }
}

class ThemeSettingChange extends Notification {
 ThemeSettingChange({required this.settings});
 final ThemeSettings settings;
}

class ThemeProvider extends InheritedWidget {
 const ThemeProvider(
     {super.key,
     required this.settings,
     required this.lightDynamic,
     required this.darkDynamic,
     required super.child});

 final ValueNotifier<ThemeSettings> settings;
 final ColorScheme? lightDynamic;
 final ColorScheme? darkDynamic;

 final pageTransitionsTheme = const PageTransitionsTheme(
   builders: <TargetPlatform, PageTransitionsBuilder>{
     TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
     TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
     TargetPlatform.linux: NoAnimationPageTransitionsBuilder(),
     TargetPlatform.macOS: NoAnimationPageTransitionsBuilder(),
     TargetPlatform.windows: NoAnimationPageTransitionsBuilder(),
   },
 );

 Color custom(CustomColor custom) {
   if (custom.blend) {
     return blend(custom.color);
   } else {
     return custom.color;
   }
 }

 Color blend(Color targetColor) {
   return Color(
       Blend.harmonize(targetColor.value, settings.value.sourceColor.value));
 }

 Color source(Color? target) {
   Color source = settings.value.sourceColor;
   if (target != null) {
     source = blend(target);
   }
   return source;
 }

 ColorScheme colors(Brightness brightness, Color? targetColor) {
   final dynamicPrimary = brightness == Brightness.light
       ? lightDynamic?.primary
       : darkDynamic?.primary;
   return ColorScheme.fromSeed(
     seedColor: dynamicPrimary ?? source(targetColor),
     brightness: brightness,
   );
 }

 ShapeBorder get shapeMedium => RoundedRectangleBorder(
       borderRadius: BorderRadius.circular(8),
     );

 CardTheme cardTheme() {
   return CardTheme(
     elevation: 0,
     shape: shapeMedium,
     clipBehavior: Clip.antiAlias,
   );
 }

 ListTileThemeData listTileTheme(ColorScheme colors) {
   return ListTileThemeData(
     shape: shapeMedium,
     selectedColor: colors.secondary,
   );
 }

 AppBarTheme appBarTheme(ColorScheme colors) {
   return AppBarTheme(
     elevation: 0,
     backgroundColor: colors.surface,
     foregroundColor: colors.onSurface,
   );
 }

 TabBarTheme tabBarTheme(ColorScheme colors) {
   return TabBarTheme(
     labelColor: colors.secondary,
     unselectedLabelColor: colors.onSurfaceVariant,
     indicator: BoxDecoration(
       border: Border(
         bottom: BorderSide(
           color: colors.secondary,
           width: 2,
         ),
       ),
     ),
   );
 }

 BottomAppBarTheme bottomAppBarTheme(ColorScheme colors) {
   return BottomAppBarTheme(
     color: colors.surface,
     elevation: 0,
   );
 }

 BottomNavigationBarThemeData bottomNavigationBarTheme(ColorScheme colors) {
   return BottomNavigationBarThemeData(
     type: BottomNavigationBarType.fixed,
     backgroundColor: colors.surfaceContainerHighest,
     selectedItemColor: colors.onSurface,
     unselectedItemColor: colors.onSurfaceVariant,
     elevation: 0,
     landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
   );
 }

 NavigationRailThemeData navigationRailTheme(ColorScheme colors) {
   return const NavigationRailThemeData();
 }

 DrawerThemeData drawerTheme(ColorScheme colors) {
   return DrawerThemeData(
     backgroundColor: colors.surface,
   );
 }

 ThemeData light([Color? targetColor]) {
   final _colors = colors(Brightness.light, targetColor);
   return ThemeData.light().copyWith(
     pageTransitionsTheme: pageTransitionsTheme,
     colorScheme: _colors,
     appBarTheme: appBarTheme(_colors),
     cardTheme: cardTheme(),
     listTileTheme: listTileTheme(_colors),
     bottomAppBarTheme: bottomAppBarTheme(_colors),
     bottomNavigationBarTheme: bottomNavigationBarTheme(_colors),
     navigationRailTheme: navigationRailTheme(_colors),
     tabBarTheme: tabBarTheme(_colors),
     drawerTheme: drawerTheme(_colors),
     scaffoldBackgroundColor: _colors.background,
     useMaterial3: true,
   );
 }

 ThemeData dark([Color? targetColor]) {
   final _colors = colors(Brightness.dark, targetColor);
   return ThemeData.dark().copyWith(
     pageTransitionsTheme: pageTransitionsTheme,
     colorScheme: _colors,
     appBarTheme: appBarTheme(_colors),
     cardTheme: cardTheme(),
     listTileTheme: listTileTheme(_colors),
     bottomAppBarTheme: bottomAppBarTheme(_colors),
     bottomNavigationBarTheme: bottomNavigationBarTheme(_colors),
     navigationRailTheme: navigationRailTheme(_colors),
     tabBarTheme: tabBarTheme(_colors),
     drawerTheme: drawerTheme(_colors),
     scaffoldBackgroundColor: _colors.background,
     useMaterial3: true,
   );
 }

 ThemeMode themeMode() {
   return settings.value.themeMode;
 }

 ThemeData theme(BuildContext context, [Color? targetColor]) {
   final brightness = MediaQuery.of(context).platformBrightness;
   return brightness == Brightness.light
       ? light(targetColor)
       : dark(targetColor);
 }

 static ThemeProvider of(BuildContext context) {
   return context.dependOnInheritedWidgetOfExactType<ThemeProvider>()!;
 }

 @override
 bool updateShouldNotify(covariant ThemeProvider oldWidget) {
   return oldWidget.settings != settings;
 }
}

class ThemeSettings {
 ThemeSettings({
   required this.sourceColor,
   required this.themeMode,
 });

 final Color sourceColor;
 final ThemeMode themeMode;
}

Color randomColor() {
 return Color(Random().nextInt(0xFFFFFFFF));
}

// Custom Colors
const linkColor = CustomColor(
 name: 'Link Color',
 color: Color(0xFF00B0FF),
);

class CustomColor {
 const CustomColor({
   required this.name,
   required this.color,
   this.blend = true,
 });

 final String name;
 final Color color;
 final bool blend;

 Color value(ThemeProvider provider) {
   return provider.custom(this);
 }
}

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 نقش‌های رنگی متفاوتی را معرفی می‌کند که مکمل یکدیگر هستند و می‌توانند در سرتاسر UI برای افزودن لایه‌های جدید بیان استفاده شوند. این نقش های رنگی جدید عبارتند از:

  • Primary , OnPrimary , PrimaryContainer , OnPrimaryContainer
  • Secondary , OnSecondary , SecondaryContainer , OnSecondaryContainer
  • Tertiary , OnTertiary , TertiaryContainer , OnTertiaryContainer
  • Error ، OnError ، ErrorContainer ، OnErrorContainer
  • Background ، OnBackground
  • Surface , OnSurface , SurfaceVariant , OnSurfaceVariant
  • Shadow ، Outline ، InversePrimary

علاوه بر این، توکن های طراحی جدید از تم های روشن و تاریک پشتیبانی می کنند:

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 برای پیاده سازی یک طرح شبکه پاسخگو در طراحی متریال استفاده می کند.

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 بپیچید تا فضای خالی اطراف آن ویجت اضافه شود. تمام مقادیر 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 تا کنون، با EdgeInsets.all(35) همه بالشتک‌ها (افقی و عمودی) را برای ویجت‌ها در صفحه اصلی روی 35 تنظیم کرده‌اید، اما می‌توانید برای هر یک از لبه‌ها نیز به‌طور مستقل، padding را تنظیم کنید. بالشتک را به گونه ای سفارشی کنید که مناسب تر با فضا باشد.

  • EdgeInsets.LTRB() چپ، بالا، راست و پایین را به صورت جداگانه تنظیم می کند
  • EdgeInsets.symmetric() padding را برای عمودی (بالا و پایین) معادل و افقی (چپ و راست) را معادل تنظیم می کند.
  • EdgeInsets.only() فقط لبه های مشخص شده را تنظیم می کند.
Scaffold(
  body: SingleChildScrollView(
    child: AdaptiveColumn(
      children: [
        AdaptiveContainer(
           columnSpan: 12,
             child: Padding(
               padding: const EdgeInsets.fromLTRB(20, 25, 20, 10), // Modify this line
               child: Row(
                 mainAxisAlignment: MainAxisAlignment.spaceBetween,
                   children: [
                     Expanded(
                       child: Text(
                         'Good morning',
                          style: context.displaySmall,
                       ),
                     ),
                     const SizedBox(width: 20),
                     const BrightnessToggle(),
                   ],
                 ),
               ),
             ),
             AdaptiveContainer(
               columnSpan: 12,
               child: Column(
                 children: [
                   const HomeHighlight(),
                   LayoutBuilder(
                     builder: (context, constraints) => HomeArtists(
                       artists: artists,
                       constraints: constraints,
                     ),
                   ),
                 ],
               ),
             ),
             AdaptiveContainer(
               columnSpan: 12,
               child: Column(
                 crossAxisAlignment: CrossAxisAlignment.start,
                 children: [
                   Padding(
                     padding: const EdgeInsets.symmetric(
                       horizontal: 15,
                       vertical: 10,
                     ), // Modify this line
                     child: Text(
                       'Recently played',
                       style: context.headlineSmall,
                     ),
                   ),
                   HomeRecent(
                     playlists: playlists,
                   ),
                 ],
               ),
             ),
             AdaptiveContainer(
               columnSpan: 12,
               child: Padding(
                 padding: const EdgeInsets.all(15), // Modify this line
                 child: Row(
                   crossAxisAlignment: CrossAxisAlignment.start,
                   children: [
                     Flexible(
                       flex: 10,
                         child: Column(
                           mainAxisAlignment: MainAxisAlignment.start,
                           crossAxisAlignment: CrossAxisAlignment.start,
                           children: [
                             Padding(
                               padding: const EdgeInsets.only(left: 8, bottom: 8), // Modify this line
                               child: Text(
                                 'Top Songs Today',
                                 style: context.titleLarge,
                               ),
                             ),
                             LayoutBuilder(
                               builder: (context, constraints) =>
                                    PlaylistSongs(
                                  playlist: topSongs,
                                  constraints: constraints,
                                ),
                              ),
                            ],
                          ),
                        ),
                    const SizedBox(width: 25),
                    Flexible(
                      flex: 10,
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.start,
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Padding(
                            padding: const EdgeInsets.only(left: 8, bottom: 8), // Modify this line
                            child: Text(
                              'New Releases',
                               style: context.titleLarge,
                            ),
                          ),
                          LayoutBuilder(
                            builder: (context, constraints) =>
                                    PlaylistSongs(
                                  playlist: newReleases,
                                  constraints: constraints,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );

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 در lib/src/shared/providers/theme.dart، PageTransitionsTheme به تم های روشن و تاریک منتقل کنید.

lib/src/shared/providers/theme.dart

ThemeData light([Color? targetColor]) {
  final _colors = colors(Brightness.light, targetColor);
  return ThemeData.light().copyWith(
    pageTransitionsTheme: pageTransitionsTheme, // Add this line
    colorScheme: ColorScheme.fromSeed(
      seedColor: source(targetColor),
      brightness: Brightness.light,
    ),
    appBarTheme: appBarTheme(_colors),
    cardTheme: cardTheme(),
    listTileTheme: listTileTheme(),
    tabBarTheme: tabBarTheme(_colors),
    scaffoldBackgroundColor: _colors.background,
  );
}

ThemeData dark([Color? targetColor]) {
  final _colors = colors(Brightness.dark, targetColor);
  return ThemeData.dark().copyWith(
    pageTransitionsTheme: pageTransitionsTheme, // Add this line
    colorScheme: ColorScheme.fromSeed(
      seedColor: source(targetColor),
      brightness: Brightness.dark,
    ),
    appBarTheme: appBarTheme(_colors),
    cardTheme: cardTheme(),
    listTileTheme: listTileTheme(),
    tabBarTheme: tabBarTheme(_colors),
    scaffoldBackgroundColor: _colors.background,
  );
}

بدون انیمیشن در iOS

با انیمیشن در iOS

مشکلات؟

اگر برنامه شما به درستی اجرا نمی‌شود، از کد موجود در پیوند زیر استفاده کنید تا به مسیر خود بازگردید.

حالت های شناور را اضافه کنید

یکی از راه‌های افزودن حرکت به برنامه دسک‌تاپ، حالت‌های شناور است، جایی که ویجت حالت خود را تغییر می‌دهد (مانند رنگ، شکل یا محتوا)، زمانی که کاربر مکان‌نما را روی آن می‌برد.

به‌طور پیش‌فرض، کلاس _OutlinedCardState (که برای کاشی‌های لیست پخش «اخیراً پخش شده» استفاده می‌شود)، یک MouseRegion را برمی‌گرداند – که فلش مکان‌نما را به یک اشاره‌گر در حالت شناور تبدیل می‌کند – اما می‌توانید بازخورد بصری بیشتری اضافه کنید.

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 اطلاعات بیشتری کسب کرده باشید!

اگر از هر یک از نکات یا ترفندهای ذکر شده در اینجا استفاده می کنید (یا نکته ای از خودتان برای به اشتراک گذاشتن دارید)، خوشحال می شویم که از شما بشنویم! با ما در توییتر در @rodydavis و @khanhnwin تماس بگیرید!

همچنین ممکن است منابع زیر برای شما مفید باشد.

موضوع بندی

منابع تطبیقی ​​و پاسخگو:

منابع طراحی عمومی:

همچنین، با انجمن Flutter ارتباط برقرار کنید !

به جلو بروید و دنیای برنامه را زیبا کنید!