आपका पहला Flutter ऐप्लिकेशन

1. परिचय

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

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

आप इन चीज़ों के बारे में जानेंगे

  • Flutter के काम करने के तरीके की बुनियादी बातें
  • Flutter में लेआउट बनाना
  • उपयोगकर्ता के इंटरैक्शन (जैसे कि बटन दबाने पर) को ऐप्लिकेशन के व्यवहार से कनेक्ट करना
  • अपने Flutter कोड को व्यवस्थित रखना
  • ऐप्लिकेशन को रिस्पॉन्सिव बनाना (अलग-अलग स्क्रीन के लिए)
  • एक जैसा रंग-रूप हासिल करना और CANNOT TRANSLATE

आप एक बुनियादी मचान से शुरुआत करेंगे, ताकि आप दिलचस्प हिस्सों पर सीधे जा सकें.

e9c6b402cd8003fd.png

फ़िलिप कोडलैब आपको पूरे कोडलैब के बारे में बता रहा है!

लैब शुरू करने के लिए, 'आगे बढ़ें' पर क्लिक करें.

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

संपादक

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

बेशक, अपनी पसंद के किसी भी एडिटर का इस्तेमाल किया जा सकता है: Android Studio, अन्य IntelliJ IDE, Emacs, Vim या Notepad++. वे सभी Flutter के साथ काम करते हैं.

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

228c71510a8e868.png

डेवलपमेंट टारगेट चुनें

Flutter कई प्लैटफ़ॉर्म पर उपलब्ध टूलकिट है. आपका ऐप्लिकेशन इनमें से किसी भी ऑपरेटिंग सिस्टम पर चल सकता है:

  • iOS
  • Android
  • Windows
  • macOS
  • Linux
  • वेब

हालांकि, एक ऐसा ऑपरेटिंग सिस्टम चुनना आम बात है जिसके लिए आपको खास तौर पर डेवलप करना है. यह आपका "डेवलपमेंट टारगेट"—यह वह ऑपरेटिंग सिस्टम है जिस पर आपका ऐप्लिकेशन डेवलपमेंट के दौरान चलता है.

16695777c07f18e5.png

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

अपने डेवलपमेंट टारगेट के तौर पर वेब को चुनना दिलचस्प हो सकता है. इस विकल्प की एक समस्या यह है कि Flutter को डेवलपमेंट से जुड़ी सबसे अहम सुविधाओं में से एक का इस्तेमाल नहीं किया जा सकता. स्टेटफ़ुल हॉट रीलोड. Flutter, वेब ऐप्लिकेशन को हॉट-रीलोड नहीं कर सकता.

अभी चुनें. याद रखें: आप अपने ऐप्लिकेशन को बाद में किसी भी समय अन्य ऑपरेटिंग सिस्टम पर चला सकते हैं. बस ध्यान रखें कि डेवलपमेंट टारगेट के बारे में साफ़ तौर पर जानकारी देने से, अगला कदम और आसान हो जाता है.

Flutter इंस्टॉल करें

Flutter SDK टूल को इंस्टॉल करने के तरीके से जुड़े सबसे अप-टू-डेट निर्देश हमेशा docs.flutter.dev पर उपलब्ध होते हैं.

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

  1. Flutter SDK टूल
  2. Flutter प्लगिन के साथ विज़ुअल स्टूडियो कोड
  3. आपके चुने गए डेवलपमेंट टारगेट के लिए ज़रूरी सॉफ़्टवेयर. उदाहरण के लिए: Windows को टारगेट करने के लिए Visual Studio या macOS को टारगेट करने के लिए Xcode

अगले सेक्शन में, आपको अपना पहला Flutter प्रोजेक्ट बनाना होगा.

अगर आपको अब तक समस्याएं आ रही हैं, तो इनमें से कुछ सवाल और जवाब (StackOverflow से) आपको समस्या हल करने में मददगार हो सकते हैं.

अक्सर पूछे जाने वाले सवाल

3. प्रोजेक्ट बनाना

अपना पहला Flutter प्रोजेक्ट बनाएं

विज़ुअल स्टूडियो कोड लॉन्च करें और F1 या Ctrl+Shift+P या Shift+Cmd+P के साथ कमांड पैलेट खोलें. "फ़्लटर न्यू" टाइप करना शुरू करें. Flutter: New Project कमांड चुनें.

इसके बाद, ऐप्लिकेशन चुनें. इसके बाद, एक फ़ोल्डर चुनें, जिसमें आपको अपना प्रोजेक्ट बनाना है. यह आपकी होम डायरेक्ट्री या C:\src\ जैसा कुछ हो सकता है.

आखिर में, अपने प्रोजेक्ट को नाम दें. namer_app या my_awesome_namer जैसा कुछ.

260a7d97f9678005.png

Flutter अब आपका प्रोजेक्ट फ़ोल्डर बनाता है और VS Code खोलता है.

अब ऐप्लिकेशन के बेसिक स्कैफ़ोल्ड से, तीन फ़ाइलों का कॉन्टेंट ओवरराइट हो जाएगा.

कॉपी करें और शुरुआती ऐप्लिकेशन चिपकाएं

बनाम कोड के बाएं पैनल में पक्का करें कि Explorer चुना गया है और pubspec.yaml फ़ाइल खोलें.

e2a5bab0be07f4f7.png

इस फ़ाइल की सामग्री को इनसे बदलें:

pubspec.yaml

name: namer_app
description: A new Flutter project.

publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 0.0.1+1

environment:
  sdk: ^3.1.1

dependencies:
  flutter:
    sdk: flutter

  english_words: ^4.0.0
  provider: ^6.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true

pubspec.yaml फ़ाइल आपके ऐप्लिकेशन के बारे में बुनियादी जानकारी के बारे में बताती है. जैसे, ऐप्लिकेशन का मौजूदा वर्शन, उसकी डिपेंडेंसी, और वे एसेट जिनसे ऐप्लिकेशन शिप किया जाएगा.

इसके बाद, प्रोजेक्ट में एक अन्य कॉन्फ़िगरेशन फ़ाइल खोलें, analysis_options.yaml.

a781f218093be8e0.png

इसकी सामग्री को निम्न से बदलें:

analysis_options.yaml

include: package:flutter_lints/flutter.yaml

linter:
  rules:
    avoid_print: false
    prefer_const_constructors_in_immutables: false
    prefer_const_constructors: false
    prefer_const_literals_to_create_immutables: false
    prefer_final_fields: false
    unnecessary_breaks: true
    use_key_in_widget_constructors: false

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

आखिर में, main.dart फ़ाइल को lib/ डायरेक्ट्री में खोलें.

e54c671c9bb4d23d.png

इस फ़ाइल की सामग्री को इनसे बदलें:

lib/main.dart

import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Namer App',
        theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    return Scaffold(
      body: Column(
        children: [
          Text('A random idea:'),
          Text(appState.current.asLowerCase),
        ],
      ),
    );
  }
}

ये 50 पंक्तियों वाले कोड अब तक पूरे ऐप्लिकेशन में हैं.

अगले सेक्शन में, ऐप्लिकेशन को डीबग मोड में चलाएं और डेवलप करना शुरू करें.

4. बटन जोड़ें

यह चरण एक नया शब्द पेयर जनरेट करने के लिए आगे बढ़ें बटन जोड़ता है.

