Flutter ऐप्लिकेशन को बोरिंग से शानदार ऐप्लिकेशन बनाएं

अपने Flutter ऐप्लिकेशन को बोरिंग से खूबसूरत बनाएं

इस कोडलैब (कोड बनाना सीखने के लिए ट्यूटोरियल) के बारे में जानकारी

subjectपिछली बार जून 24, 2025 को अपडेट किया गया
account_circleThe Flutter Team ने लिखा

1. परिचय

Flutter, Google का यूज़र इंटरफ़ेस (यूआई) टूलकिट है. इसकी मदद से, एक ही कोडबेस से मोबाइल, वेब, और डेस्कटॉप के लिए शानदार और नेटिव तौर पर कंपाइल किए गए ऐप्लिकेशन बनाए जा सकते हैं. Flutter, मौजूदा कोड के साथ काम करता है. इसका इस्तेमाल दुनिया भर के डेवलपर और संगठन करते हैं. यह मुफ़्त और ओपन सोर्स है.

इस कोडलैब में, Flutter के किसी म्यूज़िक ऐप्लिकेशन को बेहतर बनाया गया है. ऐसा करने के लिए, यह कोडलैब Material 3 में लॉन्च किए गए टूल और एपीआई का इस्तेमाल करता है.

आपको क्या सीखने को मिलेगा

  • Flutter में ऐसा ऐप्लिकेशन लिखने का तरीका जो सभी प्लैटफ़ॉर्म पर इस्तेमाल किया जा सके और शानदार दिखे.
  • अपने ऐप्लिकेशन में टेक्स्ट को डिज़ाइन करने का तरीका, ताकि यह पक्का किया जा सके कि यह उपयोगकर्ता अनुभव को बेहतर बना रहा है.
  • सही रंग चुनने, विजेट को पसंद के मुताबिक बनाने, अपनी थीम बनाने, और गहरे रंग वाले मोड को तुरंत लागू करने का तरीका.
  • अलग-अलग प्लैटफ़ॉर्म के हिसाब से काम करने वाले ऐप्लिकेशन बनाने का तरीका.
  • ऐसे ऐप्लिकेशन बनाने का तरीका जो किसी भी स्क्रीन पर अच्छे दिखें.
  • अपने Flutter ऐप्लिकेशन को शानदार बनाने के लिए, उसमें मूवमेंट जोड़ने का तरीका.

ज़रूरी शर्तें

इस कोडलैब में यह माना गया है कि आपके पास Flutter का कुछ अनुभव है. अगर नहीं, तो आपको पहले बुनियादी बातें जाननी होंगी. इन लिंक से आपको मदद मिलेगी:

आपको क्या बनाना है

इस कोडलैब में, MyArtist नाम के ऐप्लिकेशन की होम स्क्रीन बनाने का तरीका बताया गया है. यह एक संगीत प्लेयर ऐप्लिकेशन है, जहां प्रशंसक अपने पसंदीदा कलाकारों के बारे में अप-टू-डेट रह सकते हैं. इसमें बताया गया है कि सभी प्लैटफ़ॉर्म पर अपने ऐप्लिकेशन को खूबसूरत दिखाने के लिए, उसके डिज़ाइन में कैसे बदलाव किया जा सकता है.

नीचे दिए गए वीडियो में बताया गया है कि इस कोडलैब को पूरा करने के बाद, ऐप्लिकेशन कैसे काम करता है:

आपको इस कोडलैब से क्या सीखना है?

2. Flutter डेवलपमेंट एनवायरमेंट सेट अप करना

इस लैब को पूरा करने के लिए, आपके पास दो सॉफ़्टवेयर होने चाहिए—Flutter SDK टूल और एडिटर.

इनमें से किसी भी डिवाइस का इस्तेमाल करके, कोडलैब चलाया जा सकता है:

  • आपके कंप्यूटर से कनेक्ट किया गया Android या iOS डिवाइस, जो डेवलपर मोड पर सेट हो.
  • iOS सिम्युलेटर (इसके लिए, Xcode टूल इंस्टॉल करने की ज़रूरत है).
  • Android एमुलेटर (Android Studio में सेटअप करना ज़रूरी है).
  • ब्राउज़र (डीबग करने के लिए 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 ऐप्लिकेशन को डेस्कटॉप ऐप्लिकेशन के तौर पर चलाएं, जैसा कि यहां दिखाया गया है. इसके अलावा, अपने आईडीई में यह प्रोजेक्ट खोलें और ऐप्लिकेशन को चलाने के लिए, उसके टूल का इस्तेमाल करें.

flutter run

हो गया! MyArtist की होम स्क्रीन के लिए स्टार्टर कोड चल रहा होना चाहिए. आपको MyArtist की होम स्क्रीन दिखेगी. यह डेस्कटॉप पर ठीक दिखता है, लेकिन मोबाइल पर... बहुत अच्छा नहीं है. एक बात यह है कि यह नॉच के साथ काम नहीं करता. चिंता न करें, इसे ठीक किया जा सकता है!

1e67c60667821082.pngd1139cde225de452.png

कोड के बारे में जानकारी

इसके बाद, कोड के बारे में जानें.

lib/src/features/home/view/home_screen.dart खोलें, जिसमें ये शामिल हैं:

lib/src/features/home/view/home_screen.dart

import 'package:flutter/material.dart';

import '../../../shared/classes/classes.dart';
import '../../../shared/extensions.dart';
import '../../../shared/providers/providers.dart';
import '../../../shared/views/views.dart';
import '../../../utils/adaptive_components.dart';
import '../../playlists/view/playlist_songs.dart';
import 'view.dart';

class HomeScreen extends StatefulWidget {
 
const HomeScreen({super.key});

 
@override
 
State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
 
@override
 
Widget build(BuildContext context) {
   
final PlaylistsProvider playlistProvider = PlaylistsProvider();
   
final List<Playlist> playlists = playlistProvider.playlists;
   
final Playlist topSongs = playlistProvider.topSongs;
   
final Playlist newReleases = playlistProvider.newReleases;
   
final ArtistsProvider artistsProvider = ArtistsProvider();
   
final List<Artist> artists = artistsProvider.artists;
   
return LayoutBuilder(
     
builder: (context, constraints) {
       
return Scaffold(
         
body: SingleChildScrollView(
           
child: AdaptiveColumn(
             
children: [
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Padding(
                   
padding: const EdgeInsets.all(2),
                   
child: Row(
                     
mainAxisAlignment: MainAxisAlignment.spaceBetween,
                     
children: [
                       
Expanded(
                         
child: Text(
                           
'Good morning',
                           
style: context.displaySmall,
                         
),
                       
),
                       
const SizedBox(width: 20),
                       
const BrightnessToggle(),
                     
],
                   
),
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Column(
                   
children: [
                     
const HomeHighlight(),
                     
LayoutBuilder(
                       
builder: (context, constraints) => HomeArtists(
                         
artists: artists,
                         
constraints: constraints,
                       
),
                     
),
                   
],
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Column(
                   
crossAxisAlignment: CrossAxisAlignment.start,
                   
children: [
                     
Padding(
                       
padding: const EdgeInsets.all(2),
                       
child: Text(
                         
'Recently played',
                         
style: context.headlineSmall,
                       
),
                     
),
                     
HomeRecent(playlists: playlists),
                   
],
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Padding(
                   
padding: const EdgeInsets.all(2),
                   
child: Row(
                     
crossAxisAlignment: CrossAxisAlignment.start,
                     
children: [
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
child: Text(
                                 
'Top Songs Today',
                                 
style: context.titleLarge,
                               
),
                             
),
                             
LayoutBuilder(
                               
builder: (context, constraints) =>
                                   
PlaylistSongs(
                                     
playlist: topSongs,
                                     
constraints: constraints,
                                   
),
                             
),
                           
],
                         
),
                       
),
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
child: Text(
                                 
'New Releases',
                                 
style: context.titleLarge,
                               
),
                             
),
                             
LayoutBuilder(
                               
builder: (context, constraints) =>
                                   
PlaylistSongs(
                                     
playlist: newReleases,
                                     
constraints: constraints,
                                   
),
                             
),
                           
],
                         
),
                       
),
                     
],
                   
),
                 
),
               
),
             
],
           
),
         
),
       
);
     
},
   
);
 
}
}

