Flutter में अडैप्टिव ऐप्लिकेशन

Flutter में हर डिवाइस पर काम करने वाले ऐप्लिकेशन

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

subjectपिछली बार जून 3, 2025 को अपडेट किया गया
account_circleBrett Morgan ने लिखा

1. परिचय

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

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

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

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

इस कोडलैब में, आपको सबसे पहले Android और iOS के लिए Flutter ऐप्लिकेशन बनाना होगा. यह ऐप्लिकेशन, Flutter की YouTube प्लेलिस्ट को एक्सप्लोर करेगा. इसके बाद, आपको इस ऐप्लिकेशन को तीन डेस्कटॉप प्लैटफ़ॉर्म (Windows, macOS, और Linux) पर काम करने के लिए अडैप्ट करना होगा. इसके लिए, आपको ऐप्लिकेशन विंडो के साइज़ के हिसाब से, जानकारी दिखाने के तरीके में बदलाव करना होगा. इसके बाद, आपको ऐप्लिकेशन को वेब के लिए अडैप्ट करना होगा. इसके लिए, ऐप्लिकेशन में दिखने वाले टेक्स्ट को चुनने लायक बनाएं, ताकि वेब उपयोगकर्ताओं को वह टेक्स्ट दिखे जिसकी उन्हें उम्मीद है. आखिर में, आपको ऐप्लिकेशन में पुष्टि करने की सुविधा जोड़नी होगी, ताकि आप अपनी प्लेलिस्ट एक्सप्लोर कर सकें. Flutter टीम ने जो प्लेलिस्ट बनाई हैं उन्हें एक्सप्लोर करने के लिए, Android, iOS, और वेब के लिए पुष्टि करने के अलग-अलग तरीके अपनाने पड़ते हैं. वहीं, डेस्कटॉप के तीन प्लैटफ़ॉर्म, Windows, macOS, और Linux के लिए, पुष्टि करने के अलग-अलग तरीके अपनाने पड़ते हैं.

यहां Android और iOS पर Flutter ऐप्लिकेशन का स्क्रीनशॉट दिया गया है:

Android एमुलेटर पर चल रहा पूरा ऐप्लिकेशन

iOS सिम्युलेटर पर चल रहा पूरा ऐप्लिकेशन

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

macOS पर चल रहा ऐप्लिकेशन

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

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

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

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

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

  • आपके कंप्यूटर से कनेक्ट किया गया Android या iOS डिवाइस, जो डेवलपर मोड पर सेट हो.
  • iOS सिम्युलेटर (इसके लिए, Xcode टूल इंस्टॉल करने की ज़रूरत है).
  • Android एमुलेटर (Android Studio में सेटअप करना ज़रूरी है).
  • ब्राउज़र (डीबग करने के लिए Chrome ज़रूरी है).
  • Windows, Linux या macOS के लिए डेस्कटॉप ऐप्लिकेशन के तौर पर. आपको उस प्लैटफ़ॉर्म पर ऐप्लिकेशन बनाना होगा जिस पर आपको उसे डिप्लॉय करना है. इसलिए, अगर आपको Windows डेस्कटॉप ऐप्लिकेशन बनाना है, तो आपको सही बिल्ड चेन ऐक्सेस करने के लिए, Windows पर डेवलप करना होगा. ऑपरेटिंग सिस्टम के हिसाब से कुछ ज़रूरी शर्तें होती हैं. इनके बारे में ज़्यादा जानकारी के लिए, docs.flutter.dev/desktop पर जाएं.

3. अपनी प्रोफ़ाइल बनाना शुरू करें

डेवलपमेंट एनवायरमेंट की पुष्टि करना

यह पक्का करने का सबसे आसान तरीका है कि डेवलपमेंट के लिए सब कुछ तैयार है. इसके लिए, यह कमांड चलाएं:

flutter doctor

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

flutter doctor -v

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

Flutter प्रोजेक्ट बनाना

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

$ flutter create adaptive_app
Creating project adaptive_app...
Resolving dependencies in adaptive_app... (1.8s)
Got dependencies in adaptive_app.
Wrote 129 files.

All done!
You can find general documentation for Flutter at: https://docs.flutter.dev/
Detailed API documentation is available at: https://api.flutter.dev/
If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev

In order to run your application, type:

  $ cd adaptive_app
  $ flutter run

Your application code is in adaptive_app/lib/main.dart.

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

$ flutter run
Launching lib/main.dart on iPhone 15 in debug mode...
Running Xcode build...
 └─Compiling, linking and signing...                         6.5s
Xcode build done.                                           24.6s
Syncing files to device iPhone 15...                                46ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

A Dart VM Service on iPhone 15 is available at: http://127.0.0.1:50501/JHGBwC_hFJo=/
The Flutter DevTools debugger and profiler on iPhone 15 is available at: http://127.0.0.1:9102?uri=http://127.0.0.1:50501/JHGBwC_hFJo=/

अब आपको ऐप्लिकेशन चलता हुआ दिखेगा. कॉन्टेंट को अपडेट करना ज़रूरी है.

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

  • अगर ऐप्लिकेशन को कमांड लाइन का इस्तेमाल करके चलाया जाता है, तो हॉट रीलोड करने के लिए कंसोल में r टाइप करें.
  • अगर किसी IDE का इस्तेमाल करके ऐप्लिकेशन चलाया जाता है, तो फ़ाइल सेव करने पर ऐप्लिकेशन फिर से लोड हो जाता है.

lib/main.dart

import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';

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

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

 
@override
 
Widget build(BuildContext context) {
   
return MaterialApp(
     
title: 'Flutter Demo',
     
theme: ThemeData(
       
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
     
),
     
home: const ResizeablePage(),
   
);
 
}
}

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

 
@override
 
Widget build(BuildContext context) {
   
final mediaQuery = MediaQuery.of(context);
   
final themePlatform = Theme.of(context).platform;

   
return Scaffold(
     
body: Center(
       
child: Column(
         
mainAxisAlignment: MainAxisAlignment.center,
         
children: <Widget>[
           
Text(
             
'Window properties',
             
style: Theme.of(context).textTheme.headlineSmall,
           
),
           
const SizedBox(height: 8),
           
SizedBox(
             
width: 350,
             
child: Table(
               
textBaseline: TextBaseline.alphabetic,
               
children: <TableRow>[
                 
_fillTableRow(
                   
context: context,
                   
property: 'Window Size',
                   
value:
                       
'${mediaQuery.size.width.toStringAsFixed(1)} x '
                       
'${mediaQuery.size.height.toStringAsFixed(1)}',
                 
),
                 
_fillTableRow(
                   
context: context,
                   
property: 'Device Pixel Ratio',
                   
value: mediaQuery.devicePixelRatio.toStringAsFixed(2),
                 
),
                 
_fillTableRow(
                   
context: context,
                   
property: 'Platform.isXXX',
                   
value: platformDescription(),
                 
),
                 
_fillTableRow(
                   
context: context,
                   
property: 'Theme.of(ctx).platform',
                   
value: themePlatform.toString(),
                 
),
               
],
             
),
           
),
         
],
       
),
     
),
   
);
 
}

 
TableRow _fillTableRow({
   
required BuildContext context,
   
required String property,
   
required String value,
 
}) {
   
return TableRow(
     
children: [
       
TableCell(
         
verticalAlignment: TableCellVerticalAlignment.baseline,
         
child: Padding(
           
padding: const EdgeInsets.all(8.0),
           
child: Text(property),
         
),
       
),
       
TableCell(
         
verticalAlignment: TableCellVerticalAlignment.baseline,
         
child: Padding(
           
padding: const EdgeInsets.all(8.0),
           
child: Text(value),
         
),
       
),
     
],
   
);
 
}

 
String platformDescription() {
   
if (kIsWeb) {
     
return 'Web';
   
} else if (Platform.isAndroid) {
     
return 'Android';
   
} else if (Platform.isIOS) {
     
return 'iOS';
   
} else if (Platform.isWindows) {
     
return 'Windows';
   
} else if (Platform.isMacOS) {
     
return 'macOS';
   
} else if (Platform.isLinux) {
     
return 'Linux';
   
} else if (Platform.isFuchsia) {
     
return 'Fuchsia';
   
} else {
     
return 'Unknown';
   
}
 
}
}

इस ऐप्लिकेशन को इस तरह से डिज़ाइन किया गया है कि आपको यह समझने में मदद मिल सके कि अलग-अलग प्लैटफ़ॉर्म का पता कैसे लगाया जा सकता है और उन पर कैसे काम किया जा सकता है. यहां Android और iOS पर नेटिव तौर पर चलने वाला ऐप्लिकेशन दिखाया गया है:

Android एमुलेटर पर विंडो प्रॉपर्टी दिखाना

iOS सिम्युलेटर पर विंडो प्रॉपर्टी दिखाना

यहां वही कोड दिखाया गया है जो macOS पर नेटिव तौर पर चल रहा है. साथ ही, Chrome में भी वही कोड macOS पर चल रहा है.

macOS पर विंडो प्रॉपर्टी दिखाना

Chrome ब्राउज़र में विंडो की प्रॉपर्टी दिखाना

यहां ध्यान देने वाली अहम बात यह है कि पहली नज़र में, Flutter कॉन्टेंट को उस डिसप्ले के हिसाब से अडैप्ट करने की पूरी कोशिश कर रहा है जिस पर वह चल रहा है. जिस लैपटॉप पर ये स्क्रीनशॉट लिए गए हैं उसमें हाई रिज़ॉल्यूशन वाला Mac डिसप्ले है. इसलिए, ऐप्लिकेशन के macOS और वेब वर्शन, दोनों को डिवाइस पिक्सल रेशियो 2 पर रेंडर किया गया है. वहीं, iPhone 12 पर आपको 3 और Pixel 2 पर 2.63 का आसपेक्ट रेशियो दिखता है. सभी मामलों में, दिखाया गया टेक्स्ट लगभग एक जैसा होता है. इससे डेवलपर के तौर पर हमारा काम काफ़ी आसान हो जाता है.

ध्यान देने वाली दूसरी बात यह है कि कोड किस प्लैटफ़ॉर्म पर चल रहा है, यह पता करने के दो विकल्पों से अलग-अलग वैल्यू मिलती हैं. पहला विकल्प, dart:io से इंपोर्ट किए गए Platform ऑब्जेक्ट की जांच करता है. वहीं, दूसरा विकल्प (यह सिर्फ़ विजेट के build तरीके में उपलब्ध है) BuildContext आर्ग्युमेंट से Theme ऑब्जेक्ट को वापस लाता है.

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

