1. परिचय
Flutter, Google का यूज़र इंटरफ़ेस (यूआई) टूलकिट है. इसकी मदद से, मोबाइल, वेब, और डेस्कटॉप के लिए एक ही कोडबेस से ऐप्लिकेशन बनाए जा सकते हैं. इस कोडलैब में, आपको यह Flutter ऐप्लिकेशन बनाने का तरीका बताया गया है:
यह ऐप्लिकेशन, सुनने में अच्छे लगने वाले नाम जनरेट करता है. जैसे, "newstay", "lightstream", "mainbrake" या "graypine". उपयोगकर्ता, अगले नाम के लिए अनुरोध कर सकता है, मौजूदा नाम को पसंदीदा के तौर पर मार्क कर सकता है, और अलग पेज पर पसंदीदा नामों की सूची देख सकता है. ऐप्लिकेशन, अलग-अलग स्क्रीन साइज़ के हिसाब से काम करता है.
आपको क्या सीखने को मिलेगा
- Flutter के काम करने के तरीके के बारे में बुनियादी जानकारी
- Flutter में लेआउट बनाना
- उपयोगकर्ता के इंटरैक्शन (जैसे, बटन दबाना) को ऐप्लिकेशन के व्यवहार से कनेक्ट करना
- अपने फ़्लटर कोड को व्यवस्थित रखना
- अपने ऐप्लिकेशन को अलग-अलग स्क्रीन के लिए रिस्पॉन्सिव बनाना
- अपने ऐप्लिकेशन का रंग-रूप और स्टाइल एक जैसा बनाए रखना
आपको एक बेसिक स्ट्रक्चर मिलेगा, ताकि आप सीधे दिलचस्प हिस्सों पर जा सकें.

यहां फ़िलिप, आपको पूरे कोडलैब के बारे में बता रहे हैं!
लैब शुरू करने के लिए, 'आगे बढ़ें' पर क्लिक करें.
2. Flutter एनवायरमेंट सेट अप करना
संपादक
इस कोडलैब को जितना हो सके उतना आसान बनाने के लिए, हम यह मानकर चल रहे हैं कि डेवलपमेंट एनवायरमेंट के तौर पर Visual Studio Code (VS Code) का इस्तेमाल किया जाएगा. यह मुफ़्त है और सभी मुख्य प्लैटफ़ॉर्म पर काम करता है.
ज़रूरी नहीं कि आप Android Studio का ही इस्तेमाल करें. आप अपनी पसंद के किसी भी एडिटर का इस्तेमाल कर सकते हैं. जैसे, Android Studio, IntelliJ IDE, Emacs, Vim या Notepad++. ये सभी Flutter के साथ काम करते हैं.
हमारा सुझाव है कि इस कोडलैब के लिए VS Code का इस्तेमाल करें. ऐसा इसलिए, क्योंकि निर्देशों में डिफ़ॉल्ट रूप से VS Code के शॉर्टकट दिए गए हैं. "X करने के लिए, अपने एडिटर में ज़रूरी कार्रवाई करें" जैसे निर्देश देने के बजाय, "यहां क्लिक करें" या "यह बटन दबाएं" जैसे निर्देश देना ज़्यादा आसान होता है.

डेवलपमेंट का कोई टारगेट चुनें
Flutter एक मल्टी-प्लैटफ़ॉर्म टूलकिट है. आपका ऐप्लिकेशन इनमें से किसी भी ऑपरेटिंग सिस्टम पर काम कर सकता है:
- iOS
- Android
- Windows
- macOS
- Linux
- वेब
हालांकि, आम तौर पर एक ही ऑपरेटिंग सिस्टम चुना जाता है, जिसके लिए मुख्य तौर पर डेवलपमेंट किया जाता है. इसे "डेवलपमेंट टारगेट" कहा जाता है. यह वह ऑपरेटिंग सिस्टम होता है जिस पर आपका ऐप्लिकेशन डेवलपमेंट के दौरान चलता है.