यह फ़ाइल material.dart इंपोर्ट करती है और दो क्लास का इस्तेमाल करके स्टेटफ़ुल विजेट लागू करती है:

  • import स्टेटमेंट से, Material Components उपलब्ध होते हैं.
  • HomeScreen क्लास, दिखाए गए पूरे पेज को दिखाती है.
  • _HomeScreenState क्लास का build() तरीका, विजेट ट्री का रूट बनाता है. इससे यूज़र इंटरफ़ेस (यूआई) में सभी विजेट बनाने के तरीके पर असर पड़ता है.

4. टाइपोग्राफ़ी का फ़ायदा लेना

टेक्स्ट हर जगह मौजूद है. उपयोगकर्ता के साथ बातचीत करने के लिए, टेक्स्ट का इस्तेमाल करना एक अच्छा तरीका है. क्या आपका ऐप्लिकेशन उपयोगकर्ताओं के लिए आसान और मज़ेदार है या भरोसेमंद और पेशेवर है? आपका पसंदीदा बैंकिंग ऐप्लिकेशन, Comic Sans का इस्तेमाल नहीं करता. इसकी वजह है. टेक्स्ट को दिखाने के तरीके से, उपयोगकर्ता को आपके ऐप्लिकेशन के बारे में पहली बार जो इंप्रेशन मिलता है वह उस पर काफ़ी असर डालता है. यहां टेक्स्ट को बेहतर तरीके से इस्तेमाल करने के कुछ तरीके दिए गए हैं.

अपने चैनल की जानकारी देने के बजाय, इससे जुड़ी खास चीज़ें दिखाकर दर्शकों का ध्यान खींचें

जहां भी हो सके, "बताएं" के बजाय "दिखाएं". उदाहरण के लिए, स्टार्टर ऐप्लिकेशन में NavigationRail में हर मुख्य रास्ते के लिए टैब होते हैं, लेकिन मुख्य आइकॉन एक जैसे होते हैं:

86c5f73b3aa5fd35.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

क्या कोई समस्या आ रही है?

अगर आपका ऐप्लिकेशन ठीक से काम नहीं कर रहा है, तो टाइपिंग में हुई गड़बड़ियों की जांच करें. अगर ज़रूरी हो, तो नीचे दिए गए लिंक पर जाकर कोड का इस्तेमाल करके, प्रोसेस को फिर से शुरू करें.

फ़ॉन्ट चुनते समय ध्यान रखें

फ़ॉन्ट से आपके ऐप्लिकेशन की स्टाइल तय होती है. इसलिए, सही फ़ॉन्ट चुनना ज़रूरी है. फ़ॉन्ट चुनते समय, इन बातों का ध्यान रखें:

  • सैन्स-सरफ़ या सरफ़: सरफ़ फ़ॉन्ट में अक्षरों के आखिर में सजावटी स्ट्रोक या "टेल" होते हैं. इन्हें ज़्यादा औपचारिक माना जाता है. सैंस-सरफ़़ फ़ॉन्ट में सजावटी स्ट्रोक नहीं होते. साथ ही, इन्हें आम तौर पर अनौपचारिक माना जाता है. सैंस-सरफ़़्ट कैपिटल T और सरफ़़्ट कैपिटल T
  • सिर्फ़ बड़े अक्षरों वाले फ़ॉन्ट: सिर्फ़ बड़े अक्षरों वाले फ़ॉन्ट का इस्तेमाल, कम टेक्स्ट (जैसे, हेडलाइन) पर ध्यान खींचने के लिए किया जाता है. हालांकि, इसका ज़रूरत से ज़्यादा इस्तेमाल करने पर, ऐसा लग सकता है कि आप चिल्ला रहे हैं. इससे उपयोगकर्ता इसे पूरी तरह से अनदेखा कर सकता है.
  • टाइटल केस या वाक्य का केस: टाइटल या लेबल जोड़ते समय, कैपिटल लेटर का इस्तेमाल करने का तरीका ध्यान में रखें: टाइटल केस में हर शब्द का पहला अक्षर कैपिटल लेटर में होता है ("This Is a Title Case Title"). यह ज़्यादा औपचारिक होता है. वाक्य का केस, जिसमें सिर्फ़ व्यक्तिवाचक संज्ञाओं और टेक्स्ट के पहले शब्द को कैपिटल लेटर में लिखा जाता है ("यह वाक्य का केस वाला टाइटल है"). यह केस, बातचीत वाली और अनौपचारिक भाषा के हिसाब से ज़्यादा सही होता है.
  • Kerning (हर अक्षर के बीच की स्पेस), लाइन की लंबाई (स्क्रीन पर पूरे टेक्स्ट की चौड़ाई), और लाइन की ऊंचाई (टेक्स्ट की हर लाइन कितनी ऊंची है): इनमें से किसी भी चीज़ को बहुत ज़्यादा या बहुत कम करने से, आपके ऐप्लिकेशन को पढ़ना मुश्किल हो जाता है. उदाहरण के लिए, टेक्स्ट के बड़े और बिना किसी विराम वाले ब्लॉक को पढ़ते समय, अपनी जगह बनाए रखना मुश्किल हो सकता है.

इस बात को ध्यान में रखते हुए, Google Fonts पर जाएं और Montserrat जैसा कोई सैंस-सरफ़़ फ़ॉन्ट चुनें. ऐसा इसलिए, क्योंकि संगीत ऐप्लिकेशन को मज़ेदार और दिलचस्प बनाने के लिए बनाया गया है.