ऐप्लिकेशन लॉन्च करें

सबसे पहले, lib/main.dart खोलें और यह पक्का करें कि आपने अपना टारगेट डिवाइस चुना हो. बनाम कोड के नीचे दाएं कोने में, आपको मौजूदा टारगेट डिवाइस को दिखाने वाला बटन मिलेगा. इसे बदलने के लिए क्लिक करें.

जब lib/main.dart खुला हो, तब "चलाएं" ढूंढें वीएस कोड की विंडो के ऊपरी दाएं कोने में b0a5d0200af5985d.png बटन पर क्लिक करें और उस पर क्लिक करें.

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

f96e7dfb0937d7f4.png

पहला हॉट रीलोड

lib/main.dart के सबसे नीचे, पहले Text ऑब्जेक्ट की स्ट्रिंग में कुछ जोड़ें और फ़ाइल को सेव करें (Ctrl+S या Cmd+S के साथ). जैसे:

lib/main.dart

// ...

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),  // ← Example change.
          Text(appState.current.asLowerCase),
        ],
      ),
    );

// ...

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

अक्सर पूछे जाने वाले सवाल

बटन जोड़ना

इसके बाद, Column के नीचे, Text के दूसरे इंस्टेंस के ठीक नीचे बटन जोड़ें.

lib/main.dart

// ...

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),
          Text(appState.current.asLowerCase),

          // ↓ Add this.
          ElevatedButton(
            onPressed: () {
              print('button pressed!');
            },
            child: Text('Next'),
          ),

        ],
      ),
    );

// ...

बदलाव को सेव करने के बाद, ऐप्लिकेशन फिर से अपडेट हो जाता है: एक बटन दिखता है और जब इस बटन पर क्लिक किया जाता है, तो बनाम कोड में मौजूद डीबग कंसोल में एक बटन दबाया गया! मैसेज दिखता है.

Flutter क्रैश कोर्स 5 मिनट में पूरा होगा

डीबग कंसोल देखने में ही बहुत मज़ा आ सकता है, आप चाहते हैं कि बटन कोई काम कर सके. हालांकि, इससे पहले, lib/main.dart में मौजूद कोड को ध्यान से देखें, ताकि आपको यह पता चल सके कि यह कैसे काम करता है.

lib/main.dart

// ...

void main() {
  runApp(MyApp());
}

// ...

इस फ़ाइल में सबसे ऊपर, आपको main() फ़ंक्शन दिखेगा. अपने मौजूदा फ़ॉर्म में, यह Flutter को सिर्फ़ MyApp में तय किए गए ऐप्लिकेशन को चलाने के लिए कहता है.

lib/main.dart

// ...

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Namer App',
        theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

// ...

MyApp क्लास की वैल्यू StatelessWidget है. विजेट ऐसे एलिमेंट होते हैं जिनसे आप हर Flutter ऐप्लिकेशन बनाते हैं. जैसा कि आपको दिख रहा है कि ऐप्लिकेशन भी एक विजेट है.

MyApp में मौजूद कोड पूरे ऐप्लिकेशन को सेट अप करता है. इससे पूरे ऐप्लिकेशन की स्थिति बनती है (इस बारे में ज़्यादा जानकारी बाद में दी जाएगी), ऐप्लिकेशन को नाम दिया जाएगा, विज़ुअल थीम के बारे में बताया जाएगा, और "होम" सेट किया जाएगा विजेट—आपके ऐप्लिकेशन का शुरुआती बिंदु.

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

// ...

इसके बाद, MyAppState क्लास ऐप्लिकेशन की...अच्छी तरह से...स्थिति के बारे में बताती है. यह Flutter में आपका पहला कदम है. इसलिए, यह कोडलैब इसे आसान और फ़ोकस बनाए रखेगा. Flutter में ऐप्लिकेशन की स्थिति को मैनेज करने के कई असरदार तरीके हैं. इस ऐप्लिकेशन ने जो तरीका अपनाया है उसे ChangeNotifier सबसे आसानी से समझा जा सकता है.

  • MyAppState वह डेटा बताता है जो ऐप्लिकेशन को काम करने के लिए चाहिए. फ़िलहाल, इसमें किसी भी क्रम में मौजूद शब्द जोड़े वाला सिर्फ़ एक वैरिएबल होता है. आप इसे बाद में जोड़ सकते हैं.
  • स्टेट क्लास, ChangeNotifier को बढ़ाती है, जिसका मतलब है कि वह दूसरों को अपने बदलावों के बारे में सूचना दे सकती है. उदाहरण के लिए, अगर मौजूदा वर्ड पेयर बदलता है, तो ऐप्लिकेशन के कुछ विजेट के बारे में जानकारी होनी चाहिए.
  • ChangeNotifierProvider (ऊपर MyApp में दिया गया कोड देखें) का इस्तेमाल करके, पूरे ऐप्लिकेशन के लिए राज्य बनाया और उपलब्ध कराया जाता है. इससे ऐप्लिकेशन का कोई भी विजेट, अपने राज्य को होल्ड कर सकता है. d9b6ecac5494a6ff.png

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {           // ← 1
    var appState = context.watch<MyAppState>();  // ← 2

    return Scaffold(                             // ← 3
      body: Column(                              // ← 4
        children: [
          Text('A random AWESOME idea:'),        // ← 5
          Text(appState.current.asLowerCase),    // ← 6
          ElevatedButton(
            onPressed: () {
              print('button pressed!');
            },
            child: Text('Next'),
          ),
        ],                                       // ← 7
      ),
    );
  }
}

// ...

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

  1. हर विजेट एक build() तरीका तय करता है. विजेट की स्थितियां बदलने पर, हर बार अपने-आप कॉल होता है, ताकि विजेट हमेशा अप-टू-डेट रहे.
  2. MyHomePage watch तरीके का इस्तेमाल करके, ऐप्लिकेशन की मौजूदा स्थिति में होने वाले बदलावों को ट्रैक करता है.
  3. build के हर तरीके के लिए, एक विजेट या (आम तौर पर) विजेट का एक नेस्ट किया हुआ पेड़ दिखना चाहिए. इस मामले में, टॉप-लेवल का विजेट Scaffold है. इस कोडलैब में Scaffold का इस्तेमाल नहीं किया जा सकता. हालांकि, यह एक मददगार विजेट है और यह असल दुनिया के ज़्यादातर Flutter ऐप्लिकेशन में मौजूद है.
  4. Column, Flutter में मौजूद सबसे बेसिक लेआउट विजेट में से एक है. यह जितने चाहें उतने बच्चों का इस्तेमाल करता है और उन्हें ऊपर से नीचे की ओर एक कॉलम में रखता है. डिफ़ॉल्ट रूप से, कॉलम में विज़ुअल तौर पर, चाइल्ड खाते सबसे ऊपर दिखते हैं. आपको जल्द ही इसे बदल देना चाहिए, ताकि कॉलम बीच में हो जाए.
  5. आपने पहले चरण में इस Text विजेट को बदल दिया है.
  6. यह दूसरा Text विजेट appState को लेता है और उस क्लास के अकेले सदस्य current (जो कि WordPair है) को ऐक्सेस करता है. WordPair कई मददगार गैटर उपलब्ध कराता है, जैसे कि asPascalCase या asSnakeCase. यहां हम asLowerCase का इस्तेमाल करते हैं. हालांकि, अगर आपको कोई विकल्प पसंद है, तो इसे अब बदलें.
  7. ध्यान दें कि Flutter कोड, आखिरी कॉमा का ज़्यादा इस्तेमाल कैसे करता है. इस कॉमा का यहां होना ज़रूरी नहीं है, क्योंकि children इस खास Column पैरामीटर सूची का आखिरी (और सिर्फ़) सदस्य है. हालांकि, आम तौर पर आखिर में कॉमा का इस्तेमाल करना अच्छा होता है: वे ज़्यादा सदस्यों को जोड़ना आसान बना देते हैं. साथ ही, ये Dart के ऑटो-फ़ॉर्मैटर के लिए, एक नई लाइन डालने के संकेत के रूप में भी काम करते हैं. ज़्यादा जानकारी के लिए, कोड फ़ॉर्मैटिंग देखें.