उदाहरण के लिए, मान लें कि आपको Flutter ऐप्लिकेशन डेवलप करने के लिए, Windows लैपटॉप का इस्तेमाल करना है. अगर आपने डेवलपमेंट के लिए Android को टारगेट प्लैटफ़ॉर्म के तौर पर चुना है, तो आम तौर पर आपको Android डिवाइस को यूएसबी केबल की मदद से Windows लैपटॉप से कनेक्ट करना होगा. इसके बाद, डेवलपमेंट के दौरान आपका ऐप्लिकेशन, कनेक्ट किए गए Android डिवाइस पर चलेगा. हालांकि, डेवलपमेंट टारगेट के तौर पर Windows को भी चुना जा सकता है. इसका मतलब है कि डेवलपमेंट के दौरान आपका ऐप्लिकेशन, Windows ऐप्लिकेशन के तौर पर आपके एडिटर के साथ चलता है.
ऐसा हो सकता है कि आपको डेवलपमेंट के टारगेट के तौर पर वेब को चुनने का मन करे. इस विकल्प को चुनने का नुकसान यह है कि आपको Flutter की सबसे काम की डेवलपमेंट सुविधाओं में से एक, स्टेटफ़ुल हॉट रिलोड की सुविधा नहीं मिलती. Flutter, वेब ऐप्लिकेशन को हॉट-रीलोड नहीं कर सकता.
अभी अपनी पसंद का विकल्प चुनें. याद रखें: आपके पास बाद में, अपने ऐप्लिकेशन को अन्य ऑपरेटिंग सिस्टम पर चलाने का विकल्प हमेशा होता है. हालांकि, डेवलपमेंट का लक्ष्य तय होने से, अगला चरण आसानी से पूरा किया जा सकता है.
Flutter इंस्टॉल करना
Flutter SDK टूल को इंस्टॉल करने के बारे में सबसे नए निर्देश, हमेशा docs.flutter.dev पर उपलब्ध होते हैं.
Flutter की वेबसाइट पर दिए गए निर्देशों में, न सिर्फ़ एसडीके को इंस्टॉल करने के बारे में बताया गया है, बल्कि डेवलपमेंट टारगेट से जुड़े टूल और एडिटर प्लगिन के बारे में भी बताया गया है. ध्यान रखें कि इस कोडलैब के लिए, आपको सिर्फ़ ये चीज़ें इंस्टॉल करनी होंगी:
- Flutter SDK
- Flutter प्लगिन के साथ Visual Studio Code
- आपके चुने गए डेवलपमेंट टारगेट के लिए ज़रूरी सॉफ़्टवेयर. उदाहरण के लिए: Windows को टारगेट करने के लिए Visual Studio या macOS को टारगेट करने के लिए Xcode
अगले सेक्शन में, अपना पहला Flutter प्रोजेक्ट बनाया जा सकता है.
अगर आपको अब तक समस्याएं आ रही हैं, तो समस्या हल करने के लिए, आपको StackOverflow पर मौजूद इन सवालों और जवाबों से मदद मिल सकती है.
अक्सर पूछे जाने वाले सवाल
- मुझे Flutter SDK का पाथ कैसे मिलेगा?
- अगर Flutter कमांड नहीं मिलती है, तो मुझे क्या करना चाहिए?
- मैं "Waiting for another flutter command to release the startup lock" समस्या को कैसे ठीक करूं?
- मैं Flutter को यह कैसे बताऊं कि मेरा Android SDK कहां इंस्टॉल है?
flutter doctor --android-licensesचलाते समय, Java से जुड़ी गड़बड़ी को कैसे ठीक किया जा सकता है?- मुझे Android
sdkmanagerटूल नहीं मिला, तो मैं क्या करूं? - मैं "
cmdline-toolsकॉम्पोनेंट मौजूद नहीं है" गड़बड़ी को कैसे ठीक करूं? - मैं Apple Silicon (M1) पर CocoaPods कैसे चलाऊं?
- मैं VS Code में सेव करने पर अपने-आप फ़ॉर्मैट होने की सुविधा को कैसे बंद करूं?
3. प्रोजेक्ट बनाना
अपना पहला Flutter प्रोजेक्ट बनाना
Visual Studio Code लॉन्च करें और कमांड पैलेट खोलें (F1 या Ctrl+Shift+P या Shift+Cmd+P की मदद से). "flutter new" टाइप करना शुरू करें. Flutter: New Project कमांड चुनें.
इसके बाद, ऐप्लिकेशन चुनें. इसके बाद, वह फ़ोल्डर चुनें जिसमें आपको अपना प्रोजेक्ट बनाना है. यह आपकी होम डायरेक्ट्री या C:\src\ जैसा कुछ हो सकता है.
आखिर में, अपने प्रोजेक्ट को नाम दें. जैसे, namer_app या my_awesome_namer.