कमांड लाइन से, google_fonts पैकेज इंपोर्ट करें. इससे pubspec.yaml फ़ाइल भी अपडेट हो जाती है, ताकि फ़ॉन्ट को ऐप्लिकेशन की डिपेंडेंसी के तौर पर जोड़ा जा सके.

flutter pub add google_fonts

macos/Runner/DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>com.apple.security.app-sandbox</key>
        <true/>
        <key>com.apple.security.cs.allow-jit</key>
        <true/>
        <key>com.apple.security.network.server</key>
        <true/>
        <!-- Make sure the following two lines are present -->
        <key>com.apple.security.network.client</key>
        <true/>
</dict>
</plist>

lib/src/shared/extensions.dart में, नया पैकेज इंपोर्ट करें:

lib/src/shared/extensions.dart

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

Montserrat TextTheme: सेट करना

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

बदलावों को चालू करने के लिए, 7f9a9e103c7b5e5.png को हॉट रीलोड करें. (अपने IDE में मौजूद बटन का इस्तेमाल करें या हॉट रीलोड करने के लिए, कमांड लाइन में r डालें.):

1e67c60667821082.png

आपको Montserrat फ़ॉन्ट में टेक्स्ट के साथ-साथ नए 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.toARGB32(),
       
settings.value.sourceColor.toARGB32(),
     
),
   
);
 
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 
final Color sourceColor;
 
final ThemeMode themeMode;
}

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

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

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

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

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

प्रोवाइडर का इस्तेमाल करने के लिए, एक इंस्टेंस बनाएं और उसे lib/src/shared/app.dart में मौजूद MaterialApp में स्कोप वाली थीम ऑब्जेक्ट को पास करें. यह नेस्ट किए गए किसी भी Theme ऑब्जेक्ट पर लागू होगा:

lib/src/shared/app.dart

import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import 'playback/bloc/bloc.dart';
import 'providers/theme.dart';
import 'router.dart';

class MyApp extends StatefulWidget {
 
const MyApp({super.key});

 
@override
 
State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
 
final settings = ValueNotifier(ThemeSettings(
   
sourceColor:  Colors.pink,
   
themeMode: ThemeMode.system,
 
));
 
@override
 
Widget build(BuildContext context) {
   
return BlocProvider<PlaybackBloc>(
     
create: (context) => PlaybackBloc(),
     
child: DynamicColorBuilder(
       
builder: (lightDynamic, darkDynamic) => ThemeProvider(
           
lightDynamic: lightDynamic,
           
darkDynamic: darkDynamic,
           
settings: settings,
           
child: NotificationListener<ThemeSettingChange>(
             
onNotification: (notification) {
               
settings.value = notification.settings;
               
return true;
             
},
             
child: ValueListenableBuilder<ThemeSettings>(
               
valueListenable: settings,
               
builder: (context, value, _) {
                 
final theme = ThemeProvider.of(context); // Add this line
                 
return MaterialApp.router(
                   
debugShowCheckedModeBanner: false,
                   
title: 'Flutter Demo',
                   
theme: theme.light(settings.value.sourceColor), // Add this line
                   
routeInformationParser: appRouter.routeInformationParser,
                   
routerDelegate: appRouter.routerDelegate,
                 
);
               
},
             
),
           
)),
     
),
   
);
 
}
}

थीम सेट अप हो जाने के बाद, ऐप्लिकेशन के लिए रंग चुनें.

रंगों का सही सेट चुनना मुश्किल हो सकता है. हो सकता है कि आपके पास मुख्य रंग का अंदाज़ा हो, लेकिन हो सकता है कि आपको अपने ऐप्लिकेशन में एक से ज़्यादा रंग इस्तेमाल करने हों. टेक्स्ट का रंग क्या होना चाहिए? टाइटल? कॉन्टेंट? लिंक? बैकग्राउंड का रंग क्या है? Material Theme Builder, वेब-आधारित टूल है. इसे Material 3 में लॉन्च किया गया था. इसकी मदद से, अपने ऐप्लिकेशन के लिए एक-दूसरे के साथ मैच होने वाले रंगों का सेट चुना जा सकता है.

ऐप्लिकेशन के लिए सोर्स कलर चुनने के लिए, Material Theme Builder खोलें और यूज़र इंटरफ़ेस (यूआई) के लिए अलग-अलग रंग एक्सप्लोर करें. ऐसा रंग चुनना ज़रूरी है जो ब्रैंड की स्टाइल या आपकी पसंद के मुताबिक हो.

थीम बनाने के बाद, प्राइमरी कलर बबल पर राइट क्लिक करें. इससे एक डायलॉग बॉक्स खुलेगा, जिसमें प्राइमरी कलर की हेक्स वैल्यू होगी. इस वैल्यू को कॉपी करें. (इस डायलॉग बॉक्स का इस्तेमाल करके भी रंग सेट किया जा सकता है.)

थीम उपलब्ध कराने वाली कंपनी को प्राइमरी कलर की हेक्स वैल्यू दें. उदाहरण के लिए, हेक्स रंग #00cbe6 को Color(0xff00cbe6) के तौर पर दिखाया जाता है. ThemeProvider, ThemeData जनरेट करता है. इसमें, उन कंप्लिमेंटरी कलर का सेट होता है जिनकी झलक आपने Material Theme Builder में देखी थी:

final settings = ValueNotifier(ThemeSettings(
   sourceColor:  Color(0xff00cbe6), // Replace this color
   themeMode: ThemeMode.system,
 ));

ऐप्लिकेशन को तुरंत रीस्टार्ट करें. प्राइमरी कलर लागू होने के बाद, ऐप्लिकेशन ज़्यादा आकर्षक लगने लगता है. कॉन्टेक्स्ट में थीम का रेफ़रंस देकर और ColorScheme को पकड़कर, सभी नए रंग ऐक्सेस करें:

final colors = Theme.of(context).colorScheme;

किसी खास रंग का इस्तेमाल करने के लिए, colorScheme पर जाकर रंग की भूमिका ऐक्सेस करें. lib/src/shared/views/outlined_card.dart पर जाएं और OutlinedCard को बॉर्डर दें:

lib/src/shared/views/outlined_card.dart

class _OutlinedCardState extends State<OutlinedCard> {
  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      cursor: widget.clickable
          ? SystemMouseCursors.click
          : SystemMouseCursors.basic,
      child: Container(
        // Add from here...
        decoration: BoxDecoration(
          border: Border.all(
            color: Theme.of(context).colorScheme.outline,
            width: 1,
          ),
        ),
        // ... To here.
        child: widget.child,
      ),
    );
  }
}

Material 3 में, रंगों की अलग-अलग भूमिकाएं शामिल की गई हैं, जो एक-दूसरे के साथ मिलकर काम करती हैं. इनका इस्तेमाल, यूज़र इंटरफ़ेस (यूआई) में अलग-अलग लेयर जोड़ने के लिए किया जा सकता है. इन नई भूमिकाओं में ये रंग शामिल हैं:

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