BuildContext से Theme निकालने का मकसद, थीम के हिसाब से लागू करने के फ़ैसले लेने के लिए है. इसका एक मुख्य उदाहरण यह तय करना है कि Material स्लाइडर या Cupertino स्लाइडर में से किसका इस्तेमाल करना है. इस बारे में Slider.adaptive में बताया गया है.

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

4. मोबाइल ऐप्लिकेशन बनाना

पैकेज जोड़ना

इस ऐप्लिकेशन में, आपको YouTube Data API, स्टेटस मैनेजमेंट, और थीमिंग का ऐक्सेस पाने के लिए, कई तरह के Flutter पैकेज इस्तेमाल करने होंगे.

$ flutter pub add googleapis http provider url_launcher flex_color_scheme go_router
Resolving dependencies...
Downloading packages...
+ _discoveryapis_commons 1.0.7
+ flex_color_scheme 8.2.0
+ flex_seed_scheme 3.5.1
+ flutter_web_plugins 0.0.0 from sdk flutter
+ go_router 15.1.2
+ googleapis 14.0.0
+ http 1.4.0
+ http_parser 4.1.2
  leak_tracker 10.0.9 (11.0.1 available)
  leak_tracker_flutter_testing 3.0.9 (3.0.10 available)
  leak_tracker_testing 3.0.1 (3.0.2 available)
  lints 5.1.1 (6.0.0 available)
+ logging 1.3.0
  material_color_utilities 0.11.1 (0.12.0 available)
  meta 1.16.0 (1.17.0 available)
+ nested 1.0.0
+ plugin_platform_interface 2.1.8
+ provider 6.1.5
  test_api 0.7.4 (0.7.6 available)
+ typed_data 1.4.0
+ url_launcher 6.3.1
+ url_launcher_android 6.3.16
+ url_launcher_ios 6.3.3
+ url_launcher_linux 3.2.1
+ url_launcher_macos 3.2.2
+ url_launcher_platform_interface 2.3.2
+ url_launcher_web 2.4.1
+ url_launcher_windows 3.1.4
  vector_math 2.1.4 (2.1.5 available)
+ web 1.1.1
Changed 22 dependencies!
8 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

यह कमांड, ऐप्लिकेशन में कई पैकेज जोड़ता है:

  • googleapis: जनरेट की गई Dart लाइब्रेरी, जो Google API का ऐक्सेस देती है.
  • http: एचटीटीपी अनुरोध बनाने के लिए लाइब्रेरी, जो नेटिव और वेब ब्राउज़र के बीच के अंतर को छिपाती है.
  • provider: यह स्टेटस मैनेजमेंट की सुविधा देता है.
  • url_launcher: इससे प्लेलिस्ट में मौजूद किसी वीडियो पर जाया जा सकता है. डिपेंडेंसी ठीक करने के बाद, यह पता चलता है कि url_launcher को Android और iOS के साथ-साथ Windows, macOS, Linux, और वेब पर भी इस्तेमाल किया जा सकता है. इस पैकेज का इस्तेमाल करने का मतलब है कि आपको इस सुविधा के लिए, प्लैटफ़ॉर्म के हिसाब से कोई खास ऐप्लिकेशन बनाने की ज़रूरत नहीं होगी.
  • flex_color_scheme: इससे ऐप्लिकेशन को डिफ़ॉल्ट रूप से एक अच्छी कलर स्कीम मिलती है. ज़्यादा जानने के लिए, flex_color_scheme एपीआई के दस्तावेज़ देखें.
  • go_router: अलग-अलग स्क्रीन के बीच नेविगेट करने की सुविधा लागू करता है. यह पैकेज, Flutter के Router का इस्तेमाल करके नेविगेट करने के लिए, यूआरएल पर आधारित एक आसान एपीआई उपलब्ध कराता है.

url_launcher के लिए मोबाइल ऐप्लिकेशन कॉन्फ़िगर करना

url_launcher प्लग इन को Android और iOS के रनर ऐप्लिकेशन को कॉन्फ़िगर करना ज़रूरी है. iOS Flutter runner में, plist डिक्शनरी में ये लाइनें जोड़ें.

ios/Runner/Info.plist

<key>LSApplicationQueriesSchemes</key>
<array>
       
<string>https</string>
       
<string>http</string>
       
<string>tel</string>
       
<string>mailto</string>
</array>

Android Flutter runner में, Manifest.xml में ये लाइनें जोड़ें. इस queries नोड को manifest नोड के डायरेक्ट चाइल्ड और application नोड के पीयर के तौर पर जोड़ें.

android/app/src/main/AndroidManifest.xml

<queries>
   
<intent>
       
<action android:name="android.intent.action.VIEW" />
       
<data android:scheme="https" />
   
</intent>
   
<intent>
       
<action android:name="android.intent.action.DIAL" />
       
<data android:scheme="tel" />
   
</intent>
   
<intent>
       
<action android:name="android.intent.action.SEND" />
       
<data android:mimeType="*/*" />
   
</intent>
</queries>

कॉन्फ़िगरेशन में किए जाने वाले इन ज़रूरी बदलावों के बारे में ज़्यादा जानने के लिए, url_launcher दस्तावेज़ देखें.

YouTube Data API को ऐक्सेस करना

प्लेलिस्ट की सूची देखने के लिए, YouTube Data API को ऐक्सेस करने के लिए, आपको ज़रूरी एपीआई पासकोड जनरेट करने के लिए एक एपीआई प्रोजेक्ट बनाना होगा. इन चरणों में यह माना गया है कि आपके पास पहले से ही Google खाता है. अगर आपके पास पहले से कोई खाता नहीं है, तो एक खाता बनाएं.

एपीआई प्रोजेक्ट बनाने के लिए, Developer Console पर जाएं:

प्रोजेक्ट बनाने के दौरान GCP कंसोल दिखाना

प्रोजेक्ट बनाने के बाद, एपीआई लाइब्रेरी पेज पर जाएं. खोज बॉक्स में, "youtube" डालें और youtube data api v3 चुनें.

GCP कंसोल में YouTube Data API v3 चुनना

YouTube Data API के वर्शन 3 की जानकारी वाले पेज पर, एपीआई को चालू करें.

5a877ea82b83ae42.png

एपीआई चालू करने के बाद, क्रेडेंशियल पेज पर जाएं और एपीआई पासकोड बनाएं.

GCP कंसोल में क्रेडेंशियल बनाना

कुछ सेकंड बाद, आपको एक डायलॉग बॉक्स दिखेगा, जिसमें आपकी नई एपीआई कुंजी होगी. आपको जल्द ही इस कुंजी का इस्तेमाल करना होगा.

एपीआई पासकोड बनाने पर दिखने वाला पॉप-अप, जिसमें एपीआई पासकोड दिख रहा है

कोड जोड़ें

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

सबसे पहले, ऐप्लिकेशन के लिए स्टेट ऑब्जेक्ट जोड़ें. इसके बाद, ये फ़ाइलें जोड़ें.

lib/src/app_state.dart

import 'dart:collection';

import 'package:flutter/foundation.dart';
import 'package:googleapis/youtube/v3.dart';
import 'package:http/http.dart' as http;

class FlutterDevPlaylists extends ChangeNotifier {
 
FlutterDevPlaylists({
   
required String flutterDevAccountId,
   
required String youTubeApiKey,
 
}) : _flutterDevAccountId = flutterDevAccountId {
   
_api = YouTubeApi(_ApiKeyClient(client: http.Client(), key: youTubeApiKey));
   
_loadPlaylists();
 
}

 
Future<void> _loadPlaylists() async {
   
String? nextPageToken;
   
_playlists.clear();

   
do {
     
final response = await _api.playlists.list(
       
['snippet', 'contentDetails', 'id'],
       
channelId: _flutterDevAccountId,
       
maxResults: 50,
       
pageToken: nextPageToken,
     
);
     
_playlists.addAll(response.items!);
     
_playlists.sort(
       
(a, b) => a.snippet!.title!.toLowerCase().compareTo(
         
b.snippet!.title!.toLowerCase(),
       
),
     
);
     
notifyListeners();
     
nextPageToken = response.nextPageToken;
   
} while (nextPageToken != null);
 
}

 
final String _flutterDevAccountId;
 
late final YouTubeApi _api;

 
final List<Playlist> _playlists = [];
 
List<Playlist> get playlists => UnmodifiableListView(_playlists);

 
final Map<String, List<PlaylistItem>> _playlistItems = {};
 
List<PlaylistItem> playlistItems({required String playlistId}) {
   
if (!_playlistItems.containsKey(playlistId)) {
     
_playlistItems[playlistId] = [];
     
_retrievePlaylist(playlistId);
   
}
   
return UnmodifiableListView(_playlistItems[playlistId]!);
 
}

 
Future<void> _retrievePlaylist(String playlistId) async {
   
String? nextPageToken;
   
do {
     
var response = await _api.playlistItems.list(
       
['snippet', 'contentDetails'],
       
playlistId: playlistId,
       
maxResults: 25,
       
pageToken: nextPageToken,
     
);
     
var items = response.items;
     
if (items != null) {
       
_playlistItems[playlistId]!.addAll(items);
     
}
     
notifyListeners();
     
nextPageToken = response.nextPageToken;
   
} while (nextPageToken != null);
 
}
}

class _ApiKeyClient extends http.BaseClient {
 
_ApiKeyClient({required this.key, required this.client});

 
final String key;
 
final http.Client client;

 
@override
 
Future<http.StreamedResponse> send(http.BaseRequest request) {
   
final url = request.url.replace(
     
queryParameters: <String, List<String>>{
       
...request.url.queryParametersAll,
       
'key': [key],
     
},
   
);

   
return client.send(http.Request(request.method, url));
 
}
}

इसके बाद, प्लेलिस्ट की जानकारी वाला अलग पेज जोड़ें.

lib/src/playlist_details.dart

import 'package:flutter/material.dart';
import 'package:googleapis/youtube/v3.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/link.dart';

import 'app_state.dart';