अब Flutter, आपका प्रोजेक्ट फ़ोल्डर बनाता है और VS Code उसे खोलता है.
अब ऐप्लिकेशन के बेसिक स्ट्रक्चर के साथ, तीन फ़ाइलों के कॉन्टेंट को बदला जाएगा.
पहले ऐप्लिकेशन को कॉपी करके चिपकाना
VS Code के बाईं ओर मौजूद पैनल में, पक्का करें कि Explorer चुना गया हो. इसके बाद, pubspec.yaml फ़ाइल खोलें.

इस फ़ाइल के कॉन्टेंट की जगह यह कॉन्टेंट डालें:
pubspec.yaml
name: namer_app
description: "A new Flutter project."
publish_to: "none"
version: 0.1.0
environment:
sdk: ^3.9.0
dependencies:
flutter:
sdk: flutter
english_words: ^4.0.0
provider: ^6.1.5
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
uses-material-design: true
pubspec.yaml फ़ाइल में आपके ऐप्लिकेशन के बारे में बुनियादी जानकारी होती है. जैसे, उसका मौजूदा वर्शन, उसकी डिपेंडेंसी, और वे ऐसेट जिनके साथ उसे शिप किया जाएगा.
इसके बाद, प्रोजेक्ट में मौजूद दूसरी कॉन्फ़िगरेशन फ़ाइल analysis_options.yaml खोलें.

इसके कॉन्टेंट को इससे बदलें:
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 का इस्तेमाल पहली बार किया जा रहा है. इसलिए, ऐनलाइज़र को यह निर्देश दिया जा रहा है कि वह इस कोड की जांच न करे. इस सेटिंग को बाद में कभी भी बदला जा सकता है. असल में, प्रोडक्शन ऐप्लिकेशन को पब्लिश करने के लिए, आपको इस ऐनालाइज़र को और ज़्यादा सख्त बनाना होगा.
आखिर में, lib/ डायरेक्ट्री में मौजूद main.dart फ़ाइल खोलें.

