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 ऐप्लिकेशन का स्क्रीनशॉट यहां दिया गया है:
macOS पर वाइडस्क्रीन में चल रहे इस ऐप्लिकेशन का स्क्रीनशॉट, यहाँ दिए गए स्क्रीनशॉट जैसा होना चाहिए.
इस कोडलैब में, मोबाइल पर काम करने वाले Flutter ऐप्लिकेशन को ऐसे ऐप्लिकेशन में बदलने पर फ़ोकस किया गया है जो Flutter के सभी छह प्लैटफ़ॉर्म पर काम करता है. इसमें काम के न होने वाले कॉन्सेप्ट और कोड ब्लॉक को शामिल नहीं किया जाता. साथ ही, इन्हें कॉपी और पेस्ट करने के लिए उपलब्ध कराया जाता है.
आपको इस कोडलैब से क्या सीखना है?
2. Flutter डेवलपमेंट एनवायरमेंट सेट अप करना
इस लैब को पूरा करने के लिए, आपको दो सॉफ़्टवेयर की ज़रूरत होगी—Flutter SDK और एडिटर.
इनमें से किसी भी डिवाइस पर कोडलैब चलाया जा सकता है:
- आपके पास Android या iOS डिवाइस होना चाहिए, जो आपके कंप्यूटर से कनेक्ट हो और डेवलपर मोड पर सेट हो.
- iOS सिम्युलेटर (इसके लिए, Xcode टूल इंस्टॉल करना ज़रूरी है).
- Android Emulator (इसे 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
टाइप करें. - अगर आईडीई का इस्तेमाल करके ऐप्लिकेशन चलाया जाता है, तो फ़ाइल सेव करने पर ऐप्लिकेशन फिर से लोड हो जाता है.
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 पर नेटिव तौर पर चल रहा ऐप्लिकेशन दिखाया गया है:
यहां वही कोड, macOS पर नेटिव तौर पर और Chrome में चल रहा है. Chrome भी macOS पर चल रहा है.
यहां ध्यान देने वाली अहम बात यह है कि पहली नज़र में, Flutter कॉन्टेंट को उस डिसप्ले के हिसाब से अडजस्ट करने की कोशिश कर रहा है जिस पर वह चल रहा है. इन स्क्रीनशॉट को जिस लैपटॉप पर लिया गया है उसमें ज़्यादा रिज़ॉल्यूशन वाला Mac डिसप्ले है. इसलिए, ऐप्लिकेशन के macOS और वेब वर्शन, दोनों को डिवाइस पिक्सल रेशियो 2 पर रेंडर किया गया है. वहीं, iPhone 12 पर यह अनुपात 3 और Pixel 2 पर 2.63 दिखता है. सभी मामलों में, दिखाया गया टेक्स्ट काफ़ी हद तक एक जैसा होता है. इससे डेवलपर के तौर पर हमारा काम काफ़ी आसान हो जाता है.
दूसरी बात यह है कि कोड किस प्लैटफ़ॉर्म पर चल रहा है, यह देखने के लिए दो विकल्प उपलब्ध हैं. इनसे अलग-अलग वैल्यू मिलती हैं. पहले विकल्प में, dart:io
से इंपोर्ट किए गए Platform
ऑब्जेक्ट की जांच की जाती है. वहीं, दूसरे विकल्प (यह सिर्फ़ विजेट के build
तरीके में उपलब्ध है) में, BuildContext
आर्ग्युमेंट से Theme
ऑब्जेक्ट को वापस पाया जाता है.
इन दोनों तरीकों से अलग-अलग नतीजे मिलते हैं. इसकी वजह यह है कि इनका मकसद अलग-अलग होता है. Platform
से इंपोर्ट किए गए dart:io
ऑब्जेक्ट का इस्तेमाल, रेंडरिंग के विकल्पों से अलग फ़ैसले लेने के लिए किया जाता है. इसका सबसे अच्छा उदाहरण यह तय करना है कि किन प्लग इन का इस्तेमाल करना है. ऐसा हो सकता है कि किसी खास फ़िज़िकल प्लैटफ़ॉर्म के लिए, नेटिव तौर पर लागू किए गए प्लग इन से ये प्लग इन मेल न खाएं.
BuildContext
से Theme
निकालने का मकसद, थीम के हिसाब से लागू करने के फ़ैसले लेना है. इसका सबसे अच्छा उदाहरण यह तय करना है कि Slider.adaptive
में बताए गए तरीके के मुताबिक, Material slider या Cupertino slider में से किसका इस्तेमाल करना है.
अगले सेक्शन में, आपको 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 characters 1.4.0 (1.4.1 available) + flex_color_scheme 8.3.0 + flex_seed_scheme 3.5.1 > flutter_lints 6.0.0 (was 5.0.0) + flutter_web_plugins 0.0.0 from sdk flutter + go_router 16.2.0 + googleapis 14.0.0 + http 1.5.0 + http_parser 4.1.2 > lints 6.0.0 (was 5.1.1) + logging 1.3.0 material_color_utilities 0.11.1 (0.13.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.6 (0.7.7 available) + typed_data 1.4.0 + url_launcher 6.3.2 + url_launcher_android 6.3.17 + url_launcher_ios 6.3.4 + url_launcher_linux 3.2.1 + url_launcher_macos 3.2.3 + url_launcher_platform_interface 2.3.2 + url_launcher_web 2.4.1 + url_launcher_windows 3.1.4 + web 1.1.1 Changed 24 dependencies! 4 packages have newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information.
इस कमांड से, ऐप्लिकेशन में कई पैकेज जोड़े जाते हैं:
googleapis
: यह जनरेट की गई डार्ट लाइब्रेरी है. इससे Google APIs को ऐक्सेस किया जा सकता है.http
: यह एचटीटीपी अनुरोध बनाने के लिए एक लाइब्रेरी है. यह नेटिव और वेब ब्राउज़र के बीच के अंतर को छिपाती है.provider
: इससे स्टेट मैनेजमेंट की सुविधा मिलती है.url_launcher
: इससे किसी प्लेलिस्ट के वीडियो पर जाया जा सकता है. हल की गई डिपेंडेंसी से पता चलता है किurl_launcher
को Android और iOS के डिफ़ॉल्ट वर्शन के अलावा, Windows, macOS, Linux, और वेब के लिए भी लागू किया जा सकता है. इस पैकेज का इस्तेमाल करने का मतलब है कि आपको इस सुविधा के लिए, प्लैटफ़ॉर्म के हिसाब से कोड बनाने की ज़रूरत नहीं होगी.flex_color_scheme
: इससे ऐप्लिकेशन को डिफ़ॉल्ट रूप से एक अच्छी कलर स्कीम मिलती है. ज़्यादा जानने के लिए,flex_color_scheme
API के दस्तावेज़ देखें.go_router
: यह अलग-अलग स्क्रीन के बीच नेविगेशन लागू करता है. यह पैकेज, Flutter के Router का इस्तेमाल करके नेविगेट करने के लिए, यूआरएल पर आधारित एक आसान एपीआई उपलब्ध कराता है.
url_launcher
के लिए मोबाइल ऐप्लिकेशन कॉन्फ़िगर करना
url_launcher
प्लगिन के लिए, Android और iOS रनर ऐप्लिकेशन को कॉन्फ़िगर करना ज़रूरी है. iOS Flutter रनर में, plist
डिक्शनरी में ये लाइनें जोड़ें.
ios/Runner/Info.plist
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
<string>http</string>
<string>tel</string>
<string>mailto</string>
</array>
Android Flutter रनर में, 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 पर जाएं:
प्रोजेक्ट बनाने के बाद, एपीआई लाइब्रेरी पेज पर जाएं. खोज बॉक्स में, "youtube" डालें और youtube data api v3 चुनें.
YouTube Data API v3 के बारे में जानकारी देने वाले पेज पर, एपीआई चालू करें.
एपीआई चालू करने के बाद, क्रेडेंशियल पेज पर जाएं और एक एपीआई पासकोड बनाएं.
कुछ सेकंड बाद, आपको एक डायलॉग दिखेगा. इसमें आपकी नई एपीआई कुंजी होगी. आपको जल्द ही इस कुंजी का इस्तेमाल करना होगा.
कोड जोड़ें
इस चरण के बाकी हिस्से में, आपको मोबाइल ऐप्लिकेशन बनाने के लिए बहुत सारे कोड को कट और पेस्ट करना होगा. इस कोड के बारे में कोई जानकारी नहीं दी जाएगी. इस कोडलैब का मकसद, मोबाइल ऐप्लिकेशन को डेस्कटॉप और वेब, दोनों के हिसाब से बनाना है. मोबाइल के लिए 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 API कुंजी के साथ 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 पर वीडियो देखने का विकल्प मिलेगा.
हालांकि, अगर इस ऐप्लिकेशन को डेस्कटॉप पर चलाने की कोशिश की जाती है, तो डेस्कटॉप के साइज़ वाली सामान्य विंडो में बड़ा करने पर, लेआउट सही नहीं दिखेगा. अगले चरण में, आपको इस बदलाव के हिसाब से काम करने के तरीकों के बारे में जानकारी मिलेगी.
5. डेस्कटॉप के हिसाब से ढलना
डेस्कटॉप की समस्या
अगर आपने ऐप्लिकेशन को Windows, macOS या Linux जैसे किसी नेटिव डेस्कटॉप प्लैटफ़ॉर्म पर चलाया, तो आपको एक दिलचस्प समस्या दिखेगी. यह काम करता है, लेकिन यह ... अजीब लग रहा है.
इसके लिए, स्प्लिट व्यू जोड़ा गया है. इसमें बाईं ओर प्लेलिस्ट और दाईं ओर वीडियो दिखते हैं. हालांकि, आपको यह लेआउट सिर्फ़ तब चाहिए, जब कोड Android या iOS पर न चल रहा हो और विंडो का साइज़ काफ़ी बड़ा हो. इस सुविधा को लागू करने का तरीका यहां बताया गया है.
सबसे पहले, लेआउट बनाने में मदद करने के लिए split_view
पैकेज जोड़ें.
$ flutter pub add split_view Resolving dependencies... Downloading packages... characters 1.4.0 (1.4.1 available) material_color_utilities 0.11.1 (0.13.0 available) meta 1.16.0 (1.17.0 available) + split_view 3.2.1 test_api 0.7.6 (0.7.7 available) Changed 1 dependency! 4 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
s को दिखाया जा सकता है.
आखिर में, 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. अब यह आपकी उम्मीद के मुताबिक काम करेगा.
6. वेब के हिसाब से ढालना
इन इमेज में क्या है?
इस ऐप्लिकेशन को अब वेब पर चलाने की कोशिश करने पर, यह पता चलता है कि इसे वेब ब्राउज़र के हिसाब से बनाने के लिए, ज़्यादा काम करने की ज़रूरत है.
डीबग कंसोल में देखने पर, आपको एक छोटा सा सुझाव मिलेगा कि आपको आगे क्या करना है.
══╡ 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 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... Downloading packages... http 1.5.0 (from dev dependency to direct dependency) + shelf_cors_headers 0.1.5 Changed 2 dependencies!
फ़िलहाल, एक ऐसी डिपेंडेंसी है जिसकी अब ज़रूरत नहीं है. इसे इस तरह ट्रिम करें:
$ dart pub remove shelf_router Resolving dependencies... Downloading packages... These packages are no longer being depended on: - http_methods 1.1.1 - shelf_router 1.1.4 Changed 2 dependencies!
इसके बाद, 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 प्रॉक्सी चालू होने पर, आपको ऐप्लिकेशन का वेब वर्शन चलाने का विकल्प मिलेगा. यह कुछ इस तरह दिखेगा:
7. अडैप्टिव ऑथेंटिकेशन
इस चरण में, आपको ऐप्लिकेशन को बेहतर बनाना है. इसके लिए, आपको ऐप्लिकेशन को उपयोगकर्ता की पुष्टि करने की सुविधा देनी होगी. इसके बाद, उस उपयोगकर्ता की प्लेलिस्ट दिखानी होंगी. आपको अलग-अलग प्लैटफ़ॉर्म के लिए अलग-अलग प्लगिन इस्तेमाल करने होंगे, क्योंकि Android, iOS, वेब, Windows, macOS, और Linux में OAuth को अलग-अलग तरीके से हैंडल किया जाता है.
Google खाते से पुष्टि करने की सुविधा चालू करने के लिए प्लगिन जोड़ना
Google से पुष्टि करने की सुविधा को मैनेज करने के लिए, तीन पैकेज इंस्टॉल किए जाएंगे.
$ flutter pub add googleapis_auth google_sign_in \ extension_google_sign_in_as_googleapis_auth logging Resolving dependencies... Downloading packages... + args 2.7.0 characters 1.4.0 (1.4.1 available) + crypto 3.0.6 + extension_google_sign_in_as_googleapis_auth 3.0.0 + google_identity_services_web 0.3.3+1 + google_sign_in 7.1.1 + google_sign_in_android 7.0.3 + google_sign_in_ios 6.1.0 + google_sign_in_platform_interface 3.0.0 + google_sign_in_web 1.0.0 + googleapis_auth 2.0.0 logging 1.3.0 (from transitive dependency to direct dependency) material_color_utilities 0.11.1 (0.13.0 available) meta 1.16.0 (1.17.0 available) test_api 0.7.6 (0.7.7 available) Changed 11 dependencies! 4 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:async';
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:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/link.dart';
import 'app_state.dart';
final _log = Logger('AdaptiveLogin');
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.instance;
_googleSignIn.initialize();
_authEventsSubscription = _googleSignIn.authenticationEvents.listen((
event,
) async {
_log.fine('Google Sign-In authentication event: $event');
if (event is GoogleSignInAuthenticationEventSignIn) {
final googleSignInClientAuthorization = await event
.user
.authorizationClient
.authorizationForScopes(widget.scopes);
if (googleSignInClientAuthorization == null) {
_log.warning('Google Sign-In authenticated client creation failed');
return;
}
_log.fine('Google Sign-In authenticated client created');
final context = this.context;
if (context.mounted) {
context.read<AuthedUserPlaylists>().authClient =
googleSignInClientAuthorization.authClient(scopes: widget.scopes);
context.go('/');
}
}
});
// Check if user is already authenticated
_log.fine('Attempting lightweight authentication');
_googleSignIn.attemptLightweightAuthentication();
}
@override
dispose() {
_authEventsSubscription.cancel();
super.dispose();
}
late final GoogleSignIn _googleSignIn;
late final StreamSubscription _authEventsSubscription;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: widget.button(
onPressed: () {
_googleSignIn.authenticate();
},
),
),
);
}
}
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
पैकेज को सही तरीके से कॉन्फ़िगर करने के लिए, इस फ़ाइल और संबंधित रनर ऐप्लिकेशन की फ़ाइलों में अब भी कई बदलाव करने होंगे.
अब ऐप्लिकेशन में, पुष्टि किए गए उपयोगकर्ता की YouTube प्लेलिस्ट दिखती हैं. सुविधाएं सेट अप करने के बाद, आपको पुष्टि करने की सुविधा चालू करनी होगी. इसके लिए, google_sign_in
और googleapis_auth
पैकेज कॉन्फ़िगर करें. पैकेज कॉन्फ़िगर करने के लिए, आपको main.dart
फ़ाइल और रनर ऐप्लिकेशन की फ़ाइलों में बदलाव करना होगा.
googleapis_auth
को कॉन्फ़िगर करें
पुष्टि करने की सुविधा को कॉन्फ़िगर करने के लिए, सबसे पहले उस एपीआई पासकोड को हटाना होगा जिसे आपने पहले कॉन्फ़िगर किया था और इस्तेमाल किया था. अपने एपीआई प्रोजेक्ट के क्रेडेंशियल पेज पर जाएं और एपीआई पासकोड मिटाएं:
इससे एक डायलॉग बॉक्स जनरेट होता है. इसमें मिटाएं बटन पर क्लिक करके, आपको पुष्टि करनी होती है:
इसके बाद, OAuth क्लाइंट आईडी बनाएं:
ऐप्लिकेशन टाइप के लिए, डेस्कटॉप ऐप्लिकेशन चुनें.
नाम स्वीकार करें और बनाएं पर क्लिक करें.
इससे क्लाइंट आईडी और क्लाइंट सीक्रेट बनता है. आपको इसे lib/main.dart
में जोड़ना होगा, ताकि googleapis_auth
फ़्लो को कॉन्फ़िगर किया जा सके. लागू करने से जुड़ी एक अहम जानकारी यह है कि googleapis_auth फ़्लो, जनरेट किए गए OAuth टोकन को कैप्चर करने के लिए, लोकल होस्ट पर चलने वाले एक अस्थायी वेब सर्वर का इस्तेमाल करता है. 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
का एनटाइटलमेंट है. इससे हॉट रिलोड और डार्ट वीएम डीबग टूलिंग को चालू किया जा सकता है.
अब आपको Windows, macOS या Linux पर अपना ऐप्लिकेशन चलाने की सुविधा मिलनी चाहिए. हालांकि, ऐसा तब ही होगा, जब ऐप्लिकेशन को इन टारगेट पर कंपाइल किया गया हो.
Android के लिए google_sign_in
को कॉन्फ़िगर करना
अपने एपीआई प्रोजेक्ट के क्रेडेंशियल पेज पर वापस जाएं और एक और OAuth क्लाइंट आईडी बनाएं. हालांकि, इस बार Android: चुनें
फ़ॉर्म के बाकी हिस्से में, पैकेज का नाम डालें. यह नाम, android/app/src/main/AndroidManifest.xml
में बताए गए पैकेज के नाम से मेल खाना चाहिए. अगर आपने निर्देशों का पालन किया है, तो यह com.example.adaptive_app
होना चाहिए. Google Cloud Console के सहायता पेज पर दिए गए निर्देशों का इस्तेमाल करके, SHA-1 सर्टिफ़िकेट का फ़िंगरप्रिंट निकालें:
Android पर ऐप्लिकेशन को चालू करने के लिए, यह काफ़ी है. आपके इस्तेमाल किए जा रहे Google API के हिसाब से, आपको जनरेट की गई JSON फ़ाइल को अपने ऐप्लिकेशन बंडल में जोड़ना पड़ सकता है.
iOS के लिए google_sign_in
को कॉन्फ़िगर करना
अपने एपीआई प्रोजेक्ट के क्रेडेंशियल पेज पर वापस जाएं और एक और OAuth क्लाइंट आईडी बनाएं. हालांकि, इस बार iOS: चुनें
फ़ॉर्म के बाकी हिस्से के लिए, Xcode में ios/Runner.xcworkspace
खोलकर बंडल आईडी डालें. प्रोजेक्ट नेविगेटर पर जाएं. इसके बाद, नेविगेटर में Runner को चुनें. फिर, सामान्य टैब को चुनें और बंडल आइडेंटिफ़ायर को कॉपी करें. अगर आपने इस कोडलैब के हर चरण को पूरा किया है, तो यह com.example.adaptiveApp
होना चाहिए.
फ़ॉर्म के बाकी हिस्से में, बंडल आईडी डालें. ios/Runner.xcworkspace
को Xcode में खोलें. प्रोजेक्ट नेविगेटर पर जाएं. रनर > सामान्य टैब पर जाएं. बंडल आइडेंटिफ़ायर को कॉपी करें. अगर आपने इस कोडलैब को सिलसिलेवार तरीके से पूरा किया है, तो इसकी वैल्यू com.example.adaptiveApp
होनी चाहिए.
फ़िलहाल, App Store आईडी और टीम आईडी को अनदेखा करें, क्योंकि स्थानीय डेवलपमेंट के लिए इनकी ज़रूरत नहीं होती:
जनरेट की गई .plist
फ़ाइल डाउनलोड करें. इसका नाम, जनरेट किए गए क्लाइंट आईडी पर आधारित होता है. डाउनलोड की गई फ़ाइल का नाम बदलकर GoogleService-Info.plist
करें. इसके बाद, इसे अपने चालू Xcode एडिटर में खींचें और छोड़ें. इसे बाईं ओर मौजूद नेविगेटर में, Runner/Runner
में मौजूद Info.plist
फ़ाइल के साथ रखें. Xcode में विकल्पों वाले डायलॉग बॉक्स के लिए, अगर ज़रूरी हो, तो Copy items, Create folder references, और Add to the Runner टारगेट चुनें.
Xcode से बाहर निकलें. इसके बाद, अपने पसंदीदा IDE में, 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
फ़ाइल में मौजूद एंट्री से मेल खानी होगी. ऐप्लिकेशन चलाएं. इसके बाद, लॉग इन करने पर आपको अपनी प्लेलिस्ट दिखेंगी.
वेब के लिए google_sign_in
को कॉन्फ़िगर करना
अपने एपीआई प्रोजेक्ट के क्रेडेंशियल पेज पर वापस जाएं और एक और OAuth क्लाइंट आईडी बनाएं. हालांकि, इस बार वेब ऐप्लिकेशन चुनें:
फ़ॉर्म के बाकी हिस्से में, आधिकारिक JavaScript ऑरिजिन की जानकारी इस तरह भरें:
इससे क्लाइंट आईडी जनरेट होता है. web/index.html
में यह meta
टैग जोड़ें. इसे जनरेट किए गए क्लाइंट आईडी को शामिल करने के लिए अपडेट किया गया है:
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".
फिर से लॉग इन करने के बाद, आपको अपनी प्लेलिस्ट दिखेंगी:
8. अगले चरण
बधाई हो!
आपने कोडलैब पूरा कर लिया है और एक ऐसा अडैप्टिव फ़्लटर ऐप्लिकेशन बनाया है जो उन सभी छह प्लैटफ़ॉर्म पर काम करता है जिन पर फ़्लटर काम करता है. आपने कोड को इस तरह से अडैप्ट किया है कि स्क्रीन के लेआउट, टेक्स्ट के साथ इंटरैक्ट करने के तरीके, इमेज लोड करने के तरीके, और पुष्टि करने के तरीके में अंतर होने पर भी कोड काम करे.
ऐप्लिकेशन में और भी कई चीज़ें बदली जा सकती हैं. कोड को अलग-अलग एनवायरमेंट के हिसाब से ढालने के अन्य तरीकों के बारे में जानने के लिए, अनुकूलित ऐप्लिकेशन बनाना लेख पढ़ें.