इसके बाद, आपको बटन को स्टेट से कनेक्ट करना होगा.

आपका पहला व्यवहार

स्क्रोल करके MyAppState पर जाएं और कोई getNext तरीका जोड़ें.

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  // ↓ Add this.
  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }
}

// ...

getNext() वाले नए तरीके से, current को किसी नए रैंडम WordPair के साथ फिर से असाइन किया जाएगा. यह notifyListeners() का भी इस्तेमाल करता है. यह एक ChangeNotifier) तरीका है, जो यह पक्का करता है कि MyAppState देखने वाले लोगों को इसकी सूचना मिले.

बस बटन के कॉलबैक से getNext तरीके को कॉल करना बाकी है.

lib/main.dart

// ...

    ElevatedButton(
      onPressed: () {
        appState.getNext();  // ← This instead of print().
      },
      child: Text('Next'),
    ),

// ...

ऐप्लिकेशन सेव करें और अभी आज़माएं. हर बार आगे बढ़ें बटन दबाने पर, किसी भी रैंडम तरीके से शब्दों का एक नया जोड़ा जनरेट होता है.

अगले सेक्शन में, इसके यूज़र इंटरफ़ेस को बेहतर बनाया गया है.

5. ऐप्लिकेशन को शानदार बनाएं

ऐप्लिकेशन इस समय ऐसा दिखता है.

3dd8a9d8653bdc56.png

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

यह सेक्शन, ऐप्लिकेशन के डिज़ाइन पर काम करके इन समस्याओं को हल करता है. इस सेक्शन का आखिरी लक्ष्य कुछ ऐसा है:

2bbee054d81a3127.png

विजेट निकालना

वर्तमान शब्द युग्म को दिखाने के लिए ज़िम्मेदार पंक्ति अब ऐसी दिखती है: Text(appState.current.asLowerCase). इसे ज़्यादा जटिल बनाने के लिए, इस लाइन को एक अलग विजेट में बदलना बेहतर होगा. अपने यूज़र इंटरफ़ेस (यूआई) के अलग-अलग लॉजिकल हिस्सों के लिए अलग-अलग विजेट होना, Flutter में जटिलता को मैनेज करने का एक अहम तरीका है.

Flutter विजेट निकालने के लिए रिफै़क्टरिंग हेल्पर उपलब्ध कराता है, लेकिन इसका इस्तेमाल करने से पहले यह पक्का कर लें कि निकाली जा रही लाइन सिर्फ़ ज़रूरत की चीज़ें ऐक्सेस कर रही हो. फ़िलहाल, यह पंक्ति appState तक पहुंचती है, लेकिन आपको सिर्फ़ यह जानने की ज़रूरत है कि वर्तमान शब्द युग्म क्या है.

इसी वजह से, MyHomePage विजेट को इस तरह फिर से लिखें:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();  
    var pair = appState.current;                 // ← Add this.

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),
          Text(pair.asLowerCase),                // ← Change to this.
          ElevatedButton(
            onPressed: () {
              appState.getNext();
            },
            child: Text('Next'),
          ),
        ],
      ),
    );
  }
}

// ...

बढ़िया। Text विजेट अब पूरे appState को नहीं दिखाता.

अब, Refactor मेन्यू को कॉल करें. बनाम कोड में, इन दो में से किसी एक तरीके से ऐसा किया जा सकता है:

  1. कोड के उस हिस्से पर राइट क्लिक करें जिसे आपको रीफ़ैक्टर करना है (इस मामले में Text) और ड्रॉप-डाउन मेन्यू से रिफ़ैक्टर... चुनें,

या

  1. अपने कर्सर को उस पीस कोड पर ले जाएं जिसे रीफ़ैक्टर करना है (इस मामले में, Text) और Ctrl+. (Win/Linux) या Cmd+. (Mac) दबाएं.

रिफ़ैक्टर मेन्यू में, एक्सट्रैक्ट विजेट चुनें. कोई नाम असाइन करें, जैसे कि BigCard और Enter पर क्लिक करें.

इससे मौजूदा फ़ाइल के आखिर में, अपने-आप एक नई क्लास BigCard बन जाती है. क्लास कुछ ऐसा दिखता है:

lib/main.dart

// ...

class BigCard extends StatelessWidget {
  const BigCard({
    super.key,
    required this.pair,
  });

  final WordPair pair;

  @override
  Widget build(BuildContext context) {
    return Text(pair.asLowerCase);
  }
}

// ...

देखें कि इस रीफ़ैक्टरिंग के बावजूद ऐप्लिकेशन कैसे काम करता रहता है.

कार्ड जोड़ें

अब इस नए विजेट को यूज़र इंटरफ़ेस (यूआई) के उस बोल्ड हिस्से में बनाने का समय आ गया है, जिसकी कल्पना हमने इस सेक्शन की शुरुआत में की थी.

BigCard क्लास और उसमें मौजूद build() तरीका ढूंढें. पहले की तरह ही, Text विजेट पर Refactor मेन्यू को कॉल करें. हालांकि, इस बार आपको बैज को एक्सट्रैक्ट करने की ज़रूरत नहीं है.

इसके बजाय, Wrap with पैडिंग को चुनें. इससे Text विजेट की जगह Padding नाम का एक नया पैरंट विजेट बन जाएगा. सेव करने के बाद, आप देखेंगे कि रैंडम शब्द के लिए पहले से ही ज़्यादा जगह खाली है.

8.0 की डिफ़ॉल्ट वैल्यू से पैडिंग (जगह) बढ़ाएं. उदाहरण के लिए, स्पेसर पैडिंग के लिए 20 जैसा कुछ इस्तेमाल करें.

अब एक लेवल ऊपर जाएं. अपना कर्सर Padding विजेट पर रखें, रीफ़ैक्टर मेन्यू को ऊपर खोलें, और विजेट के साथ रैप करें... को चुनें.

इससे आपको पैरंट विजेट तय करने की सुविधा मिलती है. "कार्ड" टाइप करें और Enter दबाएं.

इससे Card विजेट के साथ, Padding विजेट और Text भी रैप होता है.

6031adbc0a11e16b.png

थीम और स्टाइल

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

BigCard के build() तरीके में ये बदलाव करें.

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);       // ← Add this.

    return Card(
      color: theme.colorScheme.primary,    // ← And also this.
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Text(pair.asLowerCase),
      ),
    );
  }

// ...