इस फ़ाइल के कॉन्टेंट को इससे बदलें:
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(
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 खोलें और पक्का करें कि आपने टारगेट डिवाइस चुना हो. VS Code में सबसे नीचे दाएं कोने में, आपको एक बटन दिखेगा. इस बटन पर, मौजूदा टारगेट डिवाइस दिखता है. इसे बदलने के लिए क्लिक करें.
lib/main.dart खुला होने पर, VS Code की विंडो के सबसे ऊपर दाएं कोने में मौजूद "चलाएं"
बटन ढूंढें और उस पर क्लिक करें.
एक मिनट के बाद, आपका ऐप्लिकेशन डीबग मोड में लॉन्च हो जाएगा. फ़िलहाल, यह बहुत ज़्यादा नहीं है:

पहली बार हॉट रीलोड
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 की मशहूर स्टेटफ़ुल हॉट रिलोड सुविधा है. सोर्स फ़ाइल में बदलाव सेव करने पर, हॉट रिलोड ट्रिगर होता है.
अक्सर पूछे जाने वाले सवाल
- अगर VSCode में हॉट रीलोड काम नहीं करता है, तो क्या होगा?
- क्या मुझे VSCode में हॉट रिलोड के लिए ‘r' दबाना होगा?
- क्या वेब पर हॉट रिलोड की सुविधा काम करती है?
- मैं "डीबग करें" बैनर को कैसे हटाऊं?
बटन जोड़ना
इसके बाद, 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'),
),
],
),
);
// ...
बदलाव सेव करने पर, ऐप्लिकेशन फिर से अपडेट होता है: एक बटन दिखता है. इस पर क्लिक करने पर, VS Code में मौजूद Debug Console में button pressed! मैसेज दिखता है.
पांच मिनट में Flutter का क्रैश कोर्स
डीबग कंसोल को देखना जितना मज़ेदार है, उतना ही ज़रूरी है कि बटन कुछ ज़्यादा काम करे. हालांकि, इससे पहले 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(
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में कोड देखें). इससे ऐप्लिकेशन में मौजूद कोई भी विजेट, स्टेट को ऐक्सेस कर सकता है.

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 है. यह वह विजेट है जिसमें आपने पहले ही बदलाव कर दिया है. यहां नंबर वाली हर लाइन, ऊपर दिए गए कोड में लाइन-नंबर वाली टिप्पणी से मैप होती है:
- हर विजेट, एक
build()मेथड तय करता है. विजेट की स्थितियों में बदलाव होने पर, यह मेथड अपने-आप कॉल हो जाती है, ताकि विजेट हमेशा अप-टू-डेट रहे. MyHomePage,watchतरीके का इस्तेमाल करके, ऐप्लिकेशन की मौजूदा स्थिति में हुए बदलावों को ट्रैक करता है.- हर
buildतरीके को एक विजेट या (ज़्यादातर) विजेट का नेस्ट किया गया ट्री दिखाना चाहिए. इस मामले में, टॉप-लेवल विजेटScaffoldहै. इस कोडलैब में, आपकोScaffoldके साथ काम नहीं करना है. हालांकि, यह एक काम का विजेट है और यह ज़्यादातर रीयल-वर्ल्ड Flutter ऐप्लिकेशन में मौजूद होता है. Column, Flutter में सबसे बुनियादी लेआउट विजेट में से एक है. यह किसी भी संख्या में बच्चों को लेता है और उन्हें ऊपर से नीचे तक एक कॉलम में रखता है. डिफ़ॉल्ट रूप से, कॉलम अपने चाइल्ड विजेट को सबसे ऊपर दिखाता है. इसे जल्द ही बदला जाएगा, ताकि कॉलम बीच में दिखे.- आपने पहले चरण में इस
Textविजेट में बदलाव किया था. - यह दूसरा
Textविजेट,appStateलेता है और उस क्लास के सिर्फ़ एक सदस्य,current(जो किWordPairहै) को ऐक्सेस करता है.WordPairकई मददगार गेटर उपलब्ध कराता है, जैसे किasPascalCaseयाasSnakeCase. यहां हमनेasLowerCaseका इस्तेमाल किया है. हालांकि, अगर आपको कोई दूसरा विकल्प पसंद है, तो अब इसे बदला जा सकता है. - ध्यान दें कि 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. ऐप्लिकेशन को ज़्यादा आकर्षक बनाना
फ़िलहाल, ऐप्लिकेशन इस तरह दिखता है.

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