इसके अलावा, नए डिज़ाइन टोकन, हल्के और गहरे रंग, दोनों तरह की थीम के साथ काम करते हैं:

7b51703ed96196a4.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 पर मिल सकते हैं.

अलग-अलग प्लैटफ़ॉर्म के लिए काम करने वाला और उन पर अपने-आप अडैप्ट होने वाला ऐप्लिकेशन बनाते समय, इन प्लैटफ़ॉर्म के बीच के फ़र्क़ों का ध्यान रखें:

  • इनपुट का तरीका: माउस, टच या गेमपैड
  • फ़ॉन्ट साइज़, डिवाइस का ओरिएंटेशन, और वीडियो देखने की दूरी
  • स्क्रीन साइज़ और डिवाइस का साइज़, डाइमेंशन या कॉन्फ़िगरेशन: फ़ोन, टैबलेट, फ़ोल्ड किए जा सकने वाले डिवाइस, डेस्कटॉप, वेब

lib/src/shared/views/adaptive_navigation.dart फ़ाइल में एक नेविगेशन क्लास होती है. इसमें बॉडी को रेंडर करने के लिए, डेस्टिनेशन और कॉन्टेंट की सूची दी जा सकती है. इस लेआउट का इस्तेमाल कई स्क्रीन पर किया जाता है. इसलिए, हर चाइल्ड में पास करने के लिए एक शेयर किया गया बेस लेआउट होता है. नेविगेशन रेल, डेस्कटॉप और बड़ी स्क्रीन के लिए अच्छी होती हैं. हालांकि, मोबाइल पर बॉटम नेविगेशन बार दिखाकर, लेआउट को मोबाइल फ़्रेंडली बनाएं.

lib/src/shared/views/adaptive_navigation.dart

import 'package:flutter/material.dart';

class AdaptiveNavigation extends StatelessWidget {
 
const AdaptiveNavigation({
   
super.key,
   
required this.destinations,
   
required this.selectedIndex,
   
required this.onDestinationSelected,
   
required this.child,
 
});

 
final List<NavigationDestination> destinations;
 
final int selectedIndex;
 
final void Function(int index) onDestinationSelected;
 
final Widget child;

 
@override
 
Widget build(BuildContext context) {
   
return LayoutBuilder(
     
builder: (context, dimens) {
       
if (dimens.maxWidth >= 600) {                               // Add this line
         
return Scaffold(
           
body: Row(
             
children: [
               
NavigationRail(
                 
extended: dimens.maxWidth >= 800,
                 
minExtendedWidth: 180,
                 
destinations: destinations
                     
.map(
                       
(e) => NavigationRailDestination(
                         
icon: e.icon,
                         
label: Text(e.label),
                       
),
                     
)
                     
.toList(),
                 
selectedIndex: selectedIndex,
                 
onDestinationSelected: onDestinationSelected,
               
),
               
Expanded(child: child),
             
],
           
),
         
);
       
}                                                           // Add this line
       
// Mobile Layout
       
// Add from here...
       
return Scaffold(
         
body: child,
         
bottomNavigationBar: NavigationBar(
           
destinations: destinations,
           
selectedIndex: selectedIndex,
           
onDestinationSelected: onDestinationSelected,
         
),
       
);
       
// ... To here.
     
},
   
);
 
}
}

a8487a3c4d7890c9.png

सभी स्क्रीन एक जैसे साइज़ की नहीं होतीं. अगर आपने अपने ऐप्लिकेशन का डेस्कटॉप वर्शन फ़ोन पर दिखाने की कोशिश की, तो आपको सब कुछ देखने के लिए, स्क्रीन को बार-बार ज़ूम इन और ज़ूम आउट करना होगा. आपको अपने ऐप्लिकेशन को उस स्क्रीन के हिसाब से बदलना है जिस पर वह दिखता है. रिस्पॉन्सिव डिज़ाइन की मदद से, यह पक्का किया जा सकता है कि आपका ऐप्लिकेशन हर साइज़ की स्क्रीन पर अच्छा दिखे.

अपने ऐप्लिकेशन को रिस्पॉन्सिव बनाने के लिए, कुछ अडैप्टिव ब्रेकपॉइंट जोड़ें. इन्हें डीबगिंग ब्रेकपॉइंट के साथ न जोड़ें. ये ब्रेकपॉइंट, स्क्रीन के उन साइज़ के बारे में बताते हैं जहां आपके ऐप्लिकेशन को अपना लेआउट बदलना चाहिए.

छोटी स्क्रीन पर, बड़ी स्क्रीन के मुकाबले कम कॉन्टेंट दिखता है. ऐप्लिकेशन को डेस्कटॉप ऐप्लिकेशन की तरह छोटा न दिखाने के लिए, मोबाइल के लिए एक अलग लेआउट बनाएं. इस लेआउट में, कॉन्टेंट को अलग-अलग टैब में बांटा जा सकता है. इससे, मोबाइल पर ऐप्लिकेशन को ज़्यादा नेटिव लुक मिलता है.

अलग-अलग टारगेट के लिए ऑप्टिमाइज़ किए गए लेआउट डिज़ाइन करते समय, एक्सटेंशन के लिए यहां दिए गए तरीके (lib/src/shared/extensions.dart में MyArtist प्रोजेक्ट में बताए गए) आज़माएं.

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 पिक्सल से कम है, तो उसे टैबलेट माना जाता है. 1,200 पिक्सल से ज़्यादा रिज़ॉल्यूशन वाली इमेज को डेस्कटॉप माना जाता है. अगर कोई डिवाइस, टैबलेट या डेस्कटॉप नहीं है, तो उसे मोबाइल माना जाता है. material.io पर अडैप्टिव ब्रेकपॉइंट के बारे में ज़्यादा जानें.

होम स्क्रीन का रिस्पॉन्सिव लेआउट, 12 कॉलम वाले ग्रिड के आधार पर AdaptiveContainer और AdaptiveColumn का इस्तेमाल करता है.

अडैप्टिव लेआउट के लिए दो लेआउट की ज़रूरत होती है: एक मोबाइल के लिए और दूसरा बड़ी स्क्रीन के लिए रिस्पॉन्सिव लेआउट. इस स्थिति में, LayoutBuilder डेस्कटॉप लेआउट दिखाता है. lib/src/features/home/view/home_screen.dart में, मोबाइल लेआउट को TabBar और TabBarView के तौर पर बनाएं. इसमें चार टैब होने चाहिए.

lib/src/features/home/view/home_screen.dart

import 'package:flutter/material.dart';

import '../../../shared/classes/classes.dart';
import '../../../shared/extensions.dart';
import '../../../shared/providers/providers.dart';
import '../../../shared/views/views.dart';
import '../../../utils/adaptive_components.dart';
import '../../playlists/view/playlist_songs.dart';
import 'view.dart';