class PlaylistDetails extends StatelessWidget {
 
const PlaylistDetails({
   
required this.playlistId,
   
required this.playlistName,
   
super.key,
 
});
 
final String playlistId;
 
final String playlistName;

 
@override
 
Widget build(BuildContext context) {
   
return Scaffold(
     
appBar: AppBar(title: Text(playlistName)),
     
body: Consumer<FlutterDevPlaylists>(
       
builder: (context, playlists, _) {
         
final playlistItems = playlists.playlistItems(playlistId: playlistId);
         
if (playlistItems.isEmpty) {
           
return const Center(child: CircularProgressIndicator());
         
}

         
return _PlaylistDetailsListView(playlistItems: playlistItems);
       
},
     
),
   
);
 
}
}

class _PlaylistDetailsListView extends StatelessWidget {
 
const _PlaylistDetailsListView({required this.playlistItems});
 
final List<PlaylistItem> playlistItems;

 
@override
 
Widget build(BuildContext context) {
   
return ListView.builder(
     
itemCount: playlistItems.length,
     
itemBuilder: (context, index) {
       
final playlistItem = playlistItems[index];
       
return Padding(
         
padding: const EdgeInsets.all(8.0),
         
child: ClipRRect(
           
borderRadius: BorderRadius.circular(4),
           
child: Stack(
             
alignment: Alignment.center,
             
children: [
               
if (playlistItem.snippet!.thumbnails!.high != null)
                 
Image.network(playlistItem.snippet!.thumbnails!.high!.url!),
               
_buildGradient(context),
               
_buildTitleAndSubtitle(context, playlistItem),
               
_buildPlayButton(context, playlistItem),
             
],
           
),
         
),
       
);
     
},
   
);
 
}

 
Widget _buildGradient(BuildContext context) {
   
return Positioned.fill(
     
child: DecoratedBox(
       
decoration: BoxDecoration(
         
gradient: LinearGradient(
           
colors: [Colors.transparent, Theme.of(context).colorScheme.surface],
           
begin: Alignment.topCenter,
           
end: Alignment.bottomCenter,
           
stops: const [0.5, 0.95],
         
),
       
),
     
),
   
);
 
}

 
Widget _buildTitleAndSubtitle(
   
BuildContext context,
   
PlaylistItem playlistItem,
 
) {
   
return Positioned(
     
left: 20,
     
right: 0,
     
bottom: 20,
     
child: Column(
       
mainAxisSize: MainAxisSize.min,
       
crossAxisAlignment: CrossAxisAlignment.start,
       
children: [
         
Text(
           
playlistItem.snippet!.title!,
           
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
             
fontSize: 18,
             
// fontWeight: FontWeight.bold,
           
),
         
),
         
if (playlistItem.snippet!.videoOwnerChannelTitle != null)
           
Text(
             
playlistItem.snippet!.videoOwnerChannelTitle!,
             
style: Theme.of(
               
context,
             
).textTheme.bodyMedium!.copyWith(fontSize: 12),
           
),
       
],
     
),
   
);
 
}

 
Widget _buildPlayButton(BuildContext context, PlaylistItem playlistItem) {
   
return Stack(
     
alignment: AlignmentDirectional.center,
     
children: [
       
Container(
         
width: 42,
         
height: 42,
         
decoration: const BoxDecoration(
           
color: Colors.white,
           
borderRadius: BorderRadius.all(Radius.circular(21)),
         
),
       
),
       
Link(
         
uri: Uri.parse(
           
'https://www.youtube.com/watch?v=${playlistItem.snippet!.resourceId!.videoId}',
         
),
         
builder: (context, followLink) => IconButton(
           
onPressed: followLink,
           
color: Colors.red,
           
icon: const Icon(Icons.play_circle_fill),
           
iconSize: 45,
         
),
       
),
     
],
   
);
 
}
}

इसके बाद, प्लेलिस्ट की सूची जोड़ें.

lib/src/playlists.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:googleapis/youtube/v3.dart';
import 'package:provider/provider.dart';

import 'app_state.dart';

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

 
@override
 
Widget build(BuildContext context) {
   
return Scaffold(
     
appBar: AppBar(title: const Text('FlutterDev Playlists')),
     
body: Consumer<FlutterDevPlaylists>(
       
builder: (context, flutterDev, child) {
         
final playlists = flutterDev.playlists;
         
if (playlists.isEmpty) {
           
return const Center(child: CircularProgressIndicator());
         
}

         
return _PlaylistsListView(items: playlists);
       
},
     
),
   
);
 
}
}

class _PlaylistsListView extends StatelessWidget {
 
const _PlaylistsListView({required this.items});

 
final List<Playlist> items;

 
@override
 
Widget build(BuildContext context) {
   
return ListView.builder(
     
itemCount: items.length,
     
itemBuilder: (context, index) {
       
var playlist = items[index];
       
return Padding(
         
padding: const EdgeInsets.all(8.0),
         
child: ListTile(
           
leading: Image.network(
             
playlist.snippet!.thumbnails!.default_!.url!,
           
),
           
title: Text(playlist.snippet!.title!),
           
subtitle: Text(playlist.snippet!.description!),
           
onTap: () {
             
context.go(
               
Uri(
                 
path: '/playlist/${playlist.id}',
                 
queryParameters: <String, String>{
                   
'title': playlist.snippet!.title!,
                 
},
               
).toString(),
             
);
           
},
         
),
       
);
     
},
   
);
 
}
}

main.dart फ़ाइल का कॉन्टेंट इस तरह बदलें:

lib/main.dart

import 'dart:io';

import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';

import 'src/app_state.dart';
import 'src/playlist_details.dart';
import 'src/playlists.dart';

// From https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw
const flutterDevAccountId = 'UCwXdFgeE9KYzlDdR7TG9cMw';

// TODO: Replace with your YouTube API Key
const youTubeApiKey = 'AIzaNotAnApiKey';

final _router = GoRouter(
 
routes: <RouteBase>[
   
GoRoute(
     
path: '/',
     
builder: (context, state) {
       
return const Playlists();
     
},
     
routes: <RouteBase>[
       
GoRoute(
         
path: 'playlist/:id',
         
builder: (context, state) {
           
final title = state.uri.queryParameters['title']!;
           
final id = state.pathParameters['id']!;
           
return PlaylistDetails(playlistId: id, playlistName: title);
         
},
       
),
     
],
   
),
 
],
);

void main() {
 
if (youTubeApiKey == 'AIzaNotAnApiKey') {
   
print('youTubeApiKey has not been configured.');
   
exit(1);
 
}

 
runApp(
   
ChangeNotifierProvider<FlutterDevPlaylists>(
     
create: (context) => FlutterDevPlaylists(
       
flutterDevAccountId: flutterDevAccountId,
       
youTubeApiKey: youTubeApiKey,
     
),
     
child: const PlaylistsApp(),
   
),
 
);
}

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

 
@override
 
Widget build(BuildContext context) {
   
return MaterialApp.router(
     
title: 'FlutterDev Playlists',
     
theme: FlexColorScheme.light(scheme: FlexScheme.red).toTheme,
     
darkTheme: FlexColorScheme.dark(scheme: FlexScheme.red).toTheme,
     
themeMode: ThemeMode.dark, // Or ThemeMode.System if you'd prefer
     
debugShowCheckedModeBanner: false,
     
routerConfig: _router,
   
);
 
}
}

अब आपके पास Android और iOS पर यह कोड चलाने का विकल्प है. आपको सिर्फ़ एक और चीज़ बदलनी है. पिछले चरण में जनरेट की गई YouTube एपीआई पासकोड की मदद से, youTubeApiKey कॉन्स्टेंट में बदलाव करें.

lib/main.dart

// TODO: Replace with your YouTube API Key
const youTubeApiKey = 'AIzaNotAnApiKey';

macOS पर इस ऐप्लिकेशन को चलाने के लिए, आपको एचटीटीपी अनुरोध करने के लिए ऐप्लिकेशन को इस तरह से चालू करना होगा. DebugProfile.entitlements और Release.entitilements, दोनों फ़ाइलों में इस तरह बदलाव करें:

macos/Runner/DebugProfile.entitlements

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

macos/Runner/Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
        <dict>
                <key>com.apple.security.app-sandbox</key>
                <true/>
                <!-- add the following two lines -->
                <key>com.apple.security.network.client</key>
                <true/>
        </dict>
</plist>

ऐप्लिकेशन चलाना

अब आपके पास पूरा ऐप्लिकेशन है. इसलिए, इसे Android एमुलेटर या iPhone सिम्युलेटर पर चलाया जा सकता है. आपको Flutter की प्लेलिस्ट की सूची दिखेगी. किसी प्लेलिस्ट को चुनने पर, आपको उस प्लेलिस्ट के वीडियो दिखेंगे. आखिर में, 'चलाएं' बटन पर क्लिक करने से, आपको वीडियो देखने के लिए YouTube पर रीडायरेक्ट कर दिया जाएगा.

FlutterDev के YouTube खाते की प्लेलिस्ट दिखाने वाला ऐप्लिकेशन

किसी खास प्लेलिस्ट में वीडियो दिखाना

YouTube प्लेयर में चल रहा चुना गया वीडियो

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

5. डेस्कटॉप पर काम करना

डेस्कटॉप की समस्या

अगर ऐप्लिकेशन को Windows, macOS या Linux जैसे किसी नेटिव डेस्कटॉप प्लैटफ़ॉर्म पर चलाया जाता है, तो आपको एक दिलचस्प समस्या दिखेगी. यह काम करता है, लेकिन यह ... अजीब लगता है.

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

macOS पर, प्लेलिस्ट में मौजूद वीडियो

इस समस्या को ठीक करने के लिए, स्प्लिट व्यू जोड़ें. इसमें बाईं ओर प्लेलिस्ट और दाईं ओर वीडियो दिखेंगे. हालांकि, आपको यह लेआउट सिर्फ़ तब चालू करना है, जब कोड Android या iOS पर नहीं चल रहा हो और विंडो काफ़ी चौड़ी हो. नीचे दिए गए निर्देशों में, इस सुविधा को लागू करने का तरीका बताया गया है.

सबसे पहले, लेआउट बनाने में मदद करने के लिए split_view पैकेज जोड़ें.

$ flutter pub add split_view
Resolving dependencies...
Downloading packages...
  leak_tracker 10.0.9 (11.0.1 available)
  leak_tracker_flutter_testing 3.0.9 (3.0.10 available)
  leak_tracker_testing 3.0.1 (3.0.2 available)
  lints 5.1.1 (6.0.0 available)
  material_color_utilities 0.11.1 (0.12.0 available)
  meta 1.16.0 (1.17.0 available)