ये दो नई लाइनें बहुत काम की हैं:

  • सबसे पहले, कोड, ऐप्लिकेशन की मौजूदा थीम के लिए Theme.of(context) का अनुरोध करता है.
  • इसके बाद, कोड आपके कार्ड के रंग को थीम की colorScheme प्रॉपर्टी की तरह ही बताता है. कलर स्कीम में कई रंग होते हैं. साथ ही, primary ऐप्लिकेशन का सबसे खास रंग है.

कार्ड को अब ऐप्लिकेशन के मुख्य रंग से पेंट कर दिया गया है:

a136f7682c204ea1.png

इस रंग और पूरे ऐप्लिकेशन की कलर स्कीम को बदला जा सकता है. इसके लिए, MyApp तक स्क्रोल करें और वहां ColorScheme के लिए सीड का रंग बदलें.

ध्यान दें कि रंग आसानी से कैसे ऐनिमेट होता है. इसे इंप्लिसिट ऐनिमेशन कहा जाता है. कई Flutter विजेट, वैल्यू के बीच आसानी से इंटरपोलेट करते हैं, ताकि यूज़र इंटरफ़ेस (यूआई) सिर्फ़ "जंप" न हो राज्यों के बीच होगा.

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

TextTheme

कार्ड में अब भी समस्या है: टेक्स्ट बहुत छोटा है और इसका रंग पढ़ने में मुश्किल है. इसे ठीक करने के लिए, BigCard के build() तरीके में ये बदलाव करें.

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    // ↓ Add this.
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );

    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),
        // ↓ Change this line.
        child: Text(pair.asLowerCase, style: style),
      ),
    );
  }

// ...

इस बदलाव के पीछे क्या है:

  • theme.textTheme, का इस्तेमाल करने पर, ऐप्लिकेशन की फ़ॉन्ट थीम को ऐक्सेस किया जाता है. इस क्लास में सदस्य शामिल होते हैं, जैसे कि bodyMedium (मीडियम साइज़ के स्टैंडर्ड टेक्स्ट के लिए), caption (इमेज के कैप्शन के लिए) या headlineLarge (बड़ी हेडलाइन के लिए).
  • displayMedium प्रॉपर्टी एक बड़ी स्टाइल है, जिसका इस्तेमाल टेक्स्ट दिखाने के लिए किया जाता है. यहां डिसप्ले शब्द का इस्तेमाल टाइपोग्राफ़िक के तौर पर किया गया है, जैसे कि डिसप्ले टाइपफ़ेस में. displayMedium के दस्तावेज़ में बताया गया है कि "डिसप्ले स्टाइल, छोटे और ज़रूरी टेक्स्ट के लिए रिज़र्व होती हैं"—यह सुविधा असल में हमारे इस्तेमाल के उदाहरण है.
  • थीम की displayMedium प्रॉपर्टी null हो सकती है. Dart, जिस प्रोग्रामिंग भाषा में इस ऐप्लिकेशन को लिखा जा रहा है वह शून्य से सुरक्षित है. इसलिए, यह आपको ऑब्जेक्ट के उन तरीकों को कॉल नहीं करने देगा जो संभावित तौर पर null हो सकते हैं. हालांकि, इस मामले में आप ! ऑपरेटर ("बैंग ऑपरेटर") का इस्तेमाल करके यह पक्का कर सकते हैं कि Dart को जानकारी है कि आप क्या कर रहे हैं. (इस मामले में displayMedium निश्चित तौर पर शून्य नहीं है. हालांकि, हमें यह पता चला है कि यह कोडलैब के दायरे से बाहर है.)
  • displayMedium को copyWith() को कॉल करने से, आपके तय किए गए बदलावों के साथ टेक्स्ट स्टाइल की कॉपी दिखेगी. इस मामले में, सिर्फ़ टेक्स्ट का रंग बदला जा रहा है.
  • नया रंग पाने के लिए, आपको एक बार फिर से ऐप्लिकेशन की थीम का ऐक्सेस मिलेगा. कलर स्कीम की onPrimary प्रॉपर्टी ऐसे रंग के बारे में बताती है जो ऐप्लिकेशन के मुख्य रंग पर इस्तेमाल करने के लिए सही होता है.

ऐप्लिकेशन अब कुछ ऐसा दिखना चाहिए:

2405e9342d28c193.png

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

  • copyWith() से आपको टेक्स्ट के रंग के साथ-साथ, टेक्स्ट की स्टाइल में भी बहुत कुछ बदलाव करने की सुविधा मिलती है. बदली जा सकने वाली प्रॉपर्टी की पूरी सूची पाने के लिए, अपने कर्सर को copyWith() के ब्रैकेट में कहीं भी रखें और Ctrl+Shift+Space (Win/Linux) या Cmd+Shift+Space (Mac) दबाएं.
  • इसी तरह, Card विजेट के बारे में ज़्यादा जानकारी दी जा सकती है. उदाहरण के लिए, elevation पैरामीटर की वैल्यू बढ़ाकर, कार्ड के शैडो को बड़ा किया जा सकता है.
  • रंगों के साथ एक्सपेरिमेंट करके देखें. theme.colorScheme.primary के अलावा, .secondary, .surface, और कई अन्य कार्यक्रम हैं. इन सभी रंगों के onPrimary जैसे हैं.

सुलभता को बेहतर बनाना

Flutter का इस्तेमाल करके, ऐप्लिकेशन को डिफ़ॉल्ट रूप से ऐक्सेस किया जा सकता है. उदाहरण के लिए, हर Flutter ऐप्लिकेशन, स्क्रीन रीडर जैसे TalkBack और VoiceOver जैसे टेक्स्ट और इंटरैक्टिव एलिमेंट को सही तरीके से दिखाता है.

d1fad7944fb890ea.png

हालांकि, कभी-कभी कुछ काम करना पड़ता है. ऐसे ऐप्लिकेशन में, स्क्रीन रीडर को जनरेट किए गए कुछ वर्ड पेयर को बोलने में समस्याएं हो सकती हैं. हालांकि, लोगों को cheaphead में मौजूद दो शब्दों को पहचानने में कोई समस्या नहीं हो रही है, लेकिन स्क्रीन रीडर शब्द के बीच में ph का उच्चारण f कर सकता है.

इसका एक आसान समाधान है कि pair.asLowerCase को "${pair.first} ${pair.second}" से बदलें. बाद में, pair में शामिल दो शब्दों से स्ट्रिंग (जैसे कि "cheap head") बनाने के लिए, स्ट्रिंग इंटरपोलेशन का इस्तेमाल किया जाता है. कंपाउंड शब्द के बजाय दो अलग-अलग शब्दों का इस्तेमाल करने से यह पक्का हो जाता है कि स्क्रीन रीडर उन्हें सही तरीके से पहचान पाते हैं और दृष्टि बाधित उपयोगकर्ताओं को बेहतर अनुभव दे पाते हैं.

हालांकि, आपको pair.asLowerCase का विज़ुअल आसान रखना चाहिए. Text की semanticsLabel प्रॉपर्टी का इस्तेमाल करके, टेक्स्ट विजेट के विज़ुअल कॉन्टेंट को सिमैंटिक कॉन्टेंट से बदलें, जो स्क्रीन रीडर के लिए ज़्यादा सही हो:

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );

    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),

        // ↓ Make the following change.
        child: Text(
          pair.asLowerCase,
          style: style,
          semanticsLabel: "${pair.first} ${pair.second}",
        ),
      ),
    );
  }