class HomeScreen extends StatefulWidget {
 
const HomeScreen({super.key});

 
@override
 
State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
 
@override
 
Widget build(BuildContext context) {
   
final PlaylistsProvider playlistProvider = PlaylistsProvider();
   
final List<Playlist> playlists = playlistProvider.playlists;
   
final Playlist topSongs = playlistProvider.topSongs;
   
final Playlist newReleases = playlistProvider.newReleases;
   
final ArtistsProvider artistsProvider = ArtistsProvider();
   
final List<Artist> artists = artistsProvider.artists;
   
return LayoutBuilder(
     
builder: (context, constraints) {
       
// Add from here...
       
if (constraints.isMobile) {
         
return DefaultTabController(
           
length: 4,
           
child: Scaffold(
             
appBar: AppBar(
               
centerTitle: false,
               
title: const Text('Good morning'),
               
actions: const [BrightnessToggle()],
               
bottom: const TabBar(
                 
isScrollable: true,
                 
tabs: [
                   
Tab(text: 'Home'),
                   
Tab(text: 'Recently Played'),
                   
Tab(text: 'New Releases'),
                   
Tab(text: 'Top Songs'),
                 
],
               
),
             
),
             
body: LayoutBuilder(
               
builder: (context, constraints) => TabBarView(
                 
children: [
                   
SingleChildScrollView(
                     
child: Column(
                       
children: [
                         
const HomeHighlight(),
                         
HomeArtists(
                           
artists: artists,
                           
constraints: constraints,
                         
),
                       
],
                     
),
                   
),
                   
HomeRecent(playlists: playlists, axis: Axis.vertical),
                   
PlaylistSongs(playlist: topSongs, constraints: constraints),
                   
PlaylistSongs(
                     
playlist: newReleases,
                     
constraints: constraints,
                   
),
                 
],
               
),
             
),
           
),
         
);
       
}
       
// ... To here.

       
return Scaffold(
         
body: SingleChildScrollView(
           
child: AdaptiveColumn(
             
children: [
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Padding(
                   
padding: const EdgeInsets.all(2),
                   
child: Row(
                     
mainAxisAlignment: MainAxisAlignment.spaceBetween,
                     
children: [
                       
Expanded(
                         
child: Text(
                           
'Good morning',
                           
style: context.displaySmall,
                         
),
                       
),
                       
const SizedBox(width: 20),
                       
const BrightnessToggle(),
                     
],
                   
),
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Column(
                   
children: [
                     
const HomeHighlight(),
                     
LayoutBuilder(
                       
builder: (context, constraints) => HomeArtists(
                         
artists: artists,
                         
constraints: constraints,
                       
),
                     
),
                   
],
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Column(
                   
crossAxisAlignment: CrossAxisAlignment.start,
                   
children: [
                     
Padding(
                       
padding: const EdgeInsets.all(2),
                       
child: Text(
                         
'Recently played',
                         
style: context.headlineSmall,
                       
),
                     
),
                     
HomeRecent(playlists: playlists),
                   
],
                 
),
               
),
               
AdaptiveContainer(
                 
columnSpan: 12,
                 
child: Padding(
                   
padding: const EdgeInsets.all(2),
                   
child: Row(
                     
crossAxisAlignment: CrossAxisAlignment.start,
                     
children: [
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
child: Text(
                                 
'Top Songs Today',
                                 
style: context.titleLarge,
                               
),
                             
),
                             
LayoutBuilder(
                               
builder: (context, constraints) =>
                                   
PlaylistSongs(
                                     
playlist: topSongs,
                                     
constraints: constraints,
                                   
),
                             
),
                           
],
                         
),
                       
),
                       
Flexible(
                         
flex: 10,
                         
child: Column(
                           
mainAxisAlignment: MainAxisAlignment.start,
                           
crossAxisAlignment: CrossAxisAlignment.start,
                           
children: [
                             
Padding(
                               
padding: const EdgeInsets.all(2),
                               
child: Text(
                                 
'New Releases',
                                 
style: context.titleLarge,
                               
),
                             
),
                             
LayoutBuilder(
                               
builder: (context, constraints) =>
                                   
PlaylistSongs(
                                     
playlist: newReleases,
                                     
constraints: constraints,
                                   
),
                             
),
                           
],
                         
),
                       
),
                     
],
                   
),
                 
),
               
),
             
],
           
),
         
),
       
);
     
},
   
);
 
}
}

377cfdda63a9de54.png

क्या कोई समस्या आ रही है?

अगर आपका ऐप्लिकेशन ठीक से काम नहीं कर रहा है, तो उसे फिर से चालू करने के लिए, यहां दिए गए लिंक पर दिए गए कोड का इस्तेमाल करें.

7. व्हाइटस्पेस का इस्तेमाल करना

खाली जगह, आपके ऐप्लिकेशन के लिए एक अहम विज़ुअल टूल है. इससे सेक्शन के बीच में खाली जगह बनती है.

ज़रूरत से ज़्यादा खाली जगह होना, ज़रूरत से कम खाली जगह होने से बेहतर है. ज़्यादा स्पेस में फ़िट करने के लिए, अपने फ़ॉन्ट या विज़ुअल एलिमेंट का साइज़ कम करने के बजाय, ज़्यादा खाली जगह जोड़ना बेहतर होता है.

जिन लोगों की आंखों की रोशनी कम है उनके लिए, बिना खाली जगह वाले टेक्स्ट को पढ़ना मुश्किल हो सकता है. बहुत ज़्यादा खाली जगह होने पर, यूज़र इंटरफ़ेस (यूआई) का एक जैसा न दिखना और खराब तरीके से व्यवस्थित होना. उदाहरण के लिए, नीचे दिए गए स्क्रीनशॉट देखें:

7f5e3514a7ee1750.png

d5144a50f5b4142c.png

इसके बाद, होम स्क्रीन पर खाली जगह जोड़ें, ताकि उसमें ज़्यादा आइटम जोड़े जा सकें. इसके बाद, स्पेसिंग को बेहतर बनाने के लिए लेआउट में और बदलाव किए जा सकते हैं.

किसी विजेट के आस-पास खाली जगह जोड़ने के लिए, उसे Padding ऑब्जेक्ट से रैप करें. lib/src/features/home/view/home_screen.dart में पैडिंग की सभी वैल्यू को 35 पर सेट करें:

lib/src/features/home/view/home_screen.dart