+ split_view 3.2.1
  test_api 0.7.4 (0.7.6 available)
  vector_math 2.1.4 (2.1.5 available)
Changed 1 dependency!
8 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

अडैप्टिव विजेट की सुविधा

इस कोडलैब में इस्तेमाल किया जाने वाला पैटर्न, अडैप्टिव विजेट को शामिल करने के लिए है. ये विजेट, स्क्रीन की चौड़ाई, प्लैटफ़ॉर्म की थीम वगैरह जैसे एट्रिब्यूट के आधार पर, लागू करने के विकल्प चुनते हैं. इस मामले में, आपको एक AdaptivePlaylists विजेट जोड़ना है, जो Playlists और PlaylistDetails के इंटरैक्ट करने के तरीके को फिर से काम करेगा. lib/main.dart फ़ाइल में इस तरह बदलाव करें:

lib/main.dart

import 'dart:io';

import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';

import 'src/adaptive_playlists.dart';                          // Add this import
import 'src/app_state.dart';
import 'src/playlist_details.dart';
// Remove the src/playlists.dart import

// From https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw
const flutterDevAccountId = 'UCwXdFgeE9KYzlDdR7TG9cMw';

// TODO: Replace with your YouTube API Key
const youTubeApiKey = 'AIzaNotAnApiKey';

final _router = GoRouter(
 
routes: <RouteBase>[
   
GoRoute(
     
path: '/',
     
builder: (context, state) {
       
return const AdaptivePlaylists();                      // Modify this line
     
},
     
routes: <RouteBase>[
       
GoRoute(
         
path: 'playlist/:id',
         
builder: (context, state) {
           
final title = state.uri.queryParameters['title']!;
           
final id = state.pathParameters['id']!;
           
return Scaffold(                                   // Modify from here
             
appBar: AppBar(title: Text(title)),
             
body: PlaylistDetails(playlistId: id, playlistName: title),
           
);                                                 // To here.
         
},
       
),
     
],
   
),
 
],
);

void main() {
 
if (youTubeApiKey == 'AIzaNotAnApiKey') {
   
print('youTubeApiKey has not been configured.');
   
exit(1);
 
}

 
runApp(
   
ChangeNotifierProvider<FlutterDevPlaylists>(
     
create: (context) => FlutterDevPlaylists(
       
flutterDevAccountId: flutterDevAccountId,
       
youTubeApiKey: youTubeApiKey,
     
),
     
child: const PlaylistsApp(),
   
),
 
);
}

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

 
@override
 
Widget build(BuildContext context) {
   
return MaterialApp.router(
     
title: 'FlutterDev Playlists',
     
theme: FlexColorScheme.light(scheme: FlexScheme.red).toTheme,
     
darkTheme: FlexColorScheme.dark(scheme: FlexScheme.red).toTheme,
     
themeMode: ThemeMode.dark, // Or ThemeMode.System if you'd prefer
     
debugShowCheckedModeBanner: false,
     
routerConfig: _router,
   
);
 
}
}

इसके बाद, AdaptivePlaylist विजेट के लिए फ़ाइल बनाएं:

lib/src/adaptive_playlists.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:googleapis/youtube/v3.dart';
import 'package:split_view/split_view.dart';

import 'playlist_details.dart';
import 'playlists.dart';

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

 
@override
 
Widget build(BuildContext context) {
   
final screenWidth = MediaQuery.of(context).size.width;
   
final targetPlatform = Theme.of(context).platform;

   
if (targetPlatform == TargetPlatform.android ||
       
targetPlatform == TargetPlatform.iOS ||
       
screenWidth <= 600) {
     
return const NarrowDisplayPlaylists();
   
} else {
     
return const WideDisplayPlaylists();
   
}
 
}
}

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

 
@override
 
Widget build(BuildContext context) {
   
return Scaffold(
     
appBar: AppBar(title: const Text('FlutterDev Playlists')),
     
body: Playlists(
       
playlistSelected: (playlist) {
         
context.go(
           
Uri(
             
path: '/playlist/${playlist.id}',
             
queryParameters: <String, String>{
               
'title': playlist.snippet!.title!,
             
},
           
).toString(),
         
);
       
},
     
),
   
);
 
}
}

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

 
@override
 
State<WideDisplayPlaylists> createState() => _WideDisplayPlaylistsState();
}

class _WideDisplayPlaylistsState extends State<WideDisplayPlaylists> {
 
Playlist? selectedPlaylist;
 
@override
 
Widget build(BuildContext context) {
   
return Scaffold(
     
appBar: AppBar(
       
title: switch (selectedPlaylist?.snippet?.title) {
         
String title => Text('FlutterDev Playlist: $title'),
         
_ => const Text('FlutterDev Playlists'),
       
},
     
),
     
body: SplitView(
       
viewMode: SplitViewMode.Horizontal,
       
children: [
         
Playlists(
           
playlistSelected: (playlist) {
             
setState(() {
               
selectedPlaylist = playlist;
             
});
           
},
         
),
         
switch ((selectedPlaylist?.id, selectedPlaylist?.snippet?.title)) {
           
(String id, String title) => PlaylistDetails(
             
playlistId: id,
             
playlistName: title,
           
),
           
_ => const Center(child: Text('Select a playlist')),
         
},
       
],
     
),
   
);
 
}
}

यह फ़ाइल कई वजहों से दिलचस्प है. सबसे पहले, यह विंडो की चौड़ाई (MediaQuery.of(context).size.width का इस्तेमाल करके) और थीम (Theme.of(context).platform का इस्तेमाल करके) दोनों का इस्तेमाल कर रहा है. इससे यह तय किया जा रहा है कि SplitView विजेट के साथ चौड़ा लेआउट दिखाया जाए या इसके बिना छोटा डिसप्ले दिखाया जाए.

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

इसके बाद, src/lib/playlists.dart फ़ाइल में बदलाव करके, उसे नीचे दिए गए कोड से मैच करें:

lib/src/playlists.dart

import 'package:flutter/material.dart';
import 'package:googleapis/youtube/v3.dart';
import 'package:provider/provider.dart';

import 'app_state.dart';

class Playlists extends StatelessWidget {
 
const Playlists({super.key, required this.playlistSelected});

 
final PlaylistsListSelected playlistSelected;

 
@override
 
Widget build(BuildContext context) {
   
return Consumer<FlutterDevPlaylists>(
     
builder: (context, flutterDev, child) {
       
final playlists = flutterDev.playlists;
       
if (playlists.isEmpty) {
         
return const Center(child: CircularProgressIndicator());
       
}

       
return _PlaylistsListView(
         
items: playlists,
         
playlistSelected: playlistSelected,
       
);
     
},
   
);
 
}
}

typedef PlaylistsListSelected = void Function(Playlist playlist);

class _PlaylistsListView extends StatefulWidget {
 
const _PlaylistsListView({
   
required this.items,
   
required this.playlistSelected,
 
});

 
final List<Playlist> items;
 
final PlaylistsListSelected playlistSelected;

 
@override
 
State<_PlaylistsListView> createState() => _PlaylistsListViewState();
}

class _PlaylistsListViewState extends State<_PlaylistsListView> {
 
late ScrollController _scrollController;

 
@override
 
void initState() {
   
super.initState();
   
_scrollController = ScrollController();
 
}

 
@override
 
void dispose() {
   
_scrollController.dispose();
   
super.dispose();
 
}

 
@override
 
Widget build(BuildContext context) {
   
return ListView.builder(
     
controller: _scrollController,
     
itemCount: widget.items.length,
     
itemBuilder: (context, index) {
       
var playlist = widget.items[index];
       
return Padding(
         
padding: const EdgeInsets.all(8.0),
         
child: ListTile(
           
leading: Image.network(
             
playlist.snippet!.thumbnails!.default_!.url!,
           
),
           
title: Text(playlist.snippet!.title!),
           
subtitle: Text(playlist.snippet!.description!),
           
onTap: () {
             
widget.playlistSelected(playlist);
           
},
         
),
       
);
     
},
   
);
 
}
}

इस फ़ाइल में बहुत बदलाव हुए हैं. ऊपर बताए गए playlistSelected कॉलबैक के अलावा, Scaffold विजेट को हटा दिया गया है. साथ ही, _PlaylistsListView विजेट को स्टेटलेस से स्टेटफ़ुल में बदल दिया गया है. यह बदलाव इसलिए ज़रूरी है, क्योंकि मालिकाना हक वाले ScrollController को बनाया और नष्ट किया जाना है.

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

आखिर में, lib/src/playlist_details.dart फ़ाइल में इस तरह बदलाव करें:

lib/src/playlist_details.dart

import 'package:flutter/material.dart';
import 'package:googleapis/youtube/v3.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/link.dart';

import 'app_state.dart';

class PlaylistDetails extends StatelessWidget {
 
const PlaylistDetails({
   
required this.playlistId,
   
required this.playlistName,
   
super.key,
 
});
 
final String playlistId;
 
final String playlistName;

 
@override
 
Widget build(BuildContext context) {
   
return Consumer<FlutterDevPlaylists>(
     
builder: (context, playlists, _) {
       
final playlistItems = playlists.playlistItems(playlistId: playlistId);
       
if (playlistItems.isEmpty) {
         
return const Center(child: CircularProgressIndicator());
       
}

       
return _PlaylistDetailsListView(playlistItems: playlistItems);
     
},
   
);
 
}
}

class _PlaylistDetailsListView extends StatefulWidget {
 
const _PlaylistDetailsListView({required this.playlistItems});
 
final List<PlaylistItem> playlistItems;

 
@override
 
State<_PlaylistDetailsListView> createState() =>
     
_PlaylistDetailsListViewState();
}