विजेट से डेटा निकालना
मौजूदा शब्द के जोड़े को दिखाने वाली लाइन अब ऐसी दिखती है: 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 मेन्यू खोलें. VS Code में, ऐसा इनमें से किसी एक तरीके से किया जा सकता है:
- आपको जिस कोड को फिर से फ़ैक्टर करना है उस पर राइट क्लिक करें (इस मामले में
Text) और ड्रॉप-डाउन मेन्यू से फिर से फ़ैक्टर करें... चुनें,
या
- अपने कर्सर को उस कोड पर ले जाएं जिसे आपको रीफ़ैक्टर करना है (इस मामले में
Text), औरCtrl+.(Win/Linux) याCmd+.(Mac) दबाएं.
Refactor मेन्यू में जाकर, Extract Widget चुनें. इसे कोई नाम असाइन करें. जैसे, 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 मेन्यू खोलें. हालांकि, इस बार आपको विजेट को नहीं निकालना है.
इसके बजाय, पैडिंग के साथ रैप करें को चुनें. इससे Text विजेट के चारों ओर एक नया पैरंट विजेट बन जाता है, जिसे Padding कहा जाता है. सेव करने के बाद, आपको दिखेगा कि रैंडम शब्द को पहले से ज़्यादा जगह मिल गई है.
पैडिंग को डिफ़ॉल्ट वैल्यू 8.0 से बढ़ाएं. उदाहरण के लिए, ज़्यादा पैडिंग के लिए 20 का इस्तेमाल करें.
इसके बाद, एक लेवल ऊपर जाएं. अपने कर्सर को Padding विजेट पर रखें. इसके बाद, Refactor मेन्यू को ऊपर की ओर खींचें और Wrap with widget... को चुनें.
इससे पैरंट विजेट के बारे में जानकारी दी जा सकती है. "कार्ड" टाइप करें और Enter दबाएं.
इससे Padding विजेट और इसलिए Text को भी Card विजेट में रैप किया जाता है.
lib/main.dart
// ...
class BigCard extends StatelessWidget {
const BigCard({super.key, required this.pair});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
}
// ...
अब ऐप्लिकेशन कुछ ऐसा दिखेगा:

थीम और स्टाइल
कार्ड को ज़्यादा आकर्षक बनाने के लिए, उसे गहरे रंग से पेंट करें. कलर स्कीम को एक जैसा रखना हमेशा अच्छा होता है. इसलिए, रंग चुनने के लिए ऐप्लिकेशन के 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ऐप्लिकेशन का मुख्य रंग है.
अब कार्ड को ऐप्लिकेशन के मुख्य रंग से पेंट किया गया है:

इस रंग को बदला जा सकता है. साथ ही, पूरे ऐप्लिकेशन की कलर स्कीम को भी बदला जा सकता है. इसके लिए, 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की वैल्यू null नहीं है. हम इस बारे में इस कोडलैब में नहीं बता सकते. displayMediumपरcopyWith()को कॉल करने पर, टेक्स्ट स्टाइल की कॉपी मिलती है. इसमें आपके तय किए गए बदलाव शामिल होते हैं. इस मामले में, सिर्फ़ टेक्स्ट का रंग बदला जा रहा है.- नया रंग पाने के लिए, ऐप्लिकेशन की थीम को फिर से ऐक्सेस करें. कलर स्कीम की
onPrimaryप्रॉपर्टी, ऐसे रंग को तय करती है जिसका इस्तेमाल ऐप्लिकेशन के मुख्य रंग पर किया जा सकता है.
अब ऐप्लिकेशन कुछ ऐसा दिखना चाहिए:

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

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

बच्चों को कॉलम के क्रॉस ऐक्सिस के हिसाब से पहले से ही बीच में रखा गया है. दूसरे शब्दों में कहें, तो उन्हें पहले से ही हॉरिज़ॉन्टल तौर पर बीच में रखा गया है. हालांकि, Column खुद Scaffold के बीच में नहीं है. विजेट इंस्पेक्टर का इस्तेमाल करके, हम इसकी पुष्टि कर सकते हैं.
Widget Inspector, इस कोडलैब के दायरे से बाहर है. हालांकि, यहां देखा जा सकता है कि जब Column को हाइलाइट किया जाता है, तब यह ऐप्लिकेशन की पूरी चौड़ाई नहीं लेता है. यह सिर्फ़ उतना हॉरिज़ॉन्टल स्पेस लेता है जितना इसके चाइल्ड को चाहिए.
सिर्फ़ कॉलम को बीच में अलाइन किया जा सकता है. अपने कर्सर को Column पर रखें. इसके बाद, Ctrl+. या Cmd+. की मदद से Refactor मेन्यू को कॉल करें और Wrap with Center को चुनें.
अब ऐप्लिकेशन कुछ ऐसा दिखना चाहिए:

अगर आपको लगता है कि इसमें कुछ और बदलाव किए जा सकते हैं, तो ऐसा करें.
- ऊपर मौजूद
Textविजेट कोBigCardसे हटाया जा सकता है. यह तर्क दिया जा सकता है कि जानकारी देने वाले टेक्स्ट ("A random AWESOME idea:") की अब ज़रूरत नहीं है, क्योंकि यूज़र इंटरफ़ेस (यूआई) इसके बिना भी समझ में आता है. इससे कोड ज़्यादा साफ़ दिखता है. 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'),
),
],
),
),
);
}
}
// ...
ऐप्लिकेशन ऐसा दिखता है:

अगले सेक्शन में, जनरेट किए गए शब्दों को पसंदीदा (या 'पसंद करें') के तौर पर मार्क करने की सुविधा जोड़ी जाएगी.
6. सुविधा जोड़ना
यह ऐप्लिकेशन काम करता है. साथ ही, कभी-कभी शब्दों के दिलचस्प जोड़े भी उपलब्ध कराता है. हालांकि, जब उपयोगकर्ता अगला पर क्लिक करता है, तो शब्दों का हर जोड़ा हमेशा के लिए गायब हो जाता है. बेहतरीन सुझावों को "याद रखने" का कोई तरीका होना चाहिए. जैसे, ‘पसंद करें' बटन.

कारोबारी नियम जोड़ना
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के अलावा कोई और वैल्यू जोड़ने की कोशिश की, तो Dart उसे रन करने से भी मना कर देगा. इसके बाद,favoritesसूची का इस्तेमाल किया जा सकता है. आपको यह पता होगा कि इसमें कभी भी कोई अवांछित ऑब्जेक्ट (जैसे किnull) नहीं छिपा होगा.
- आपने
toggleFavorite()नाम का एक नया तरीका भी जोड़ा है. इससे, शब्दों के मौजूदा जोड़े को पसंदीदा शब्दों की सूची से हटाया जा सकता है. हालांकि, ऐसा सिर्फ़ तब किया जा सकता है, जब वह जोड़ा पहले से सूची में मौजूद हो. इसके अलावा, इस तरीके से शब्दों के जोड़े को सूची में जोड़ा भी जा सकता है. हालांकि, ऐसा सिर्फ़ तब किया जा सकता है, जब वह जोड़ा सूची में पहले से मौजूद न हो. दोनों ही मामलों में, कोड इसके बादnotifyListeners();को कॉल करता है.
बटन जोड़ना
"कारोबारी नियम" को अलग करने के बाद, अब यूज़र इंटरफ़ेस पर फिर से काम करने का समय है. ‘अगला' बटन के बाईं ओर ‘पसंद करें' बटन रखने के लिए, Row की ज़रूरत होती है. Row विजेट, Column विजेट का हॉरिज़ॉन्टल वर्शन है. इसे आपने पहले देखा था.
सबसे पहले, मौजूदा बटन को Row में रैप करें. MyHomePage के build() तरीके पर जाएं. इसके बाद, अपने कर्सर को ElevatedButton पर रखें. Ctrl+. या Cmd+. की मदद से, Refactor मेन्यू को कॉल करें. इसके बाद, Wrap with Row को चुनें.
सेव करने पर, आपको दिखेगा कि 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'),
),
],
),
],
),
),
);
}
}
// ...
यूज़र इंटरफ़ेस (यूआई) पहले जैसा हो जाता है.

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

अगर आपने इसे नीचे दिए गए तरीके से नहीं किया है, तो भी कोई समस्या नहीं है. दरअसल, अगर आपको वाकई में कोई बड़ी चुनौती नहीं चाहिए, तो दिल के निशान वाले आइकॉन के बारे में चिंता न करें.
अगर आपको सफलता नहीं मिलती है, तो भी कोई बात नहीं. यह Flutter के साथ आपका पहला घंटा है.

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 लागू करना होगा.