return Scaffold(
  body: SingleChildScrollView(
    child: AdaptiveColumn(
      children: [
        AdaptiveContainer(
          columnSpan: 12,
          child: Padding(
            padding: const EdgeInsets.all(35),                   // Modify this line
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Expanded(
                  child: Text(
                    'Good morning',
                    style: context.displaySmall,
                  ),
                ),
                const SizedBox(width: 20),
                const BrightnessToggle(),
              ],
            ),
          ),
        ),
        AdaptiveContainer(
          columnSpan: 12,
          child: Column(
            children: [
              const HomeHighlight(),
              LayoutBuilder(
                builder: (context, constraints) => HomeArtists(
                  artists: artists,
                  constraints: constraints,
                ),
              ),
            ],
          ),
        ),
        AdaptiveContainer(
          columnSpan: 12,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Padding(
                padding: const EdgeInsets.all(35),               // Modify this line
                child: Text(
                  'Recently played',
                  style: context.headlineSmall,
                ),
              ),
              HomeRecent(playlists: playlists),
            ],
          ),
        ),
        AdaptiveContainer(
          columnSpan: 12,
          child: Padding(
            padding: const EdgeInsets.all(35),                   // Modify this line
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Flexible(
                  flex: 10,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Padding(
                        padding: const EdgeInsets.all(35),       // Modify this line
                        child: Text(
                          'Top Songs Today',
                          style: context.titleLarge,
                        ),
                      ),
                      LayoutBuilder(
                        builder: (context, constraints) =>
                            PlaylistSongs(
                              playlist: topSongs,
                              constraints: constraints,
                            ),
                      ),
                    ],
                  ),
                ),
                Flexible(
                  flex: 10,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Padding(
                        padding: const EdgeInsets.all(35),       // Modify this line
                        child: Text(
                          'New Releases',
                          style: context.titleLarge,
                        ),
                      ),
                      LayoutBuilder(
                        builder: (context, constraints) =>
                            PlaylistSongs(
                              playlist: newReleases,
                              constraints: constraints,
                            ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    ),
  ),
);

ऐप्लिकेशन को हॉट रीलोड करें. यह पहले जैसा ही दिखेगा, लेकिन विजेट के बीच ज़्यादा खाली जगह होगी. अतिरिक्त पैडिंग बेहतर दिखती है, लेकिन सबसे ऊपर मौजूद हाइलाइट बैनर अब भी किनारों के बहुत करीब है.

lib/src/features/home/view/home_highlight.dart में, बैनर के पैडिंग को 15 पर सेट करें:

lib/src/features/home/view/home_highlight.dart

class HomeHighlight extends StatelessWidget {
  const HomeHighlight({super.key});

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(15),                   // Modify this line
            child: Clickable(
              child: SizedBox(
                height: 275,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(10),
                  child: Image.asset(
                    'assets/images/news/concert.jpeg',
                    fit: BoxFit.cover,
                  ),
                ),
              ),
              onTap: () => launchUrl(Uri.parse('https://docs.flutter.dev')),
            ),
          ),
        ),
      ],
    );
  }
}

ऐप्लिकेशन को हॉट रीलोड करें. सबसे नीचे मौजूद दो प्लेलिस्ट के बीच कोई खाली जगह नहीं है. इसलिए, ऐसा लगता है कि वे एक ही टेबल से जुड़ी हैं. ऐसा नहीं है. आपको इसे ठीक करना होगा.

df1d9af97d039cc8.png

प्लेलिस्ट के बीच में खाली जगह जोड़ने के लिए, उनमें मौजूद Row में साइज़ विजेट डालें. lib/src/features/home/view/home_screen.dart में, 35 की चौड़ाई वाला SizedBox जोड़ें:

lib/src/features/home/view/home_screen.dart

AdaptiveContainer(
  columnSpan: 12,
  child: Padding(
    padding: const EdgeInsets.all(35),
    child: Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Flexible(
          flex: 10,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Padding(
                padding: const EdgeInsets.all(35),
                child: Text(
                  'Top Songs Today',
                  style: context.titleLarge,
                ),
              ),
              LayoutBuilder(
                builder: (context, constraints) =>
                    PlaylistSongs(
                      playlist: topSongs,
                      constraints: constraints,
                    ),
              ),
            ],
          ),
        ),
        const SizedBox(width: 35),                                  // Add this line
        Flexible(
          flex: 10,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Padding(
                padding: const EdgeInsets.all(35),
                child: Text(
                  'New Releases',
                  style: context.titleLarge,
                ),
              ),
              LayoutBuilder(
                builder: (context, constraints) =>
                    PlaylistSongs(
                      playlist: newReleases,
                      constraints: constraints,
                    ),
              ),
            ],
          ),
        ),
      ],
    ),
  ),
),

ऐप्लिकेशन को हॉट रीलोड करें. ऐप्लिकेशन ऐसा दिखेगा:

d8b2a3d47736dbab.png

अब होम स्क्रीन पर कॉन्टेंट के लिए ज़रूरत के मुताबिक जगह है. हालांकि, सब कुछ बहुत अलग-अलग दिखता है और सेक्शन के बीच कोई संबंध नहीं दिखता.

अब तक, आपने EdgeInsets.all(35) का इस्तेमाल करके, होम स्क्रीन पर विजेट के लिए सभी पैडिंग (हॉरिज़ॉन्टल और वर्टिकल, दोनों) को 35 पर सेट किया है. हालांकि, हर किनारे के लिए पैडिंग को अलग से भी सेट किया जा सकता है. स्पेस में बेहतर तरीके से फ़िट करने के लिए, पैडिंग को पसंद के मुताबिक बनाएं.

  • EdgeInsets.LTRB(), बाईं ओर, ऊपर, दाईं ओर, और नीचे को अलग-अलग सेट करता है
  • EdgeInsets.symmetric(), वर्टिकल (ऊपर और नीचे) और हॉरिज़ॉन्टल (बाईं और दाईं) पैडिंग को बराबर पर सेट करता है
  • EdgeInsets.only() सिर्फ़ तय किए गए किनारों को सेट करता है.

lib/src/features/home/view/home_screen.dart

return Scaffold(
  body: SingleChildScrollView(
    child: AdaptiveColumn(
      children: [
        AdaptiveContainer(
          columnSpan: 12,
          child: Padding(
            padding: const EdgeInsets.fromLTRB(20, 25, 20, 10),  // Modify this line
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Expanded(
                  child: Text(
                    'Good morning',
                    style: context.displaySmall,
                  ),
                ),
                const SizedBox(width: 20),
                const BrightnessToggle(),
              ],
            ),
          ),
        ),
        AdaptiveContainer(
          columnSpan: 12,
          child: Column(
            children: [
              const HomeHighlight(),
              LayoutBuilder(
                builder: (context, constraints) => HomeArtists(
                  artists: artists,
                  constraints: constraints,
                ),
              ),
            ],
          ),
        ),
        AdaptiveContainer(
          columnSpan: 12,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Padding(
                padding: const EdgeInsets.symmetric(             // Modify from here...
                  horizontal: 15,
                  vertical: 10,
                ),                                               // To here.
                child: Text(
                  'Recently played',
                  style: context.headlineSmall,
                ),
              ),
              HomeRecent(playlists: playlists),
            ],
          ),
        ),
        AdaptiveContainer(
          columnSpan: 12,
          child: Padding(
            padding: const EdgeInsets.all(15),                   // Modify this line
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Flexible(
                  flex: 10,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Padding(
                        padding: const EdgeInsets.only(          // Modify from here...
                          left: 8,
                          bottom: 8,
                        ),                                       // To here.
                        child: Text(
                          'Top Songs Today',
                          style: context.titleLarge,
                        ),
                      ),
                      LayoutBuilder(
                        builder: (context, constraints) =>
                            PlaylistSongs(
                              playlist: topSongs,
                              constraints: constraints,
                            ),
                      ),
                    ],
                  ),
                ),
                const SizedBox(width: 25),                       // Modify this line
                Flexible(
                  flex: 10,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Padding(
                        padding: const EdgeInsets.only(          // Modify from here...
                          left: 8,
                          bottom: 8,
                        ),                                       // To here.
                        child: Text(
                          'New Releases',
                          style: context.titleLarge,
                        ),
                      ),
                      LayoutBuilder(
                        builder: (context, constraints) =>
                            PlaylistSongs(
                              playlist: newReleases,
                              constraints: constraints,
                            ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    ),
  ),
);