class _PlaylistDetailsListViewState extends State<_PlaylistDetailsListView> {
 
late ScrollController _scrollController;

 
@override
 
void initState() {
   
super.initState();
   
_scrollController = ScrollController();
 
}

 
@override
 
void dispose() {
   
_scrollController.dispose();
   
super.dispose();
 
}

 
@override
 
Widget build(BuildContext context) {
   
return ListView.builder(
     
controller: _scrollController,
     
itemCount: widget.playlistItems.length,
     
itemBuilder: (context, index) {
       
final playlistItem = widget.playlistItems[index];
       
return Padding(
         
padding: const EdgeInsets.all(8.0),
         
child: ClipRRect(
           
borderRadius: BorderRadius.circular(4),
           
child: Stack(
             
alignment: Alignment.center,
             
children: [
               
if (playlistItem.snippet!.thumbnails!.high != null)
                 
Image.network(playlistItem.snippet!.thumbnails!.high!.url!),
               
_buildGradient(context),
               
_buildTitleAndSubtitle(context, playlistItem),
               
_buildPlayButton(context, playlistItem),
             
],
           
),
         
),
       
);
     
},
   
);
 
}

 
Widget _buildGradient(BuildContext context) {
   
return Positioned.fill(
     
child: DecoratedBox(
       
decoration: BoxDecoration(
         
gradient: LinearGradient(
           
colors: [Colors.transparent, Theme.of(context).colorScheme.surface],
           
begin: Alignment.topCenter,
           
end: Alignment.bottomCenter,
           
stops: const [0.5, 0.95],
         
),
       
),
     
),
   
);
 
}

 
Widget _buildTitleAndSubtitle(
   
BuildContext context,
   
PlaylistItem playlistItem,
 
) {
   
return Positioned(
     
left: 20,
     
right: 0,
     
bottom: 20,
     
child: Column(
       
mainAxisSize: MainAxisSize.min,
       
crossAxisAlignment: CrossAxisAlignment.start,
       
children: [
         
Text(
           
playlistItem.snippet!.title!,
           
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
             
fontSize: 18,
             
// fontWeight: FontWeight.bold,
           
),
         
),
         
if (playlistItem.snippet!.videoOwnerChannelTitle != null)
           
Text(
             
playlistItem.snippet!.videoOwnerChannelTitle!,
             
style: Theme.of(
               
context,
             
).textTheme.bodyMedium!.copyWith(fontSize: 12),
           
),
       
],
     
),
   
);
 
}

 
Widget _buildPlayButton(BuildContext context, PlaylistItem playlistItem) {
   
return Stack(
     
alignment: AlignmentDirectional.center,
     
children: [
       
Container(
         
width: 42,
         
height: 42,
         
decoration: const BoxDecoration(
           
color: Colors.white,
           
borderRadius: BorderRadius.all(Radius.circular(21)),
         
),
       
),
       
Link(
         
uri: Uri.parse(
           
'https://www.youtube.com/watch?v=${playlistItem.snippet!.resourceId!.videoId}',
         
),
         
builder: (context, followLink) => IconButton(
           
onPressed: followLink,
           
color: Colors.red,
           
icon: const Icon(Icons.play_circle_fill),
           
iconSize: 45,
         
),
       
),
     
],
   
);
 
}
}

ऊपर दिए गए Playlists विजेट की तरह, इस फ़ाइल में भी Scaffold विजेट को हटाने और मालिकाना हक वाले ScrollController को शामिल करने के लिए बदलाव किए गए हैं.

ऐप्लिकेशन को फिर से चलाएं!

ऐप्लिकेशन को अपने पसंदीदा डेस्कटॉप पर चलाना, चाहे वह Windows, macOS या Linux हो. अब यह उम्मीद के मुताबिक काम करेगा.

macOS पर स्प्लिट व्यू में चल रहा ऐप्लिकेशन

6. वेब के हिसाब से बदलना

उन इमेज में क्या समस्या है?

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

Chrome ब्राउज़र में चल रहा ऐप्लिकेशन, जिसमें YouTube इमेज के थंबनेल नहीं हैं

डीबग कंसोल में जाकर, आपको एक छोटा सा सुझाव दिखेगा कि आपको आगे क्या करना है.

══╡ EXCEPTION CAUGHT BY IMAGE RESOURCE SERVICE ╞════════════════════════════════════════════════════
The following ProgressEvent$ object was thrown resolving an image codec:
  [object ProgressEvent]

When the exception was thrown, this was the stack

Image provider: NetworkImage("https://i.ytimg.com/vi/4AoFA19gbLo/default.jpg", scale: 1.0)
Image key: NetworkImage("https://i.ytimg.com/vi/4AoFA19gbLo/default.jpg", scale: 1.0)
════════════════════════════════════════════════════════════════════════════════════════════════════

सीओआरएस प्रॉक्सी बनाना

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

$ dart create --template server-shelf yt_cors_proxy
Creating yt_cors_proxy using template server-shelf...

  .gitignore
  analysis_options.yaml
  CHANGELOG.md
  pubspec.yaml
  README.md
  Dockerfile
  .dockerignore
  test/server_test.dart
  bin/server.dart

Running pub get...                     3.9s
  Resolving dependencies...
  Changed 53 dependencies!

Created project yt_cors_proxy in yt_cors_proxy! In order to get started, run the following commands:

  cd yt_cors_proxy
  dart run bin/server.dart

डायरेक्ट्री को yt_cors_proxy सर्वर में बदलें और कुछ ज़रूरी डिपेंडेंसी जोड़ें:

$ cd yt_cors_proxy
$ dart pub add shelf_cors_headers http
"http" was found in dev_dependencies. Removing "http" and adding it to dependencies instead.
Resolving dependencies...
  http 1.1.2 (from dev dependency to direct dependency)
  js 0.6.7 (0.7.0 available)
  lints 2.1.1 (3.0.0 available)
+ shelf_cors_headers 0.1.5
Changed 2 dependencies!
2 packages have newer versions incompatible with dependency constraints.
Try `dart pub outdated` for more information.

फ़िलहाल, कुछ डिपेंडेंसी की ज़रूरत नहीं है. इन्हें इस तरह ट्रिम करें:

$ dart pub remove args shelf_router
Resolving dependencies...
  args 2.4.2 (from direct dependency to transitive dependency)
  js 0.6.7 (0.7.0 available)
  lints 2.1.1 (3.0.0 available)
These packages are no longer being depended on:
- http_methods 1.1.1
- shelf_router 1.1.4
Changed 3 dependencies!
2 packages have newer versions incompatible with dependency constraints.
Try `dart pub outdated` for more information.

इसके बाद, server.dart फ़ाइल के कॉन्टेंट में बदलाव करके, उसे इनसे मैच कराएं:

yt_cors_proxy/bin/server.dart

import 'dart:async';
import 'dart:io';

import 'package:http/http.dart' as http;
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_cors_headers/shelf_cors_headers.dart';

Future<Response> _requestHandler(Request req) async {
 
final target = req.url.replace(scheme: 'https', host: 'i.ytimg.com');
 
final response = await http.get(target);
 
return Response.ok(response.bodyBytes, headers: response.headers);
}

void main(List<String> args) async {
 
// Use any available host or container IP (usually `0.0.0.0`).
 
final ip = InternetAddress.anyIPv4;

 
// Configure a pipeline that adds CORS headers and proxies requests.
 
final handler = Pipeline()
     
.addMiddleware(logRequests())
     
.addMiddleware(corsHeaders(headers: {ACCESS_CONTROL_ALLOW_ORIGIN: '*'}))
     
.addHandler(_requestHandler);

 
// For running in containers, we respect the PORT environment variable.
 
final port = int.parse(Platform.environment['PORT'] ?? '8080');
 
final server = await serve(handler, ip, port);
 
print('Server listening on port ${server.port}');
}

इस सर्वर को इस तरह चलाया जा सकता है:

$ dart run bin/server.dart
Server listening on port 8080

इसके अलावा, इसे Docker इमेज के तौर पर बनाया जा सकता है और बनाई गई Docker इमेज को इस तरह चलाया जा सकता है:

$ docker build . -t yt-cors-proxy
[+] Building 2.7s (14/14) FINISHED
$ docker run -p 8080:8080 yt-cors-proxy
Server listening on port 8080

इसके बाद, इस सीओआरएस प्रॉक्सी का फ़ायदा पाने के लिए, Flutter कोड में बदलाव करें. हालांकि, ऐसा सिर्फ़ वेब ब्राउज़र में चलाने पर करें.

अडैप्टिव विजेट का एक जोड़ा

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

lib/src/adaptive_image.dart

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

class AdaptiveImage extends StatelessWidget {
 
AdaptiveImage.network(String url, {super.key}) {
   
if (kIsWeb) {
     
_url = Uri.parse(
       
url,
     
).replace(host: 'localhost', port: 8080, scheme: 'http').toString();
   
} else {
     
_url = url;
   
}
 
}

 
late final String _url;

 
@override
 
Widget build(BuildContext context) {
   
return Image.network(_url);
 
}
}

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

lib/src/adaptive_text.dart

import 'package:flutter/material.dart';

class AdaptiveText extends StatelessWidget {
 
const AdaptiveText(this.data, {super.key, this.style});
 
final String data;
 
final TextStyle? style;

 
@override
 
Widget build(BuildContext context) {
   
return switch (Theme.of(context).platform) {
     
TargetPlatform.android || TargetPlatform.iOS => Text(data, style: style),
     
_ => SelectableText(data, style: style),
   
};
 
}
}

अब, इन बदलावों को कोडबेस में लागू करें:

lib/src/playlist_details.dart

import 'package:flutter/material.dart';
import 'package:googleapis/youtube/v3.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/link.dart';

import 'adaptive_image.dart';                                 // Add this line,
import 'adaptive_text.dart';                                  // And this line
import 'app_state.dart';

class PlaylistDetails extends StatelessWidget {
 
const PlaylistDetails({
   
required this.playlistId,
   
required this.playlistName,
   
super.key,
 
});
 
final String playlistId;
 
final String playlistName;

 
@override
 
Widget build(BuildContext context) {
   
return Consumer<FlutterDevPlaylists>(
     
builder: (context, playlists, _) {
       
final playlistItems = playlists.playlistItems(playlistId: playlistId);
       
if (playlistItems.isEmpty) {
         
return const Center(child: CircularProgressIndicator());
       
}

       
return _PlaylistDetailsListView(playlistItems: playlistItems);
     
},
   
);
 
}
}

class _PlaylistDetailsListView extends StatefulWidget {
 
const _PlaylistDetailsListView({required this.playlistItems});
 
final List<PlaylistItem> playlistItems;

 
@override
 
State<_PlaylistDetailsListView> createState() =>
     
_PlaylistDetailsListViewState();
}