// ...

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

यूज़र इंटरफ़ेस (यूआई) को बीच में लाएं

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

सबसे पहले, याद रखें कि BigCard, Column का हिस्सा है. डिफ़ॉल्ट रूप से, कॉलम में बच्चे सबसे ऊपर दिखते हैं. हालांकि, हम इसे आसानी से बदल सकते हैं. MyHomePage के build() तरीके पर जाकर, यह बदलाव करें:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,  // ← Add this.
        children: [
          Text('A random AWESOME idea:'),
          BigCard(pair: pair),
          ElevatedButton(
            onPressed: () {
              appState.getNext();
            },
            child: Text('Next'),
          ),
        ],
      ),
    );
  }
}

// ...

यह बच्चों को Column के मुख्य (वर्टिकल) ऐक्सिस पर सेंटर करता है.

b555d4c7f5000edf.png

चाइल्ड एंट्री कॉलम के क्रॉस ऐक्सिस पर पहले से ही फ़ोकस में होती हैं (दूसरे शब्दों में, वे पहले से ही हॉरिज़ॉन्टल तौर पर सेंटर में होती हैं). हालांकि, Column खुद Scaffold के अंदर नहीं है. हम विजेट की जांच करने वाले टूल का इस्तेमाल करके, इसकी पुष्टि कर सकते हैं.

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

सिर्फ़ कॉलम को बीच में रखा जा सकता है. अपना कर्सर Column पर रखें, रिफ़ैक्टर मेन्यू (Ctrl+. या Cmd+. के साथ) को कॉल करें और सेंटर के साथ रैप करें को चुनें.

ऐप्लिकेशन अब कुछ ऐसा दिखना चाहिए:

455688d93c30d154.png

अगर आप चाहें, तो इसमें थोड़ा और बदलाव कर सकते हैं.

  • BigCard के ऊपर दिया गया Text विजेट हटाया जा सकता है. यह तर्क दिया जा सकता है कि जानकारी देने वाले टेक्स्ट ("एक रैंडम AWESOME आइडिया:") की अब ज़रूरत नहीं है, क्योंकि इसके बिना भी यूज़र इंटरफ़ेस (यूआई) समझ में आता है. और इस तरह से यह ज़्यादा साफ़ होता है.
  • BigCard से ElevatedButton के बीच का भी SizedBox(height: 10) विजेट जोड़ा जा सकता है. इस तरह से, दो विजेट के बीच थोड़ा ज़्यादा अंतर होता है. SizedBox विजेट सिर्फ़ जगह लेता है और अपने-आप कुछ भी रेंडर नहीं करता. आम तौर पर, इसका इस्तेमाल विज़ुअल "गैप" बनाने के लिए किया जाता है.

वैकल्पिक बदलावों के साथ, MyHomePage में यह कोड शामिल है:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            ElevatedButton(
              onPressed: () {
                appState.getNext();
              },
              child: Text('Next'),
            ),
          ],
        ),
      ),
    );
  }
}

// ...

ऐप्लिकेशन कुछ ऐसा दिखता है:

3d53d2b071e2f372.png

अगले सेक्शन में, जनरेट किए गए शब्दों को पसंदीदा (या ‘पसंद’) करने की सुविधा जोड़ी जाएगी.

6. फ़ंक्शन जोड़ें

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

e6b01a8c90df8ffa.png

कारोबार का लॉजिक जोड़ें

स्क्रोल करके MyAppState पर जाएं और यह कोड जोड़ें:

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }

  // ↓ Add the code below.
  var favorites = <WordPair>[];

  void toggleFavorite() {
    if (favorites.contains(current)) {
      favorites.remove(current);
    } else {
      favorites.add(current);
    }
    notifyListeners();
  }
}

// ...

बदलावों की जांच करें:

  • आपने MyAppState में favorites नाम की एक नई प्रॉपर्टी जोड़ी है. इस प्रॉपर्टी को एक खाली सूची के साथ शुरू किया गया है: [].
  • आपने यह भी बताया कि सूची में कभी भी सिर्फ़ शब्द जोड़े हो सकते हैं: <WordPair>[], सामान्य का इस्तेमाल करके. इससे आपके ऐप्लिकेशन को और बेहतर बनाने में मदद मिलती है—अगर डार्ट आपके ऐप्लिकेशन में WordPair के अलावा कुछ और जोड़ने की कोशिश करते हैं, तो वह आपके ऐप्लिकेशन को चलाना भी नहीं छोड़ता. इसके बदले में, favorites सूची का इस्तेमाल करके यह पता लगाया जा सकता है कि इसमें कभी भी कोई अनचाही चीज़ नहीं छिप सकती, जैसे कि null.
  • आपने एक नई विधि भी जोड़ी है, toggleFavorite(), जो या तो पसंदीदा की सूची से वर्तमान शब्द युग्म को निकाल देती है (अगर वह पहले से वहां मौजूद है) या उसे जोड़ देती है (अगर वह अभी तक वहां नहीं है). दोनों ही मामलों में, कोड बाद में notifyListeners(); को कॉल करता है.

बटन जोड़ें

"कारोबार के नियम" सेटिंग के साथ अब यूज़र इंटरफ़ेस पर फिर से काम किया जा सकता है. 'पसंद करें' बटन को क्रम से लगाना ‘आगे बढ़ें’ बटन पर क्लिक करें बटन के लिए Row की ज़रूरत है. Row विजेट, Column के बराबर हॉरिज़ॉन्टल है, जिसे आपने पहले देखा था.

सबसे पहले, मौजूदा बटन को Row में रैप करें. MyHomePage के build() वाले तरीके पर जाएं, अपना कर्सर ElevatedButton पर रखें, Ctrl+. या Cmd+. के साथ रीफ़ैक्टर मेन्यू को कॉल करें, और लाइन के साथ रैप करें को चुनें.

सेव करते समय, आप देखेंगे कि Row, Column की तरह ही काम करता है—डिफ़ॉल्ट रूप से, यह अपने बच्चों को बाईं ओर ले जाता है. (Column ने अपने बच्चों को सबसे ऊपर ले जाया.) इसे ठीक करने के लिए, पहले वाले तरीके का इस्तेमाल करें. हालांकि, mainAxisAlignment के साथ ऐसा किया जा सकता है. हालांकि, सीखने-सिखाने के लिए, mainAxisSize का इस्तेमाल करें. ऐसा करने से, Row को सभी उपलब्ध हॉरिज़ॉन्टल स्पेस नहीं लेने पड़ते.