lib/src/features/home/view/home_highlight.dart में, बैनर के बाईं और दाईं ओर की पैडिंग को 35 पर और ऊपर और नीचे की पैडिंग को 5 पर सेट करें:

lib/src/features/home/view/home_highlight.dart

class HomeHighlight extends StatelessWidget {
  const HomeHighlight({super.key});

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            // Modify the following line
            padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 5),
            child: Clickable(
              child: SizedBox(
                height: 275,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(10),
                  child: Image.asset(
                    'assets/images/news/concert.jpeg',
                    fit: BoxFit.cover,
                  ),
                ),
              ),
              onTap: () => launchUrl(Uri.parse('https://docs.flutter.dev')),
            ),
          ),
        ),
      ],
    );
  }
}

ऐप्लिकेशन को हॉट रीलोड करें. लेआउट और स्पेसिंग बहुत बेहतर दिखती है! आखिरी चरण में, कुछ मोशन और ऐनिमेशन जोड़ें.

7f5e3514a7ee1750.png

क्या कोई समस्या आ रही है?

अगर आपका ऐप्लिकेशन ठीक से काम नहीं कर रहा है, तो उसे फिर से चालू करने के लिए, यहां दिए गए लिंक पर दिए गए कोड का इस्तेमाल करें.

8. मोशन और ऐनिमेशन जोड़ना

मोशन और ऐनिमेशन, ऐप्लिकेशन में गति और ऊर्जा जोड़ने के साथ-साथ, उपयोगकर्ता के ऐप्लिकेशन से इंटरैक्ट करने पर सुझाव, शिकायत या राय देने के बेहतरीन तरीके हैं.

स्क्रीन के बीच ऐनिमेशन जोड़ना

ThemeProvider, मोबाइल प्लैटफ़ॉर्म (iOS, Android) के लिए स्क्रीन ट्रांज़िशन ऐनिमेशन के साथ PageTransitionsTheme तय करता है. डेस्कटॉप उपयोगकर्ताओं को माउस या ट्रैकपैड पर क्लिक करने से पहले ही फ़ीडबैक मिल जाता है. इसलिए, पेज ट्रांज़िशन ऐनिमेशन की ज़रूरत नहीं होती.

Flutter, स्क्रीन ट्रांज़िशन ऐनिमेशन उपलब्ध कराता है. इन्हें टारगेट प्लैटफ़ॉर्म के आधार पर, अपने ऐप्लिकेशन के लिए कॉन्फ़िगर किया जा सकता है. इस बारे में lib/src/shared/providers/theme.dart में बताया गया है:

lib/src/shared/providers/theme.dart

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

lib/src/shared/providers/theme.dart में, हल्के और गहरे रंग वाली दोनों थीम में PageTransitionsTheme को पास करें

lib/src/shared/providers/theme.dart

ThemeData light([Color? targetColor]) {
  final colorScheme = colors(Brightness.light, targetColor);
  return ThemeData.light().copyWith(
    pageTransitionsTheme: pageTransitionsTheme,                     // Add this line
    colorScheme: colorScheme,
    appBarTheme: appBarTheme(colorScheme),
    cardTheme: cardTheme(),
    listTileTheme: listTileTheme(colorScheme),
    bottomAppBarTheme: bottomAppBarTheme(colorScheme),
    bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
    navigationRailTheme: navigationRailTheme(colorScheme),
    tabBarTheme: tabBarTheme(colorScheme),
    drawerTheme: drawerTheme(colorScheme),
    scaffoldBackgroundColor: colorScheme.surface,
  );
}

ThemeData dark([Color? targetColor]) {
  final colorScheme = colors(Brightness.dark, targetColor);
  return ThemeData.dark().copyWith(
    pageTransitionsTheme: pageTransitionsTheme,                     // Add this line
    colorScheme: colorScheme,
    appBarTheme: appBarTheme(colorScheme),
    cardTheme: cardTheme(),
    listTileTheme: listTileTheme(colorScheme),
    bottomAppBarTheme: bottomAppBarTheme(colorScheme),
    bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
    navigationRailTheme: navigationRailTheme(colorScheme),
    tabBarTheme: tabBarTheme(colorScheme),
    drawerTheme: drawerTheme(colorScheme),
    scaffoldBackgroundColor: colorScheme.surface,
  );
}

iOS पर ऐनिमेशन के बिना

iOS पर ऐनिमेशन के साथ

क्या कोई समस्या आ रही है?

अगर आपका ऐप्लिकेशन ठीक से काम नहीं कर रहा है, तो उसे फिर से चालू करने के लिए, यहां दिए गए लिंक पर दिए गए कोड का इस्तेमाल करें.

9. कर्सर घुमाने पर दिखने वाली स्थितियां जोड़ना

डेस्कटॉप ऐप्लिकेशन में मोशन जोड़ने का एक तरीका, होवर स्टेटस है. इसमें, जब उपयोगकर्ता कर्सर को किसी विजेट पर घुमाता है, तो विजेट का रंग, आकार या कॉन्टेंट बदल जाता है.

डिफ़ॉल्ट रूप से, _OutlinedCardState क्लास (जिसका इस्तेमाल "हाल ही में चलाए गए" प्लेलिस्ट टाइल के लिए किया जाता है) एक MouseRegion दिखाती है. यह कर्सर ऐरो को कर्सर पॉइंटर में बदल देती है. हालांकि, इसमें ज़्यादा विज़ुअल फ़ीडबैक जोड़े जा सकते हैं.

lib/src/shared/views/outlined_card.dart खोलें और _hovered स्टेटस को लागू करने के लिए, इसके कॉन्टेंट को यहां दिए गए तरीके से बदलें.

lib/src/shared/views/outlined_card.dart

import 'package:flutter/material.dart';

class OutlinedCard extends StatefulWidget {
 
const OutlinedCard({super.key, required this.child, this.clickable = true});

 
final Widget child;
 
final bool clickable;

 
@override
 
State<OutlinedCard> createState() => _OutlinedCardState();
}