class _PlaylistDetailsListViewState extends State<_PlaylistDetailsListView> {
 
late ScrollController _scrollController;

 
@override
 
void initState() {
   
super.initState();
   
_scrollController = ScrollController();
 
}

 
@override
 
void dispose() {
   
_scrollController.dispose();
   
super.dispose();
 
}

 
@override
 
Widget build(BuildContext context) {
   
return ListView.builder(
     
controller: _scrollController,
     
itemCount: widget.playlistItems.length,
     
itemBuilder: (context, index) {
       
final playlistItem = widget.playlistItems[index];
       
return Padding(
         
padding: const EdgeInsets.all(8.0),
         
child: ClipRRect(
           
borderRadius: BorderRadius.circular(4),
           
child: Stack(
             
alignment: Alignment.center,
             
children: [
               
if (playlistItem.snippet!.thumbnails!.high != null)
                 
AdaptiveImage.network(                      // Modify this line
                   
playlistItem.snippet!.thumbnails!.high!.url!,
                 
),
               
_buildGradient(context),
               
_buildTitleAndSubtitle(context, playlistItem),
               
_buildPlayButton(context, playlistItem),
             
],
           
),
         
),
       
);
     
},
   
);
 
}

 
Widget _buildGradient(BuildContext context) {
   
return Positioned.fill(
     
child: DecoratedBox(
       
decoration: BoxDecoration(
         
gradient: LinearGradient(
           
colors: [Colors.transparent, Theme.of(context).colorScheme.surface],
           
begin: Alignment.topCenter,
           
end: Alignment.bottomCenter,
           
stops: const [0.5, 0.95],
         
),
       
),
     
),
   
);
 
}

 
Widget _buildTitleAndSubtitle(
   
BuildContext context,
   
PlaylistItem playlistItem,
 
) {
   
return Positioned(
     
left: 20,
     
right: 0,
     
bottom: 20,
     
child: Column(
       
mainAxisSize: MainAxisSize.min,
       
crossAxisAlignment: CrossAxisAlignment.start,
       
children: [
         
AdaptiveText(                                       // Also, this line
           
playlistItem.snippet!.title!,
           
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
             
fontSize: 18,
             
// fontWeight: FontWeight.bold,
           
),
         
),
         
if (playlistItem.snippet!.videoOwnerChannelTitle != null)
           
AdaptiveText(                                     // And this line
             
playlistItem.snippet!.videoOwnerChannelTitle!,
             
style: Theme.of(
               
context,
             
).textTheme.bodyMedium!.copyWith(fontSize: 12),
           
),
       
],
     
),
   
);
 
}

 
Widget _buildPlayButton(BuildContext context, PlaylistItem playlistItem) {
   
return Stack(
     
alignment: AlignmentDirectional.center,
     
children: [
       
Container(
         
width: 42,
         
height: 42,
         
decoration: const BoxDecoration(
           
color: Colors.white,
           
borderRadius: BorderRadius.all(Radius.circular(21)),
         
),
       
),
       
Link(
         
uri: Uri.parse(
           
'https://www.youtube.com/watch?v=${playlistItem.snippet!.resourceId!.videoId}',
         
),
         
builder: (context, followLink) => IconButton(
           
onPressed: followLink,
           
color: Colors.red,
           
icon: const Icon(Icons.play_circle_fill),
           
iconSize: 45,
         
),
       
),
     
],
   
);
 
}
}

ऊपर दिए गए कोड में, आपने Image.network और Text, दोनों विजेट को अडैप्ट किया है. इसके बाद, Playlists विजेट को अडजस्ट करें.

lib/src/playlists.dart

import 'package:flutter/material.dart';
import 'package:googleapis/youtube/v3.dart';
import 'package:provider/provider.dart';

import 'adaptive_image.dart';                                 // Add this line
import 'app_state.dart';

class Playlists extends StatelessWidget {
 
const Playlists({super.key, required this.playlistSelected});

 
final PlaylistsListSelected playlistSelected;

 
@override
 
Widget build(BuildContext context) {
   
return Consumer<FlutterDevPlaylists>(
     
builder: (context, flutterDev, child) {
       
final playlists = flutterDev.playlists;
       
if (playlists.isEmpty) {
         
return const Center(child: CircularProgressIndicator());
       
}

       
return _PlaylistsListView(
         
items: playlists,
         
playlistSelected: playlistSelected,
       
);
     
},
   
);
 
}
}

typedef PlaylistsListSelected = void Function(Playlist playlist);

class _PlaylistsListView extends StatefulWidget {
 
const _PlaylistsListView({
   
required this.items,
   
required this.playlistSelected,
 
});

 
final List<Playlist> items;
 
final PlaylistsListSelected playlistSelected;

 
@override
 
State<_PlaylistsListView> createState() => _PlaylistsListViewState();
}

class _PlaylistsListViewState extends State<_PlaylistsListView> {
 
late ScrollController _scrollController;

 
@override
 
void initState() {
   
super.initState();
   
_scrollController = ScrollController();
 
}

 
@override
 
void dispose() {
   
_scrollController.dispose();
   
super.dispose();
 
}

 
@override
 
Widget build(BuildContext context) {
   
return ListView.builder(
     
controller: _scrollController,
     
itemCount: widget.items.length,
     
itemBuilder: (context, index) {
       
var playlist = widget.items[index];
       
return Padding(
         
padding: const EdgeInsets.all(8.0),
         
child: ListTile(
           
leading: AdaptiveImage.network(                   // Change this one.
             
playlist.snippet!.thumbnails!.default_!.url!,
           
),
           
title: Text(playlist.snippet!.title!),
           
subtitle: Text(playlist.snippet!.description!),
           
onTap: () {
             
widget.playlistSelected(playlist);
           
},
         
),
       
);
     
},
   
);
 
}
}

इस बार आपने सिर्फ़ Image.network विजेट में बदलाव किया है, लेकिन दो Text विजेट को पहले जैसा ही रखा है. ऐसा जान-बूझकर किया गया है, क्योंकि टेक्स्ट विजेट का इस्तेमाल करने पर, जब उपयोगकर्ता टेक्स्ट पर टैप करता है, तो ListTile का onTap फ़ंक्शन ब्लॉक हो जाता है.

ऐप्लिकेशन को वेब पर सही तरीके से चलाना

CORS प्रॉक्सी चालू होने पर, आपको ऐप्लिकेशन का वेब वर्शन चलाने में मदद मिलेगी. साथ ही, यह कुछ ऐसा दिखेगा:

Chrome ब्राउज़र में चल रहा ऐप्लिकेशन, जिसमें YouTube इमेज के थंबनेल दिख रहे हैं

7. अडैप्टिव ऑथेंटिकेशन

इस चरण में, आपको ऐप्लिकेशन को उपयोगकर्ता की पुष्टि करने की सुविधा देनी होगी. इसके बाद, उस उपयोगकर्ता की प्लेलिस्ट दिखानी होंगी. आपको उन अलग-अलग प्लैटफ़ॉर्म को कवर करने के लिए, कई प्लग इन का इस्तेमाल करना होगा जिन पर ऐप्लिकेशन चल सकता है. इसकी वजह यह है कि Android, iOS, वेब, Windows, macOS, और Linux के बीच OAuth को मैनेज करने का तरीका बहुत अलग है.

Google की पुष्टि करने की सुविधा चालू करने के लिए प्लग इन जोड़ना

Google से पुष्टि करने की सुविधा को मैनेज करने के लिए, आपको तीन पैकेज इंस्टॉल करने होंगे.

$ flutter pub add googleapis_auth google_sign_in \
    extension_google_sign_in_as_googleapis_auth
Resolving dependencies...
+ args 2.4.2
+ crypto 3.0.3
+ extension_google_sign_in_as_googleapis_auth 2.0.12
+ google_identity_services_web 0.3.0+2
+ google_sign_in 6.2.1
+ google_sign_in_android 6.1.21
+ google_sign_in_ios 5.7.2
+ google_sign_in_platform_interface 2.4.4
+ google_sign_in_web 0.12.3+2
+ googleapis_auth 1.4.1
+ js 0.6.7 (0.7.0 available)
  matcher 0.12.16 (0.12.16+1 available)
  material_color_utilities 0.5.0 (0.8.0 available)
  meta 1.10.0 (1.11.0 available)
  path 1.8.3 (1.9.0 available)
  test_api 0.6.1 (0.7.0 available)
  web 0.3.0 (0.4.0 available)
Changed 11 dependencies!
7 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

Windows, macOS, और Linux पर पुष्टि करने के लिए, googleapis_auth पैकेज का इस्तेमाल करें. ये डेस्कटॉप प्लैटफ़ॉर्म, वेब ब्राउज़र का इस्तेमाल करके पुष्टि करते हैं. Android, iOS, और वेब पर पुष्टि करने के लिए, google_sign_in और extension_google_sign_in_as_googleapis_auth पैकेज का इस्तेमाल करें. दूसरा पैकेज, दोनों पैकेज के बीच इंटरऑपरेबल शिम के तौर पर काम करता है.

कोड अपडेट करना

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

lib/src/adaptive_login.dart

import 'dart:io' show Platform;

import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:googleapis_auth/auth_io.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/link.dart';

import 'app_state.dart';

typedef _AdaptiveLoginButtonWidget =
   
Widget Function({required VoidCallback? onPressed});

class AdaptiveLogin extends StatelessWidget {
 
const AdaptiveLogin({
   
super.key,
   
required this.clientId,
   
required this.scopes,
   
required this.loginButtonChild,
 
});

 
final ClientId clientId;
 
final List<String> scopes;
 
final Widget loginButtonChild;

 
@override
 
Widget build(BuildContext context) {
   
if (kIsWeb || Platform.isAndroid || Platform.isIOS) {
     
return _GoogleSignInLogin(button: _loginButton, scopes: scopes);
   
} else {
     
return _GoogleApisAuthLogin(
       
button: _loginButton,
       
scopes: scopes,
       
clientId: clientId,
     
);
   
}
 
}

 
Widget _loginButton({required VoidCallback? onPressed}) =>
     
ElevatedButton(onPressed: onPressed, child: loginButtonChild);
}

class _GoogleSignInLogin extends StatefulWidget {
 
const _GoogleSignInLogin({required this.button, required this.scopes});

 
final _AdaptiveLoginButtonWidget button;
 
final List<String> scopes;

 
@override
 
State<_GoogleSignInLogin> createState() => _GoogleSignInLoginState();
}