ये बदलाव करें:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,   // ← Add this.
              children: [
                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

यूज़र इंटरफ़ेस (यूआई), वहीं पर मौजूद है जहां वह पहले था.

3d53d2b071e2f372.png

इसके बाद, पसंद करें बटन जोड़ें और उसे toggleFavorite() से कनेक्ट करें. चुनौती पूरी करने के लिए, पहले नीचे दिए गए कोड ब्लॉक को देखे बिना, खुद ही इसे करने की कोशिश करें.

e6b01a8c90df8ffa.png

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

असफल होने में भी कोई बुराई नहीं है. आखिरकार, Flutter के साथ आपका यह पहला घंटा है.

252f7c4a212c94d2.png

MyHomePage में दूसरा बटन जोड़ने का एक तरीका यहां दिया गया है. इस बार, आइकॉन वाला बटन बनाने के लिए, ElevatedButton.icon() कंस्ट्रक्टर का इस्तेमाल करें. साथ ही, build तरीके में सबसे ऊपर, सही आइकॉन को चुनें. यह आइकॉन इस हिसाब से चुना जाता है कि मौजूदा शब्दों का जोड़ा पहले से ही पसंदीदा सूची में शामिल है या नहीं. साथ ही, दोनों बटनों को थोड़ा अलग रखने के लिए SizedBox के इस्तेमाल पर फिर से ध्यान दें.

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    // ↓ Add this.
    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [

                // ↓ And this.
                ElevatedButton.icon(
                  onPressed: () {
                    appState.toggleFavorite();
                  },
                  icon: Icon(icon),
                  label: Text('Like'),
                ),
                SizedBox(width: 10),

                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

ऐप्लिकेशन ऐसा दिखना चाहिए:

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

7. नेविगेशन रेल जोड़ें

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

f62c54f5401a187.png

इस चरण को पूरा करने के लिए, MyHomePage को दो अलग-अलग विजेट में बांटें.

सभी MyHomePage चुनें, इसे मिटाएं, और इस कोड से बदलें:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: 0,
              onDestinationSelected: (value) {
                print('selected: $value');
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}


class GeneratorPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          BigCard(pair: pair),
          SizedBox(height: 10),
          Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              ElevatedButton.icon(
                onPressed: () {
                  appState.toggleFavorite();
                },
                icon: Icon(icon),
                label: Text('Like'),
              ),
              SizedBox(width: 10),
              ElevatedButton(
                onPressed: () {
                  appState.getNext();
                },
                child: Text('Next'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

// ...

सेव करने के बाद, आपको दिखेगा कि यूज़र इंटरफ़ेस (यूआई) का विज़ुअल साइड तैयार है—लेकिन यह काम नहीं करता. नेविगेशन रेल में ♥︎ (दिल का आइकॉन) क्लिक करने से कुछ नहीं होता.

388bc25fe198c54a.png

बदलावों की जांच करें.

  • सबसे पहले, ध्यान दें कि MyHomePage का पूरा कॉन्टेंट एक नए विजेट, GeneratorPage में एक्सट्रैक्ट किया गया है. पुराने MyHomePage विजेट का सिर्फ़ Scaffold ही एक हिस्सा है, जो निकाला नहीं जा सका.
  • नए MyHomePage में एक Row है, जिसमें दो बच्चे हैं. पहला विजेट SafeArea है और दूसरा Expanded विजेट है.
  • SafeArea यह पक्का करता है कि उसका बच्चा किसी हार्डवेयर नॉच या स्टेटस बार से छिप न गया हो. इस ऐप्लिकेशन में बैज, NavigationRail के चारों ओर रैप हो जाता है, ताकि मोबाइल स्टेटस बार की वजह से नेविगेशन बटन छिप न जाएं.
  • नेविगेशन रेल में extended: false लाइन को true में बदला जा सकता है. यह आइकॉन के बगल में मौजूद लेबल दिखाता है. आगे के चरण में, आपको ऐप्लिकेशन में ज़रूरत के मुताबिक हॉरिज़ॉन्टल स्पेस होने पर, इस प्रोसेस को अपने-आप करने का तरीका बताया जाएगा.
  • नेविगेशन रेल में दो मंज़िलें (होम और पसंदीदा) हैं, जिनके आइकॉन और लेबल अलग-अलग हैं. यह मौजूदा selectedIndex के बारे में भी जानकारी देता है. शून्य का चुना गया इंडेक्स, पहले डेस्टिनेशन को चुनता है. एक इंडेक्स में से किसी एक इंडेक्स को चुना जाता है. दूसरा डेस्टिनेशन चुना जाता है और यह क्रम इसी तरह जारी रहता है. फ़िलहाल, इसे शून्य पर हार्ड कोड किया गया है.
  • नेविगेशन रेल से यह भी तय होता है कि जब उपयोगकर्ता onDestinationSelected के साथ किसी डेस्टिनेशन को चुनता है, तो क्या होता है. फ़िलहाल, ऐप्लिकेशन सिर्फ़ print() के साथ अनुरोध की गई इंडेक्स वैल्यू दिखाता है.
  • Row का दूसरा चाइल्ड विजेट, Expanded विजेट है. बड़े किए गए विजेट, लाइनों और कॉलम में बहुत काम के होते हैं. इनकी मदद से, कुछ ऐसे लेआउट बनाए जा सकते हैं जिनमें कुछ बच्चे अपनी ज़रूरत के हिसाब से ही जगह लेते हैं (इस मामले में SafeArea). वहीं, दूसरे विजेट, खाली जगह (इस मामले में Expanded) का ज़्यादा से ज़्यादा इस्तेमाल कर सकते हैं. Expanded विजेट के बारे में सोचने का एक तरीका यह है कि वे "लालची" होते हैं. अगर आपको इस विजेट की भूमिका को बेहतर तरीके से समझना है, तो SafeArea विजेट को किसी अन्य Expanded के साथ रैप करके देखें. इससे बनने वाला लेआउट कुछ ऐसा दिखता है:

6bbda6c1835a1ae.png

  • Expanded के दो विजेट, जो उपलब्ध हॉरिज़ॉन्टल स्पेस को उनके बीच बांटते हैं. हालांकि, नेविगेशन रेल को बाईं ओर थोड़ा स्लाइस करने की ही ज़रूरत पड़ती थी.
  • Expanded विजेट में, रंगीन Container और कंटेनर के अंदर GeneratorPage मौजूद है.

स्टेटलेस बनाम स्टेटफ़ुल विजेट

अब तक, MyAppState में आपके राज्य की सभी ज़रूरतें पूरी की गई हैं. इसलिए, आपने अब तक जो विजेट लिखे हैं वे स्टेटलेस हैं. उनमें अपनी कोई म्यूटेबल स्थिति नहीं होती. कोई भी विजेट खुद नहीं बदल सकता—उसे MyAppState से गुज़रना होगा.

यह बदलने वाला है.

आपके पास नेविगेशन रेल के selectedIndex की वैल्यू को होल्ड करने का कोई तरीका होना चाहिए. आपको onDestinationSelected कॉलबैक में जाकर भी इस वैल्यू को बदलना है.

आप selectedIndex को MyAppState की दूसरी प्रॉपर्टी के तौर पर जोड़ सकते हैं. और यह काम भी करेगा. हालांकि, यह कल्पना भी की जा सकती है कि अगर हर विजेट की वैल्यू इसमें सेव होती है, तो ऐप्लिकेशन की मौजूदा स्थिति और बेहतर हो जाएगी.

e52d9c0937cc0823.jpeg

कुछ राज्य सिर्फ़ एक विजेट के लिए काम के होते हैं. इसलिए, यह उस विजेट के साथ ही रहना चाहिए.

StatefulWidget डालें. यह एक तरह का विजेट है, जिसमें State शामिल है. सबसे पहले, MyHomePage को स्टेटफ़ुल विजेट में बदलें.

अपना कर्सर MyHomePage की पहली लाइन (जो class MyHomePage... से शुरू होता है) पर रखें. इसके बाद, Ctrl+. या Cmd+. का इस्तेमाल करके रीफ़ैक्टर मेन्यू को कॉल करें. इसके बाद, StatefulWidget में बदलें चुनें.

IDE आपके लिए एक नई क्लास बनाता है, _MyHomePageState. यह क्लास State को बढ़ाती है और इसलिए अपने खुद के मान प्रबंधित कर सकती है. (यह खुद बदल सकती है.) यह भी ध्यान दें कि पुराने, स्टेटलेस विजेट की build विधि (विजेट में रहने के बजाय) _MyHomePageState पर स्थानांतरित हो गई है. इसे हर हाल में बदला गया है—build तरीके में कुछ भी नहीं बदला है. यह बस अब कहीं और रहती है.

सेटस्टेट

नए स्टेटफ़ुल विजेट के लिए, सिर्फ़ एक वैरिएबल ट्रैक करना ज़रूरी है: selectedIndex. _MyHomePageState में ये तीन बदलाव करें:

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {

  var selectedIndex = 0;     // ← Add this property.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: selectedIndex,    // ← Change to this.
              onDestinationSelected: (value) {

                // ↓ Replace print with this.
                setState(() {
                  selectedIndex = value;
                });

              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}

// ...

बदलावों की जांच करें:

  1. आपको एक नया वैरिएबल, selectedIndex, और 0 में शुरू करना है.
  2. इस नए वैरिएबल का इस्तेमाल अब तक मौजूद हार्ड कोड किए गए 0 के बजाय, NavigationRail डेफ़िनिशन में किया जा सकता है.
  3. जब onDestinationSelected कॉलबैक को कॉल किया जाता है, तो कंसोल में सिर्फ़ नई वैल्यू को प्रिंट करने के बजाय, इसे setState() कॉल में selectedIndex को असाइन किया जाता है. यह कॉल, पहले इस्तेमाल किए गए notifyListeners() तरीके जैसा ही है. इससे यह पक्का होता है कि यूज़र इंटरफ़ेस (यूआई) अपडेट हो गया है.

नेविगेशन रेल अब उपयोगकर्ता के इंटरैक्शन का जवाब देती है. हालांकि, दाईं ओर के बड़े किए गए हिस्से में कोई बदलाव नहीं होता है. ऐसा इसलिए होता है, क्योंकि कोड यह तय करने के लिए selectedIndex का इस्तेमाल नहीं कर रहा कि कौनसी स्क्रीन दिखे.

चुने गए इंडेक्स का इस्तेमाल करें

नीचे दिए गए कोड को _MyHomePageState के build तरीके के सबसे ऊपर, return Scaffold से ठीक पहले रखें:

lib/main.dart

// ...

Widget page;
switch (selectedIndex) {
  case 0:
    page = GeneratorPage();
    break;
  case 1:
    page = Placeholder();
    break;
  default:
    throw UnimplementedError('no widget for $selectedIndex');
}

// ...

कोड के इस हिस्से की जांच करें:

  1. यह कोड, Widget तरह के एक नए वैरिएबल page के बारे में बताता है.
  2. इसके बाद, स्विच स्टेटमेंट, selectedIndex की मौजूदा वैल्यू के हिसाब से page को एक स्क्रीन असाइन करता है.
  3. फ़िलहाल, कोई FavoritesPage उपलब्ध नहीं है. इसलिए, Placeholder का इस्तेमाल करें; एक आसान विजेट, जो यूज़र इंटरफ़ेस (यूआई) के उस हिस्से को 'पूरा नहीं हुआ' के तौर पर मार्क करते हुए, क्रॉस किए गए रेक्टैंगल की तरह दिखता है.

5685cf886047f6ec.png

  1. फ़ेल-तेज़ सिद्धांत को लागू करने पर, स्विच स्टेटमेंट यह पक्का करता है कि अगर selectedIndex 0 या 1 न हो, तो भी गड़बड़ी मिले. इससे आने वाले समय में गड़बड़ियों को रोकने में मदद मिलती है. अगर कभी नेविगेशन रेल में कोई नया डेस्टिनेशन जोड़ा जाता है और इस कोड को अपडेट करना भूल जाते हैं, तो डेवलपमेंट के दौरान प्रोग्राम क्रैश हो जाता है. इसके बजाय, आपको यह अंदाज़ा लगाने में मदद मिलती है कि चीज़ों के काम नहीं कर रहा है या आपको गड़बड़ी के कोड को प्रोडक्शन में पब्लिश करने की अनुमति देनी है.

अब page में वह विजेट है जिसे आपको दाईं ओर दिखाना है. इसलिए, अब आपको पता चल जाएगा कि और बदलाव करने की ज़रूरत है.

बचे हुए बदलाव के बाद, यह रहा _MyHomePageState:

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {
  var selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    Widget page;
    switch (selectedIndex) {
      case 0:
        page = GeneratorPage();
        break;
      case 1:
        page = Placeholder();
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: selectedIndex,
              onDestinationSelected: (value) {
                setState(() {
                  selectedIndex = value;
                });
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: page,  // ← Here.
            ),
          ),
        ],
      ),
    );
  }
}


// ...

ऐप्लिकेशन अब हमारे GeneratorPage और प्लेसहोल्डर के बीच स्विच कर जाता है, जो जल्द ही पसंदीदा पेज बन जाएगा.

जवाबदेही

इसके बाद, नेविगेशन रेल को रिस्पॉन्सिव बनाएं. इसका मतलब है कि जब लेबल बनाने के लिए ज़रूरी जगह होगी, तब extended: true का इस्तेमाल करके, यह उन्हें अपने-आप लेबल दिखाएगा.

a8873894c32e0d0b.png

Flutter ऐसे कई विजेट उपलब्ध कराता है जो आपके ऐप्लिकेशन को अपने-आप रिस्पॉन्सिव बनाने में मदद करते हैं. उदाहरण के लिए, Wrap, Row या Column से मिलता-जुलता विजेट है, जो बच्चों को अपने-आप अगली "लाइन" पर ले जाता है वर्टिकल या हॉरिज़ॉन्टल स्पेस न होने पर, इसे "रन" कहते हैं. FittedBox एक विजेट है, जो आपकी पसंद के हिसाब से, उपलब्ध जगह में अपने-आप फ़िट हो जाता है.

हालांकि, ज़रूरत के मुताबिक जगह होने पर NavigationRail, लेबल अपने-आप नहीं दिखाता, क्योंकि उसे हर कॉन्टेक्स्ट में जगह के बारे में जानकारी नहीं मिलती. यह आप पर निर्भर करता है कि डेवलपर कौन है.

मान लें कि आपने लेबल सिर्फ़ तब दिखाने का फ़ैसला किया है, जब MyHomePage की चौड़ाई कम से कम 600 पिक्सल हो.

इस मामले में, LayoutBuilder का विजेट इस्तेमाल किया जाएगा. आपके पास कितना स्थान उपलब्ध है, इसके आधार पर यह आपको अपना विजेट ट्री बदलने की सुविधा देता है.

एक बार फिर से, VS कोड में Flutter के Refactor मेन्यू का इस्तेमाल करके ज़रूरी बदलाव करें. हालांकि, इस बार समस्या थोड़ी और मुश्किल है:

  1. _MyHomePageState की build विधि के अंदर, अपना कर्सर Scaffold पर रखें.
  2. Ctrl+. (Windows/Linux) या Cmd+. (Mac) का इस्तेमाल करके, Refactor मेन्यू को कॉल करें.
  3. Wrap with Builder चुनें और Enter दबाएं.
  4. जोड़े गए नए Builder के नाम को LayoutBuilder में बदलें.
  5. कॉलबैक पैरामीटर सूची को (context) से (context, constraints) में बदलें.

हर बार कंस्ट्रेंट बदलने पर, LayoutBuilder के builder कॉलबैक को कॉल किया जाता है. ऐसा तब होता है, जब, उदाहरण के लिए:

  • उपयोगकर्ता, ऐप्लिकेशन की विंडो का साइज़ बदलता है
  • उपयोगकर्ता अपने फ़ोन को पोर्ट्रेट मोड से लैंडस्केप मोड में या पीछे की ओर घुमाता है
  • MyHomePage के बगल में मौजूद कुछ विजेट का साइज़ बड़ा हो जाता है. इससे MyHomePage का कंस्ट्रेंट कम हो जाता है
  • और ऐसे ही अन्य

अब आपका कोड यह तय कर सकता है कि मौजूदा constraints की क्वेरी करके, लेबल को दिखाया जाए या नहीं. _MyHomePageState के build वाले तरीके में, एक लाइन में यह बदलाव करें:

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {
  var selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    Widget page;
    switch (selectedIndex) {
      case 0:
        page = GeneratorPage();
        break;
      case 1:
        page = Placeholder();
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

    return LayoutBuilder(builder: (context, constraints) {
      return Scaffold(
        body: Row(
          children: [
            SafeArea(
              child: NavigationRail(
                extended: constraints.maxWidth >= 600,  // ← Here.
                destinations: [
                  NavigationRailDestination(
                    icon: Icon(Icons.home),
                    label: Text('Home'),
                  ),
                  NavigationRailDestination(
                    icon: Icon(Icons.favorite),
                    label: Text('Favorites'),
                  ),
                ],
                selectedIndex: selectedIndex,
                onDestinationSelected: (value) {
                  setState(() {
                    selectedIndex = value;
                  });
                },
              ),
            ),
            Expanded(
              child: Container(
                color: Theme.of(context).colorScheme.primaryContainer,
                child: page,
              ),
            ),
          ],
        ),
      );
    });
  }
}


// ...

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

बस Placeholder को ही एक असली पसंदीदा स्क्रीन से बदलना ही बचा है. इसकी जानकारी अगले सेक्शन में दी गई है.

8. नया पेज जोड़ें

क्या आपको याद है कि हमने पसंदीदा पेज के बजाय, Placeholder विजेट का इस्तेमाल किया था?

इसे ठीक करने का समय आ गया है.

अगर आपको ज़्यादा रोमांच महसूस होता है, तो खुद ही इसे करने की कोशिश करें. आपका लक्ष्य, favorites की सूची को एक नए स्टेटलेस विजेट FavoritesPage में दिखाना है. इसके बाद, Placeholder की जगह वह विजेट दिखाना है.

यहां कुछ पॉइंटर दिए गए हैं:

  • जब स्क्रोल करने वाला Column हो, तब ListView विजेट का इस्तेमाल करें.
  • याद रखें, context.watch<MyAppState>() का इस्तेमाल करके, किसी भी विजेट से MyAppState इंस्टेंस को ऐक्सेस करें.
  • अगर आपको कोई नया विजेट भी आज़माना है, तो ListTile में title (आम तौर पर, टेक्स्ट के लिए), leading (आइकॉन या अवतार के लिए) और onTap (इंटरैक्शन के लिए) जैसी प्रॉपर्टी उपलब्ध कराई जाती हैं. हालांकि, आपको जिन विजेट के बारे में पहले से पता है उनका इस्तेमाल करके, ऐसे ही इफ़ेक्ट हासिल किए जा सकते हैं.
  • Dart, संग्रह की लिटरल वैल्यू में for लूप का इस्तेमाल करने की अनुमति देता है. उदाहरण के लिए, अगर messages में स्ट्रिंग की सूची शामिल है, तो आपको ऐसा कोड मिल सकता है:

f0444bba08f205aa.png

दूसरी ओर, अगर आप फ़ंक्शनल प्रोग्रामिंग से ज़्यादा परिचित हैं, तो Dart आपको messages.map((m) => Text(m)).toList() की तरह कोड भी लिखने की सुविधा देता है. साथ ही, आपके पास किसी भी समय विजेट की सूची बनाने और उसे build तरीके में जोड़ने का विकल्प है.

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

252f7c4a212c94d2.png

इसके बाद, पसंदीदा पेज को लागू करने का सिर्फ़ एक तरीका होता है. इसे लागू करने का तरीका, (उम्मीद है कि) आपको कोड के साथ काम करने के लिए प्रेरित करेगा—यूज़र इंटरफ़ेस में सुधार करेगा और उसे अपने हिसाब से बनाएगा.

यह रही FavoritesPage की नई क्लास:

lib/main.dart

// ...

class FavoritesPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    if (appState.favorites.isEmpty) {
      return Center(
        child: Text('No favorites yet.'),
      );
    }

    return ListView(
      children: [
        Padding(
          padding: const EdgeInsets.all(20),
          child: Text('You have '
              '${appState.favorites.length} favorites:'),
        ),
        for (var pair in appState.favorites)
          ListTile(
            leading: Icon(Icons.favorite),
            title: Text(pair.asLowerCase),
          ),
      ],
    );
  }
}