class _OutlinedCardState extends State<OutlinedCard> {
 
bool _hovered = false;

 
@override
 
Widget build(BuildContext context) {
   
final borderRadius = BorderRadius.circular(_hovered ? 20 : 8);
   
const animationCurve = Curves.easeInOut;
   
return MouseRegion(
     
onEnter: (_) {
       
if (!widget.clickable) return;
       
setState(() {
         
_hovered = true;
       
});
     
},
     
onExit: (_) {
       
if (!widget.clickable) return;
       
setState(() {
         
_hovered = false;
       
});
     
},
     
cursor: widget.clickable
         
? SystemMouseCursors.click
         
: SystemMouseCursors.basic,
     
child: AnimatedContainer(
       
duration: kThemeAnimationDuration,
       
curve: animationCurve,
       
decoration: BoxDecoration(
         
border: Border.all(
           
color: Theme.of(context).colorScheme.outline,
           
width: 1,
         
),
         
borderRadius: borderRadius,
       
),
       
foregroundDecoration: BoxDecoration(
         
color: Theme.of(
           
context,
         
).colorScheme.onSurface.withAlpha(_hovered ? 30 : 0),
         
borderRadius: borderRadius,
       
),
       
child: TweenAnimationBuilder<BorderRadius>(
         
duration: kThemeAnimationDuration,
         
curve: animationCurve,
         
tween: Tween(begin: BorderRadius.zero, end: borderRadius),
         
builder: (context, borderRadius, child) => ClipRRect(
           
clipBehavior: Clip.antiAlias,
           
borderRadius: borderRadius,
           
child: child,
         
),
         
child: widget.child,
       
),
     
),
   
);
 
}
}

ऐप्लिकेशन को हॉट रीलोड करें. इसके बाद, हाल ही में चलाए गए वीडियो की प्लेलिस्ट की टाइल पर कर्सर घुमाएं.

OutlinedCard, ओपैसिटी बदलता है और कोनों को गोल करता है.

आखिर में, lib/src/shared/views/hoverable_song_play_button.dart में बताए गए HoverableSongPlayButton विजेट का इस्तेमाल करके, प्लेलिस्ट में गाने के नंबर को चलाएं बटन में ऐनिमेट करें. lib/src/features/playlists/view/playlist_songs.dart में, Center विजेट (जिसमें गाने का नंबर है) को HoverableSongPlayButton के साथ रैप करें:

lib/src/features/playlists/view/playlist_songs.dart

rowBuilder: (context, index) => DataRow.byIndex(
  index: index,
  cells: [
    DataCell(
      HoverableSongPlayButton(                                      // Modify from here...
        hoverMode: HoverMode.overlay,
        song: playlist.songs[index],
        child: Center(
          child: Text(
            (index + 1).toString(),
            textAlign: TextAlign.center,
          ),
        ),
      ),                                                            // To here.
    ),
    DataCell(
      Row(
        children: [
          Padding(
            padding: const EdgeInsets.all(2),
            child: ClippedImage(playlist.songs[index].image.image),
          ),
          const SizedBox(width: 10),
          Expanded(child: Text(playlist.songs[index].title)),
        ],
      ),
    ),
    DataCell(Text(playlist.songs[index].length.toHumanizedString())),
  ],
),

ऐप्लिकेशन को हॉट रीलोड करें. इसके बाद, आज के सबसे लोकप्रिय गाने या नई रिलीज़ प्लेलिस्ट में, गाने के नंबर पर कर्सर घुमाएं.

यह नंबर, ऐनिमेशन के ज़रिए चलाएं बटन में बदल जाता है. इस पर क्लिक करने से गाना चलने लगता है.

GitHub पर प्रोजेक्ट का फ़ाइनल कोड देखें.

10. बधाई हो!

आपने यह कोडलैब पूरा कर लिया है! आपने जाना कि ऐप्लिकेशन को ज़्यादा खूबसूरत बनाने के लिए, उसमें कई छोटे बदलाव किए जा सकते हैं. साथ ही, उसे ज़्यादा ऐक्सेस किया जा सकता है, ज़्यादा स्थानीय बनाया जा सकता है, और कई प्लैटफ़ॉर्म के लिए ज़्यादा सही बनाया जा सकता है. इनमें ये तकनीकें शामिल हैं. हालांकि, इनके अलावा और भी तकनीकें हो सकती हैं:

  • टाइपोग्राफ़ी: टेक्स्ट सिर्फ़ एक कम्यूनिकेशन टूल नहीं है. टेक्स्ट को दिखाने के तरीके का इस्तेमाल करके, उपयोगकर्ताओं के अनुभव और आपके ऐप्लिकेशन के बारे में उनकी धारणा पर अच्छा असर डालें.
  • थीमिंग: ऐसा डिज़ाइन सिस्टम बनाएं जिसका इस्तेमाल हर विजेट के लिए डिज़ाइन से जुड़े फ़ैसले लिए बिना किया जा सके.
  • डिवाइस और प्लैटफ़ॉर्म के हिसाब से काम करना: यह देखें कि उपयोगकर्ता आपका ऐप्लिकेशन किस डिवाइस और प्लैटफ़ॉर्म पर चला रहा है. साथ ही, यह भी देखें कि उस डिवाइस और प्लैटफ़ॉर्म पर ऐप्लिकेशन की क्या सुविधाएं काम करती हैं. स्क्रीन साइज़ और आपके ऐप्लिकेशन के दिखने के तरीके का ध्यान रखें.
  • मोशन और ऐनिमेशन: अपने ऐप्लिकेशन में मोशन जोड़ने से, उपयोगकर्ता अनुभव बेहतर बनता है. साथ ही, इससे उपयोगकर्ताओं को फ़ीडबैक भी मिलता है.

कुछ छोटे बदलावों से, आपके ऐप्लिकेशन को बोरिंग से खूबसूरत बनाया जा सकता है:

पहले

1e67c60667821082.png

बाद में

अगले चरण

हमें उम्मीद है कि आपको Flutter में शानदार ऐप्लिकेशन बनाने के बारे में ज़्यादा जानकारी मिली होगी!

अगर आपने यहां बताई गई कोई सलाह या तरकीब अपनाई है या आपके पास कोई सलाह है, तो हमें बताएं. हमें आपसे सुनकर खुशी होगी! Twitter पर @rodydavis और @khanhnwin पर हमसे संपर्क करें!

आपको यहां दिए गए संसाधन भी काम के लग सकते हैं.

थीम बनाई जा रही है

अडैप्टिव और रिस्पॉन्सिव संसाधन:

डिज़ाइन से जुड़े सामान्य संसाधन:

साथ ही, Flutter कम्यूनिटी से जुड़ें!

आगे बढ़ें और ऐप्लिकेशन की दुनिया को खूबसूरत बनाएं!