class _GoogleSignInLoginState extends State<_GoogleSignInLogin> {
 
@override
 
initState() {
   
super.initState();
   
_googleSignIn = GoogleSignIn(scopes: widget.scopes);
   
_googleSignIn.onCurrentUserChanged.listen((account) {
     
if (account != null) {
       
_googleSignIn.authenticatedClient().then((authClient) {
         
final context = this.context;
         
if (authClient != null && context.mounted) {
           
context.read<AuthedUserPlaylists>().authClient = authClient;
           
context.go('/');
         
}
       
});
     
}
   
});
 
}

 
late final GoogleSignIn _googleSignIn;

 
@override
 
Widget build(BuildContext context) {
   
return Scaffold(
     
body: Center(
       
child: widget.button(
         
onPressed: () {
           
_googleSignIn.signIn();
         
},
       
),
     
),
   
);
 
}
}

class _GoogleApisAuthLogin extends StatefulWidget {
 
const _GoogleApisAuthLogin({
   
required this.button,
   
required this.scopes,
   
required this.clientId,
 
});

 
final _AdaptiveLoginButtonWidget button;
 
final List<String> scopes;
 
final ClientId clientId;

 
@override
 
State<_GoogleApisAuthLogin> createState() => _GoogleApisAuthLoginState();
}

class _GoogleApisAuthLoginState extends State<_GoogleApisAuthLogin> {
 
@override
 
initState() {
   
super.initState();
   
clientViaUserConsent(widget.clientId, widget.scopes, (url) {
     
setState(() {
       
_authUrl = Uri.parse(url);
     
});
   
}).then((authClient) {
     
final context = this.context;
     
if (context.mounted) {
       
context.read<AuthedUserPlaylists>().authClient = authClient;
       
context.go('/');
     
}
   
});
 
}

 
Uri? _authUrl;

 
@override
 
Widget build(BuildContext context) {
   
final authUrl = _authUrl;
   
if (authUrl != null) {
     
return Scaffold(
       
body: Center(
         
child: Link(
           
uri: authUrl,
           
builder: (context, followLink) =>
               
widget.button(onPressed: followLink),
         
),
       
),
     
);
   
}

   
return const Scaffold(body: Center(child: CircularProgressIndicator()));
 
}
}

यह फ़ाइल कई काम करती है. AdaptiveLogin का build तरीका, मुश्किल काम को आसान बनाता है. kIsWeb और dart:io के Platform.isXXX, दोनों को कॉल करके, यह तरीका रनटाइम प्लैटफ़ॉर्म की जांच करता है. यह Android, iOS, और वेब के लिए, _GoogleSignInLogin स्टेटफ़ुल विजेट को इंस्टैंशिएट करता है. Windows, macOS, और Linux के लिए, यह _GoogleApisAuthLogin स्टेटफ़ुल विजेट को इंस्टैंशिएट करता है.

इन क्लास का इस्तेमाल करने के लिए, अतिरिक्त कॉन्फ़िगरेशन की ज़रूरत होती है. यह कॉन्फ़िगरेशन, इस नए विजेट का इस्तेमाल करने के लिए, बाकी कोडबेस को अपडेट करने के बाद मिलता है. FlutterDevPlaylists को AuthedUserPlaylists में बदलकर, इसकी नई भूमिका को बेहतर तरीके से दिखाएं. साथ ही, कोड को अपडेट करके यह दिखाएं कि http.Client अब निर्माण के बाद पास हो गया है. आखिर में, _ApiKeyClient क्लास की ज़रूरत अब नहीं है:

lib/src/app_state.dart

import 'dart:collection';

import 'package:flutter/foundation.dart';
import 'package:googleapis/youtube/v3.dart';
import 'package:http/http.dart' as http;

class AuthedUserPlaylists extends ChangeNotifier {      // Rename class
  set authClient(http.Client client) {                  // Drop constructor, add setter
   
_api = YouTubeApi(client);
   
_loadPlaylists();
 
}

 
bool get isLoggedIn => _api != null;                  // Add property

 
Future<void> _loadPlaylists() async {
   
String? nextPageToken;
   
_playlists.clear();

   
do {
     
final response = await _api!.playlists.list(      // Add ! to _api
       
['snippet', 'contentDetails', 'id'],
       
mine: true,                                     // convert from channelId: to mine:
       
maxResults: 50,
       
pageToken: nextPageToken,
     
);
     
_playlists.addAll(response.items!);
     
_playlists.sort(
       
(a, b) => a.snippet!.title!.toLowerCase().compareTo(
         
b.snippet!.title!.toLowerCase(),
       
),
     
);
     
notifyListeners();
     
nextPageToken = response.nextPageToken;
   
} while (nextPageToken != null);
 
}

 
YouTubeApi? _api;                                     // Convert to optional

 
final List<Playlist> _playlists = [];
 
List<Playlist> get playlists => UnmodifiableListView(_playlists);

 
final Map<String, List<PlaylistItem>> _playlistItems = {};
 
List<PlaylistItem> playlistItems({required String playlistId}) {
   
if (!_playlistItems.containsKey(playlistId)) {
     
_playlistItems[playlistId] = [];
     
_retrievePlaylist(playlistId);
   
}
   
return UnmodifiableListView(_playlistItems[playlistId]!);
 
}

 
Future<void> _retrievePlaylist(String playlistId) async {
   
String? nextPageToken;
   
do {
     
var response = await _api!.playlistItems.list(    // Add ! to _api
       
['snippet', 'contentDetails'],
       
playlistId: playlistId,
       
maxResults: 25,
       
pageToken: nextPageToken,
     
);
     
var items = response.items;
     
if (items != null) {
       
_playlistItems[playlistId]!.addAll(items);
     
}
     
notifyListeners();
     
nextPageToken = response.nextPageToken;
   
} while (nextPageToken != null);
 
}
}

// Delete the now unused _ApiKeyClient class

इसके बाद, दिए गए ऐप्लिकेशन स्टेटस ऑब्जेक्ट के लिए, PlaylistDetails विजेट को नए नाम से अपडेट करें:

lib/src/playlist_details.dart

class PlaylistDetails extends StatelessWidget {
  const PlaylistDetails({
    required this.playlistId,
    required this.playlistName,
    super.key,
  });
  final String playlistId;
  final String playlistName;

  @override
  Widget build(BuildContext context) {
    return Consumer<AuthedUserPlaylists>(               // Update this line
      builder: (context, playlists, _) {
        final playlistItems = playlists.playlistItems(playlistId: playlistId);
        if (playlistItems.isEmpty) {
          return const Center(child: CircularProgressIndicator());
        }

        return _PlaylistDetailsListView(playlistItems: playlistItems);
      },
    );
  }
}

इसी तरह, Playlists विजेट को अपडेट करें:

lib/src/playlists.dart

class Playlists extends StatelessWidget {
  const Playlists({super.key, required this.playlistSelected});

  final PlaylistsListSelected playlistSelected;

  @override
  Widget build(BuildContext context) {
    return Consumer<AuthedUserPlaylists>(               // Update this line
      builder: (context, flutterDev, child) {
        final playlists = flutterDev.playlists;
        if (playlists.isEmpty) {
          return const Center(child: CircularProgressIndicator());
        }

        return _PlaylistsListView(
          items: playlists,
          playlistSelected: playlistSelected,
        );
      },
    );
  }
}

आखिर में, नए AdaptiveLogin विजेट का सही तरीके से इस्तेमाल करने के लिए, main.dart फ़ाइल को अपडेट करें:

lib/main.dart

// Drop dart:io import

import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:googleapis_auth/googleapis_auth.dart'; // Add this line
import 'package:provider/provider.dart';

import 'src/adaptive_login.dart';                      // Add this line
import 'src/adaptive_playlists.dart';
import 'src/app_state.dart';
import 'src/playlist_details.dart';

// Drop flutterDevAccountId and youTubeApiKey

// Add from this line
// From https://developers.google.com/youtube/v3/guides/auth/installed-apps#identify-access-scopes
final scopes = ['https://www.googleapis.com/auth/youtube.readonly'];

// TODO: Replace with your Client ID and Client Secret for Desktop configuration
final clientId = ClientId(
 
'TODO-Client-ID.apps.googleusercontent.com',
 
'TODO-Client-secret',
);
// To this line

final _router = GoRouter(
 
routes: <RouteBase>[
   
GoRoute(
     
path: '/',
     
builder: (context, state) {
       
return const AdaptivePlaylists();
     
},
     
// Add redirect configuration
     
redirect: (context, state) {
       
if (!context.read<AuthedUserPlaylists>().isLoggedIn) {
         
return '/login';
       
} else {
         
return null;
       
}
     
},
     
// To this line
     
routes: <RouteBase>[
       
// Add new login Route
       
GoRoute(
         
path: 'login',
         
builder: (context, state) {
           
return AdaptiveLogin(
             
clientId: clientId,
             
scopes: scopes,
             
loginButtonChild: const Text('Login to YouTube'),
           
);
         
},
       
),
       
// To this line
       
GoRoute(
         
path: 'playlist/:id',
         
builder: (context, state) {
           
final title = state.uri.queryParameters['title']!;
           
final id = state.pathParameters['id']!;
           
return Scaffold(
             
appBar: AppBar(title: Text(title)),
             
body: PlaylistDetails(playlistId: id, playlistName: title),
           
);
         
},
       
),
     
],
   
),
 
],
);

void main() {
 
runApp(
   
ChangeNotifierProvider<AuthedUserPlaylists>(       // Modify this line
     
create: (context) => AuthedUserPlaylists(),      // Modify this line
     
child: const PlaylistsApp(),
   
),
 
);
}

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

 
@override
 
Widget build(BuildContext context) {
   
return MaterialApp.router(
     
title: 'Your Playlists',                         // Change FlutterDev to Your
     
theme: FlexColorScheme.light(scheme: FlexScheme.red).toTheme,
     
darkTheme: FlexColorScheme.dark(scheme: FlexScheme.red).toTheme,
     
themeMode: ThemeMode.dark,                       // Or ThemeMode.System
     
debugShowCheckedModeBanner: false,
     
routerConfig: _router,
   
);
 
}
}

इस फ़ाइल में किए गए बदलावों से पता चलता है कि Flutter की YouTube प्लेलिस्ट दिखाने के बजाय, पुष्टि किए गए उपयोगकर्ता की प्लेलिस्ट दिखाई जा रही हैं. कोड अब पूरा हो गया है. हालांकि, पुष्टि करने के लिए google_sign_in और googleapis_auth पैकेज को सही तरीके से कॉन्फ़िगर करने के लिए, इस फ़ाइल और संबंधित Runner ऐप्लिकेशन में कई बदलाव करने की ज़रूरत है.