विजेट ये काम करता है:

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

अब बस Placeholder विजेट को FavoritesPage से बदलना है. और वाह!

आपको इस ऐप्लिकेशन का फ़ाइनल कोड, GitHub पर कोडलैब रेपो में मिल सकता है.

9. अगले चरण

बधाई हो!

ख़ुद को देखिए! आपने एक Column और दो Text विजेट के साथ काम न करने वाला एक मचान बनाया और उसे एक रिस्पॉन्सिव और मज़ेदार छोटे ऐप्लिकेशन में बदल दिया.

d6e3d5f736411f13.png

हमने इन विषयों के बारे में बताया

  • Flutter के काम करने के तरीके की बुनियादी बातें
  • Flutter में लेआउट बनाना
  • उपयोगकर्ता के इंटरैक्शन (जैसे कि बटन दबाने पर) को ऐप्लिकेशन के व्यवहार से कनेक्ट करना
  • अपने Flutter कोड को व्यवस्थित रखना
  • अपने ऐप्लिकेशन को रिस्पॉन्सिव बनाएं
  • एक जैसा रंग-रूप हासिल करना और CANNOT TRANSLATE

आगे क्या?

  • इस लैब के दौरान लिखे गए ऐप्लिकेशन के साथ ज़्यादा प्रयोग करें.
  • एक ही ऐप्लिकेशन के इस बेहतर वर्शन का कोड देखें और जानें कि आप ऐनिमेशन वाली सूचियां, ग्रेडिएंट, क्रॉस-फ़ेड वगैरह कैसे जोड़ सकते हैं.
  • flutter.dev/learn पर जाकर, अपने सीखने के सफ़र को फ़ॉलो करें.