इस चरण के मुख्य हिस्से पर तुरंत पहुंचने के लिए, 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'),
),
],
),
],
),
);
}
}
// ...
सेव करने पर, आपको दिखेगा कि यूज़र इंटरफ़ेस (यूआई) का विज़ुअल हिस्सा तैयार है. हालांकि, यह काम नहीं करता. नेविगेशन रेल में मौजूद ♥︎ (दिल) पर क्लिक करने से कुछ नहीं होता.

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

- दो
Expandedविजेट, हॉरिज़ॉन्टल तौर पर उपलब्ध पूरी जगह को आपस में बांट लेते हैं. भले ही, नेविगेशन रेल को सिर्फ़ बाईं ओर थोड़ी जगह की ज़रूरत हो. Expandedविजेट के अंदर, रंगीनContainerहै. साथ ही, कंटेनर के अंदरGeneratorPageहै.
स्टेटलेस बनाम स्टेटफ़ुल विजेट
अब तक, MyAppState ने आपकी सभी ज़रूरतों को पूरा किया है. इसलिए, अब तक लिखे गए सभी विजेट, स्टेटलेस हैं. इनमें खुद का कोई भी ऐसा स्टेट शामिल नहीं होता जिसे बदला जा सकता हो. कोई भी विजेट अपने-आप नहीं बदल सकता. इसके लिए, उन्हें MyAppState से गुज़रना होगा.
यह अब बदलने वाला है.
आपको नेविगेशन रेल के selectedIndex की वैल्यू को सेव करने के लिए किसी तरीके की ज़रूरत होगी. आपको इस वैल्यू को onDestinationSelected कॉलबैक से भी बदलने की सुविधा चाहिए.
MyAppState की एक और प्रॉपर्टी के तौर पर, selectedIndex को जोड़ा जा सकता है. और यह काम करेगा. हालांकि, अगर हर विजेट अपनी वैल्यू को इसमें सेव करता है, तो ऐप्लिकेशन की स्थिति तेज़ी से बदल जाएगी.

कुछ स्टेट सिर्फ़ एक विजेट के लिए काम की होती है. इसलिए, इसे उस विजेट के साथ ही रहना चाहिए.
StatefulWidget डालें. यह एक तरह का विजेट है, जिसमें State होता है. सबसे पहले, MyHomePage को स्टेटफ़ुल विजेट में बदलें.
अपने कर्सर को MyHomePage की पहली लाइन पर रखें (वह लाइन जो class MyHomePage... से शुरू होती है). इसके बाद, Ctrl+. या Cmd+. का इस्तेमाल करके, Refactor मेन्यू खोलें. इसके बाद, Convert to StatefulWidget को चुनें.
आईडीई आपके लिए एक नई क्लास, _MyHomePageState बनाता है. यह क्लास State से जुड़ी है. इसलिए, यह अपनी वैल्यू मैनेज कर सकती है. (यह अपने-आप बदल सकता है.) यह भी ध्यान दें कि पुराने, स्टेटलेस विजेट का build तरीका, अब विजेट में नहीं है. इसे _MyHomePageState में ले जाया गया है. इसे हूबहू ट्रांसफ़र किया गया है. build तरीके में कोई बदलाव नहीं किया गया है. अब यह सिर्फ़ कहीं और मौजूद है.
setState
नए स्टेटफ़ुल विजेट को सिर्फ़ एक वैरिएबल को ट्रैक करने की ज़रूरत होती है: 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(),
),
),
],
),
);
}
}
// ...
बदलावों की जांच करें:
- आपने एक नया वैरिएबल,
selectedIndexबनाया है और उसे0पर सेट किया है. - अब तक इस्तेमाल किए जा रहे हार्ड-कोड किए गए
0के बजाय,NavigationRailकी डेफ़िनिशन में इस नए वैरिएबल का इस्तेमाल करें. - जब
onDestinationSelectedकॉलबैक को कॉल किया जाता है, तो नई वैल्यू को सिर्फ़ कंसोल पर प्रिंट करने के बजाय, उसेsetState()कॉल के अंदरselectedIndexको असाइन किया जाता है. यह कॉल, पहले इस्तेमाल किए गएnotifyListeners()तरीके जैसा ही है. इससे यह पक्का होता है कि यूज़र इंटरफ़ेस (यूआई) अपडेट हो गया है.
नेविगेशन रेल अब उपयोगकर्ता के इंटरैक्शन पर प्रतिक्रिया देती है. हालांकि, दाईं ओर बड़ा किया गया हिस्सा पहले जैसा ही रहता है. ऐसा इसलिए है, क्योंकि कोड में selectedIndex का इस्तेमाल यह तय करने के लिए नहीं किया जा रहा है कि कौनसी स्क्रीन दिखेगी.
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');
}
// ...
इस कोड की जांच करें:
- इस कोड में,
Widgetटाइप का एक नया वैरिएबलpageबनाया गया है. - इसके बाद, स्विच स्टेटमेंट,
selectedIndexमें मौजूद मौजूदा वैल्यू के हिसाब से,pageको स्क्रीन असाइन करता है. - फ़िलहाल,
FavoritesPageउपलब्ध नहीं है. इसलिए,Placeholderका इस्तेमाल करें. यह एक काम का विजेट है. इसे जहां भी रखा जाता है वहां एक क्रॉस किया गया आयत बनाता है. इससे यूज़र इंटरफ़ेस (यूआई) के उस हिस्से को अधूरा मार्क किया जाता है.