ऐप्लिकेशन अब पुष्टि किए गए उपयोगकर्ता की YouTube प्लेलिस्ट दिखाता है. सभी सुविधाएं सेट अप करने के बाद, आपको पुष्टि करने की सुविधा चालू करनी होगी. ऐसा करने के लिए, google_sign_in और googleapis_auth पैकेज कॉन्फ़िगर करें. पैकेज कॉन्फ़िगर करने के लिए, आपको main.dart फ़ाइल और Runner ऐप्लिकेशन की फ़ाइलों में बदलाव करना होगा.

googleapis_auth को कॉन्फ़िगर करें

पुष्टि करने की सुविधा को कॉन्फ़िगर करने का पहला चरण, उस एपीआई पासकोड को हटाना है जिसे आपने पहले कॉन्फ़िगर और इस्तेमाल किया था. अपने एपीआई प्रोजेक्ट के क्रेडेंशियल पेज पर जाएं और एपीआई पासकोड मिटाएं:

GCP कंसोल में एपीआई प्रोजेक्ट के क्रेडेंशियल पेज

इससे एक डायलॉग जनरेट होता है. इसकी पुष्टि करने के लिए, 'मिटाएं' बटन दबाएं:

&#39;क्रेडेंशियल मिटाएं&#39; पॉप-अप

इसके बाद, OAuth क्लाइंट आईडी बनाएं:

OAuth क्लाइंट आईडी बनाना

ऐप्लिकेशन टाइप के लिए, डेस्कटॉप ऐप्लिकेशन चुनें.

डेस्कटॉप ऐप्लिकेशन का टाइप चुनना

नाम स्वीकार करें और बनाएं पर क्लिक करें.

क्लाइंट आईडी को नाम देना

इससे क्लाइंट आईडी और क्लाइंट सीक्रेट बनता है. आपको googleapis_auth फ़्लो को कॉन्फ़िगर करने के लिए, lib/main.dart में इन्हें जोड़ना होगा. लागू करने से जुड़ी एक अहम जानकारी यह है कि जनरेट किए गए OAuth टोकन को कैप्चर करने के लिए, googleapis_auth फ़्लो, localhost पर चल रहे एक अस्थायी वेब सर्वर का इस्तेमाल करता है. macOS पर, macos/Runner/Release.entitlements फ़ाइल में बदलाव करना ज़रूरी है:

macos/Runner/Release.entitlements

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

आपको macos/Runner/DebugProfile.entitlements फ़ाइल में यह बदलाव करने की ज़रूरत नहीं है, क्योंकि इसमें पहले से ही com.apple.security.network.server के लिए एनटाइटलमेंट है, ताकि हॉट रीलोड और Dart VM डीबग टूल को चालू किया जा सके.

अब आपके पास Windows, macOS या Linux पर अपना ऐप्लिकेशन चलाने का विकल्प होगा. हालांकि, इसके लिए ज़रूरी है कि ऐप्लिकेशन को उन टारगेट के लिए कंपाइल किया गया हो.

लॉग इन किए हुए उपयोगकर्ता के लिए प्लेलिस्ट दिखाने वाला ऐप्लिकेशन

Android के लिए google_sign_in को कॉन्फ़िगर करना

अपने एपीआई प्रोजेक्ट के क्रेडेंशियल पेज पर वापस जाएं और एक और OAuth क्लाइंट आईडी बनाएं. हालांकि, इस बार Android चुनें:

Android ऐप्लिकेशन का टाइप चुनना

फ़ॉर्म के बाकी हिस्से में, android/app/src/main/AndroidManifest.xml में बताए गए पैकेज के नाम को पैकेज के नाम के तौर पर भरें. अगर आपने निर्देशों का सही तरीके से पालन किया है, तो यह com.example.adaptive_app होना चाहिए. Google Cloud Console के सहायता पेज पर दिए गए निर्देशों का इस्तेमाल करके, SHA-1 सर्टिफ़िकेट का फ़िंगरप्रिंट निकालें:

Android क्लाइंट आईडी को नाम देना

Android पर ऐप्लिकेशन को काम करने के लिए, यह जानकारी काफ़ी है. इस्तेमाल किए जा रहे Google API के हिसाब से, आपको अपने ऐप्लिकेशन बंडल में जनरेट की गई JSON फ़ाइल जोड़नी पड़ सकती है.

Android पर ऐप्लिकेशन चलाना

iOS के लिए google_sign_in को कॉन्फ़िगर करना

अपने एपीआई प्रोजेक्ट के क्रेडेंशियल पेज पर वापस जाएं और एक और OAuth क्लाइंट आईडी बनाएं. हालांकि, इस बार iOS चुनें:

iOS ऐप्लिकेशन का टाइप चुनना

फ़ॉर्म के बाकी हिस्से के लिए, Xcode में ios/Runner.xcworkspace खोलकर बंडल आईडी भरें. प्रोजेक्ट नेविगेटर पर जाएं और नेविगेटर में 'रनर' चुनें. इसके बाद, 'सामान्य' टैब चुनें और बंडल आइडेंटिफ़ायर कॉपी करें. अगर आपने इस कोडलैब को सिलसिलेवार तरीके से पूरा किया है, तो यह com.example.adaptiveApp होना चाहिए.

फ़ॉर्म के बाकी हिस्से के लिए, बंडल आईडी भरें. Xcode में ios/Runner.xcworkspace खोलें. प्रोजेक्ट नेविगेटर पर जाएं. Runner > सामान्य टैब पर जाएं. बंडल आइडेंटिफ़ायर को कॉपी करें. अगर आपने इस कोडलैब को सिलसिलेवार तरीके से पूरा किया है, तो इसकी वैल्यू com.example.adaptiveApp होनी चाहिए.

Xcode में बंडल आइडेंटिफ़ायर कहां देखें

फ़िलहाल, ऐप स्टोर आईडी और टीम आईडी को अनदेखा करें, क्योंकि लोकल डेवलपमेंट के लिए इनकी ज़रूरत नहीं होती:

iOS क्लाइंट आईडी को नाम देना

जनरेट की गई .plist फ़ाइल डाउनलोड करें. इसका नाम, जनरेट किए गए आपके क्लाइंट आईडी पर आधारित होता है. डाउनलोड की गई फ़ाइल का नाम बदलकर GoogleService-Info.plist करें. इसके बाद, उसे बाईं ओर मौजूद नेविगेटर में Runner/Runner में मौजूद Info.plist फ़ाइल के बगल में, चल रहे Xcode एडिटर में खींचें और छोड़ें. Xcode में विकल्पों के डायलॉग बॉक्स के लिए, ज़रूरत पड़ने पर आइटम कॉपी करें, फ़ोल्डर रेफ़रंस बनाएं, और Runner में जोड़ें टारगेट चुनें.

Xcode में iOS ऐप्लिकेशन में, जनरेट की गई plist फ़ाइल जोड़ना

Xcode से बाहर निकलें. इसके बाद, अपने पसंदीदा आईडीई में, Info.plist में ये चीज़ें जोड़ें:

ios/Runner/Info.plist

<key>CFBundleURLTypes</key>
<array>
       
<dict>
               
<key>CFBundleTypeRole</key>
               
<string>Editor</string>
               
<key>CFBundleURLSchemes</key>
               
<array>
                       
<!-- TODO Replace this value: -->
                       
<!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
                       
<string>com.googleusercontent.apps.TODO-REPLACE-ME</string>
               
</array>
       
</dict>
</array>

जनरेट की गई GoogleService-Info.plist फ़ाइल में मौजूद एंट्री से मैच करने के लिए, आपको वैल्यू में बदलाव करना होगा. अपना ऐप्लिकेशन चलाएं और लॉग इन करने के बाद, आपको अपनी प्लेलिस्ट दिखेंगी.

iOS पर चल रहा ऐप्लिकेशन

वेब के लिए google_sign_in कॉन्फ़िगर करना

अपने एपीआई प्रोजेक्ट के क्रेडेंशियल पेज पर वापस जाएं और एक और OAuth क्लाइंट आईडी बनाएं. हालांकि, इस बार वेब ऐप्लिकेशन चुनें:

वेब ऐप्लिकेशन का टाइप चुनना

फ़ॉर्म के बाकी हिस्से के लिए, आधिकारिक JavaScript ऑरिजिन को इस तरह भरें:

वेब ऐप्लिकेशन के क्लाइंट आईडी को नाम देना

इससे क्लाइंट आईडी जनरेट होता है. जनरेट किए गए क्लाइंट आईडी को शामिल करने के लिए अपडेट किया गया यह meta टैग, web/index.html में जोड़ें:

web/index.html

<meta name="google-signin-client_id" content="YOUR_GOOGLE_SIGN_IN_OAUTH_CLIENT_ID.apps.googleusercontent.com">

इस सैंपल को चलाने के लिए, थोड़ी मदद की ज़रूरत पड़ती है. आपको पिछले चरण में बनाई गई CORS प्रॉक्सी को चलाना होगा. साथ ही, आपको वेब ऐप्लिकेशन OAuth क्लाइंट आईडी फ़ॉर्म में बताए गए पोर्ट पर Flutter वेब ऐप्लिकेशन चलाना होगा. इसके लिए, नीचे दिए गए निर्देशों का पालन करें.

किसी टर्मिनल में, सीओआरएस प्रॉक्सी सर्वर को इस तरह चलाएं:

$ dart run bin/server.dart
Server listening on port 8080

दूसरे टर्मिनल में, Flutter ऐप्लिकेशन को इस तरह चलाएं:

$ flutter run -d chrome --web-hostname localhost --web-port 8090
Launching lib/main.dart on Chrome in debug mode...
Waiting for connection from debug service on Chrome...             20.4s
This app is linked to the debug service: ws://127.0.0.1:52430/Nb3Q7puZqvI=/ws
Debug service listening on ws://127.0.0.1:52430/Nb3Q7puZqvI=/ws

💪 Running with sound null safety 💪

🔥  To hot restart changes while running, press "r" or "R".
For a more detailed help message, press "h". To quit, press "q".

फिर से लॉग इन करने के बाद, आपको अपनी प्लेलिस्ट दिखेंगी:

Chrome ब्राउज़र में चल रहा ऐप्लिकेशन

8. अगले चरण

बधाई हो!

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

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