- fail-fast सिद्धांत लागू करने पर, स्विच स्टेटमेंट यह भी पक्का करता है कि अगर
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 का इस्तेमाल करें.

Flutter कई ऐसे विजेट उपलब्ध कराता है जिनकी मदद से, आपके ऐप्लिकेशन अपने-आप रिस्पॉन्सिव बन जाते हैं. उदाहरण के लिए, Wrap, Row या Column जैसा एक विजेट है. जब वर्टिकल या हॉरिज़ॉन्टल स्पेस कम होता है, तो यह बच्चों को अगली "लाइन" (जिसे "रन" कहा जाता है) में अपने-आप रैप कर देता है. FittedBox नाम का एक विजेट है. यह विजेट, आपकी तय की गई सेटिंग के हिसाब से, उपलब्ध जगह में अपने-आप फ़िट हो जाता है.
हालांकि, NavigationRail के पास ज़रूरत के मुताबिक स्टोरेज होने पर भी, यह अपने-आप लेबल नहीं दिखाता है. ऐसा इसलिए, क्योंकि यह नहीं जान सकता कि हर संदर्भ में ज़रूरत के मुताबिक स्टोरेज कितना होता है. डेवलपर के तौर पर, आपको यह कॉल करना होगा.
मान लें कि आपको लेबल सिर्फ़ तब दिखाने हैं, जब MyHomePage की चौड़ाई कम से कम 600 पिक्सल हो.
इस मामले में, इस्तेमाल किया जाने वाला विजेट LayoutBuilder है. इससे, उपलब्ध जगह के हिसाब से विजेट ट्री को बदला जा सकता है.
ज़रूरी बदलाव करने के लिए, VS Code में Flutter के Refactor मेन्यू का फिर से इस्तेमाल करें. हालांकि, इस बार यह थोड़ा मुश्किल है:
_MyHomePageStateकेbuildतरीके में, अपने कर्सर कोScaffoldपर रखें.Ctrl+.(Windows/Linux) याCmd+.(Mac) का इस्तेमाल करके, Refactor मेन्यू खोलें.- Wrap with Builder को चुनें और Enter दबाएं.
- नई जोड़ी गई
Builderका नाम बदलकरLayoutBuilderकरें. - कॉल बैक पैरामीटर की सूची को
(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में स्ट्रिंग की सूची शामिल है, तो आपके पास इस तरह का कोड हो सकता है:

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

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

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