ফ্লটারে অভিযোজিত অ্যাপ

1. ভূমিকা

Flutter হল Google-এর UI টুলকিট যা একটি একক কোডবেস থেকে মোবাইল, ওয়েব এবং ডেস্কটপের জন্য সুন্দর, স্থানীয়ভাবে সংকলিত অ্যাপ্লিকেশন তৈরির জন্য। এই কোডল্যাবে, আপনি শিখবেন কীভাবে একটি ফ্লাটার অ্যাপ তৈরি করতে হয় যা এটি যে প্ল্যাটফর্মে চলছে তার সাথে খাপ খাইয়ে নেয়, তা Android, iOS, ওয়েব, Windows, macOS বা Linux হোক।

আপনি কি শিখবেন

  • Flutter দ্বারা সমর্থিত ছয়টি প্ল্যাটফর্মে কাজ করার জন্য মোবাইলের জন্য ডিজাইন করা একটি ফ্লাটার অ্যাপ কীভাবে বাড়ানো যায়।
  • প্ল্যাটফর্ম সনাক্তকরণের জন্য বিভিন্ন ফ্লাটার API এবং প্রতিটি API কখন ব্যবহার করতে হবে।
  • ওয়েবে একটি অ্যাপ চালানোর সীমাবদ্ধতা এবং প্রত্যাশার সাথে খাপ খাইয়ে নেওয়া।
  • Flutter এর প্ল্যাটফর্মের সম্পূর্ণ পরিসরকে সমর্থন করার জন্য একে অপরের পাশাপাশি বিভিন্ন প্যাকেজ কীভাবে ব্যবহার করবেন।

আপনি কি নির্মাণ করবেন

এই কোডল্যাবে, আপনি প্রাথমিকভাবে Android এবং iOS-এর জন্য একটি Flutter অ্যাপ তৈরি করবেন যা Flutter-এর YouTube প্লেলিস্টগুলি অন্বেষণ করে৷ তারপরে আপনি এই অ্যাপ্লিকেশনটিকে তিনটি ডেস্কটপ প্ল্যাটফর্মে (উইন্ডোজ, ম্যাকওএস এবং লিনাক্স) কাজ করার জন্য মানিয়ে নেবেন অ্যাপ্লিকেশন উইন্ডোর আকার অনুযায়ী তথ্য কীভাবে প্রদর্শিত হয় তা পরিবর্তন করে। তারপরে আপনি অ্যাপটিতে প্রদর্শিত পাঠ্য নির্বাচনযোগ্য করে ওয়েবের জন্য অ্যাপ্লিকেশনটিকে মানিয়ে নেবেন, যেমন ওয়েব ব্যবহারকারীরা আশা করেন। অবশেষে, আপনি অ্যাপটিতে প্রমাণীকরণ যোগ করবেন যাতে আপনি আপনার নিজস্ব প্লেলিস্টগুলি অন্বেষণ করতে পারেন, যেমনটি ফ্লাটার টিমের দ্বারা তৈরি করা হয়েছে, যার জন্য তিনটি ডেস্কটপ প্ল্যাটফর্ম উইন্ডোজ বনাম Android, iOS এবং ওয়েবের জন্য প্রমাণীকরণের জন্য বিভিন্ন পদ্ধতির প্রয়োজন। macOS, এবং Linux।

এখানে Android এবং iOS-এ Flutter অ্যাপের একটি স্ক্রিনশট রয়েছে:

অ্যান্ড্রয়েড এমুলেটরে চলমান সমাপ্ত অ্যাপ

iOS সিমুলেটরে চলমান সমাপ্ত অ্যাপ

macOS-এ ওয়াইডস্ক্রীনে চলমান এই অ্যাপটি নিম্নলিখিত স্ক্রিনশটের মতো হওয়া উচিত।

macOS-এ চলমান সমাপ্ত অ্যাপ

এই কোডল্যাবটি একটি মোবাইল ফ্লাটার অ্যাপকে একটি অভিযোজিত অ্যাপে রূপান্তরিত করার উপর ফোকাস করে যা ছয়টি ফ্লাটার প্ল্যাটফর্ম জুড়ে কাজ করে। অ-প্রাসঙ্গিক ধারণা এবং কোড ব্লকগুলিকে চকচকে করা হয়েছে এবং আপনাকে সহজভাবে অনুলিপি এবং পেস্ট করার জন্য সরবরাহ করা হয়েছে।

আপনি এই কোডল্যাব থেকে কি শিখতে চান?

আমি এই বিষয়ে নতুন, এবং আমি একটি ভাল ওভারভিউ চাই। আমি এই বিষয় সম্পর্কে কিছু জানি, কিন্তু আমি একটি রিফ্রেশার চাই. আমি আমার প্রকল্পে ব্যবহার করার জন্য উদাহরণ কোড খুঁজছি। আমি নির্দিষ্ট কিছু একটি ব্যাখ্যা খুঁজছি.

2. আপনার ফ্লটার ডেভেলপমেন্ট এনভায়রনমেন্ট সেট আপ করুন

এই ল্যাবটি সম্পূর্ণ করার জন্য আপনার দুটি টুকরো সফ্টওয়্যার প্রয়োজন - ফ্লাটার SDK এবং একটি সম্পাদক

আপনি এই ডিভাইসগুলির যেকোনো একটি ব্যবহার করে কোডল্যাব চালাতে পারেন:

  • আপনার কম্পিউটারের সাথে সংযুক্ত এবং বিকাশকারী মোডে সেট করা একটি শারীরিক Android বা iOS ডিভাইস৷
  • আইওএস সিমুলেটর (এক্সকোড সরঞ্জাম ইনস্টল করা প্রয়োজন)।
  • অ্যান্ড্রয়েড এমুলেটর (অ্যান্ড্রয়েড স্টুডিওতে সেটআপ প্রয়োজন)।
  • একটি ব্রাউজার (ডিবাগিংয়ের জন্য Chrome প্রয়োজন)।
  • একটি Windows , Linux , বা macOS ডেস্কটপ অ্যাপ্লিকেশন হিসাবে। আপনি যে প্ল্যাটফর্মে স্থাপন করার পরিকল্পনা করছেন সেখানে আপনাকে অবশ্যই বিকাশ করতে হবে। সুতরাং, আপনি যদি একটি উইন্ডোজ ডেস্কটপ অ্যাপ বিকাশ করতে চান, তাহলে যথাযথ বিল্ড চেইন অ্যাক্সেস করতে আপনাকে অবশ্যই উইন্ডোজে বিকাশ করতে হবে। অপারেটিং সিস্টেম-নির্দিষ্ট প্রয়োজনীয়তা রয়েছে যা docs.flutter.dev/desktop- এ বিস্তারিতভাবে কভার করা হয়েছে।

3. শুরু করুন

আপনার উন্নয়ন পরিবেশ নিশ্চিত করা

সবকিছু বিকাশের জন্য প্রস্তুত তা নিশ্চিত করার সবচেয়ে সহজ উপায়, অনুগ্রহ করে নিম্নলিখিত কমান্ডটি চালান:

$ flutter doctor

যদি চেকমার্ক ছাড়াই কিছু দেখানো হয়, তাহলে ভুল কী তা আরও বিশদ জানতে অনুগ্রহ করে নিম্নলিখিতটি চালান:

$ flutter doctor -v

মোবাইল বা ডেস্কটপ ডেভেলপমেন্টের জন্য আপনাকে ডেভেলপার টুল ইনস্টল করতে হতে পারে। আপনার হোস্ট অপারেটিং সিস্টেমের উপর নির্ভর করে আপনার টুলিং কনফিগার করার বিষয়ে আরও বিস্তারিত জানার জন্য, অনুগ্রহ করে ফ্লটার ইনস্টলেশন ডকুমেন্টেশনে ডকুমেন্টেশন দেখুন।

একটি ফ্লটার প্রকল্প তৈরি করা হচ্ছে

ডেস্কটপ অ্যাপের জন্য Flutter লেখা শুরু করার একটি সহজ উপায় হল একটি Flutter প্রকল্প তৈরি করতে Flutter কমান্ড-লাইন টুল ব্যবহার করা। বিকল্পভাবে, আপনার IDE এর UI এর মাধ্যমে একটি 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.

সবকিছু কাজ করছে তা নিশ্চিত করতে, বয়লারপ্লেট ফ্লাটার অ্যাপ্লিকেশনটিকে একটি মোবাইল অ্যাপ হিসাবে চালান যা নীচে দেখানো হয়েছে। বিকল্পভাবে, আপনার IDE-তে এই প্রকল্পটি খুলুন এবং অ্যাপ্লিকেশন চালানোর জন্য এর টুলিং ব্যবহার করুন। পূর্ববর্তী পদক্ষেপের জন্য ধন্যবাদ, ডেস্কটপ অ্যাপ্লিকেশন হিসাবে চলমান একমাত্র উপলব্ধ বিকল্প হওয়া উচিত।

$ 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';
    }
  }
}

উপরের অ্যাপটি আপনাকে বিভিন্ন প্ল্যাটফর্মকে কীভাবে শনাক্ত করা যায় এবং মানিয়ে নেওয়া যায় তার অনুভূতি দেওয়ার জন্য ডিজাইন করা হয়েছে। এখানে অ্যানড্রয়েড এবং আইওএসে নেটিভভাবে চলমান অ্যাপটি রয়েছে:

অ্যান্ড্রয়েড এমুলেটরে উইন্ডো বৈশিষ্ট্য দেখানো হচ্ছে

iOS সিমুলেটরে উইন্ডো বৈশিষ্ট্য দেখানো হচ্ছে

এবং এখানে একই কোড স্থানীয়ভাবে ম্যাকওএস এবং ক্রোমের ভিতরে চলছে, আবার ম্যাকোসে চলছে।

macOS-এ উইন্ডোর বৈশিষ্ট্য দেখানো হচ্ছে

Chrome ব্রাউজারে উইন্ডোর বৈশিষ্ট্যগুলি দেখানো হচ্ছে৷

এখানে লক্ষণীয় গুরুত্বপূর্ণ বিষয় হল, প্রথম নজরে, Flutter এটি যে ডিসপ্লেতে চলছে তার সাথে বিষয়বস্তুকে মানিয়ে নিতে যা করতে পারে তা করছে৷ যে ল্যাপটপে এই স্ক্রিনশটগুলি নেওয়া হয়েছে তাতে একটি উচ্চ রেজোলিউশনের ম্যাক ডিসপ্লে রয়েছে, যার কারণে অ্যাপটির macOS এবং ওয়েব সংস্করণ দুটিই ডিভাইস পিক্সেল অনুপাত 2 এ রেন্ডার করা হয়েছে। এদিকে, iPhone 12-এ, আপনি 3 অনুপাত দেখতে পাচ্ছেন, এবং পিক্সেল 2-এ 2.63। সমস্ত ক্ষেত্রে প্রদর্শিত পাঠ্য মোটামুটি একই রকম, যা ডেভেলপার হিসেবে আমাদের কাজকে অনেক সহজ করে তোলে।

উল্লেখ্য দ্বিতীয় পয়েন্ট হল যে কোডটি কোন প্ল্যাটফর্মে চলছে তা পরীক্ষা করার জন্য দুটি বিকল্প বিভিন্ন মানের ফলাফলে। প্রথম বিকল্পটি dart:io থেকে আমদানি করা Platform অবজেক্ট পরিদর্শন করে, যখন দ্বিতীয় বিকল্পটি (শুধুমাত্র উইজেটের build পদ্ধতির মধ্যে উপলব্ধ), BuildContext আর্গুমেন্ট থেকে Theme অবজেক্টটি পুনরুদ্ধার করে।

এই দুটি পদ্ধতি ভিন্ন ফলাফল প্রদানের কারণ হল তাদের উদ্দেশ্য ভিন্ন। dart:io থেকে আমদানি করা Platform অবজেক্টটি এমন সিদ্ধান্ত নেওয়ার জন্য ব্যবহার করা হয় যা রেন্ডারিং পছন্দের থেকে স্বাধীন। এর একটি প্রধান উদাহরণ হল কোন প্লাগইনগুলি ব্যবহার করতে হবে তা নির্ধারণ করা, কোন নির্দিষ্ট শারীরিক প্ল্যাটফর্মের জন্য মেলে বা নাও থাকতে পারে।

BuildContext থেকে Theme এক্সট্র্যাক্ট করা হচ্ছে থিম কেন্দ্রিক সিদ্ধান্ত বাস্তবায়নের জন্য। এর একটি প্রধান উদাহরণ হল ম্যাটেরিয়াল স্লাইডার বা Cupertino স্লাইডার ব্যবহার করার সিদ্ধান্ত নেওয়া, যেমনটি Slider.adaptive এ আলোচনা করা হয়েছে।

পরবর্তী বিভাগে আপনি একটি মৌলিক YouTube প্লেলিস্ট এক্সপ্লোরার অ্যাপ তৈরি করবেন যা সম্পূর্ণরূপে Android এবং iOS-এর জন্য অপ্টিমাইজ করা হয়েছে। নিম্নলিখিত বিভাগগুলিতে আপনি অ্যাপটিকে ডেস্কটপ এবং ওয়েবে আরও ভালভাবে কাজ করতে বিভিন্ন অভিযোজন যুক্ত করবেন।

4. একটি মোবাইল অ্যাপ তৈরি করুন৷

প্যাকেজ যোগ করুন

এই অ্যাপটিতে আপনি YouTube ডেটা এপিআই , স্টেট ম্যানেজমেন্ট এবং থিমিংয়ের স্পর্শ পেতে বিভিন্ন ধরনের ফ্লাটার প্যাকেজ ব্যবহার করবেন।

$ flutter pub add googleapis http provider url_launcher flex_color_scheme go_router
Resolving dependencies... 
Downloading packages... 
+ _discoveryapis_commons 1.0.6
+ flex_color_scheme 7.3.1
+ flex_seed_scheme 1.5.0
+ flutter_web_plugins 0.0.0 from sdk flutter
+ go_router 14.0.1
+ googleapis 13.1.0
+ http 1.2.1
+ http_parser 4.0.2
  leak_tracker 10.0.4 (10.0.5 available)
  leak_tracker_flutter_testing 3.0.3 (3.0.5 available)
+ logging 1.2.0
  material_color_utilities 0.8.0 (0.11.1 available)
  meta 1.12.0 (1.14.0 available)
+ nested 1.0.0
+ plugin_platform_interface 2.1.8
+ provider 6.1.2
  test_api 0.7.0 (0.7.1 available)
+ typed_data 1.3.2
+ url_launcher 6.2.6
+ url_launcher_android 6.3.1
+ url_launcher_ios 6.2.5
+ url_launcher_linux 3.1.1
+ url_launcher_macos 3.1.0
+ url_launcher_platform_interface 2.3.2
+ url_launcher_web 2.3.1
+ url_launcher_windows 3.1.1
+ web 0.5.1
Changed 22 dependencies!
5 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

এই কমান্ডটি অ্যাপ্লিকেশনটিতে বেশ কয়েকটি প্যাকেজ যুক্ত করে:

  • googleapis : একটি জেনারেটেড ডার্ট লাইব্রেরি যা Google API- এ অ্যাক্সেস প্রদান করে।
  • http : HTTP অনুরোধ তৈরি করার জন্য একটি লাইব্রেরি যা নেটিভ এবং ওয়েব ব্রাউজারের মধ্যে পার্থক্য লুকিয়ে রাখে।
  • provider : রাষ্ট্র পরিচালনা প্রদান করে।
  • url_launcher : একটি প্লেলিস্ট থেকে একটি ভিডিওতে লাফানোর উপায় প্রদান করে। সমাধান করা নির্ভরতা থেকে দেখানো হয়েছে, url_launcher ডিফল্ট Android এবং iOS ছাড়াও Windows, macOS, Linux এবং ওয়েবের জন্য বাস্তবায়ন রয়েছে। এই প্যাকেজটি ব্যবহার করার অর্থ হল এই কার্যকারিতার জন্য আপনাকে নির্দিষ্ট প্ল্যাটফর্ম তৈরি করার প্রয়োজন হবে না।
  • flex_color_scheme : অ্যাপটিকে একটি সুন্দর ডিফল্ট রঙের স্কিম দেয়। আরও জানতে, flex_color_scheme API ডকুমেন্টেশন দেখুন।
  • go_router : বিভিন্ন স্ক্রিনের মধ্যে নেভিগেশন প্রয়োগ করে। এই প্যাকেজটি Flutter's Router ব্যবহার করে নেভিগেট করার জন্য একটি সুবিধাজনক, url-ভিত্তিক API প্রদান করে।

url_launcher এর জন্য মোবাইল অ্যাপ কনফিগার করা হচ্ছে

url_launcher প্লাগইনটির জন্য Android এবং iOS রানার অ্যাপ্লিকেশনের কনফিগারেশন প্রয়োজন। iOS ফ্লাটার রানারে, 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 এ নিম্নলিখিত লাইন যোগ করুন। manifest নোডের সরাসরি চাইল্ড এবং application নোডের পিয়ার হিসেবে এই queries নোড যোগ করুন।

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 ডেটা API অ্যাক্সেস করা হচ্ছে

প্লেলিস্ট তালিকাভুক্ত করতে YouTube ডেটা API অ্যাক্সেস করতে, আপনাকে প্রয়োজনীয় API কী তৈরি করতে একটি API প্রকল্প তৈরি করতে হবে। এই পদক্ষেপগুলি অনুমান করে যে আপনার ইতিমধ্যেই একটি Google অ্যাকাউন্ট আছে, তাই যদি আপনি ইতিমধ্যে একটি সহজ না পেয়ে থাকেন তাহলে একটি তৈরি করুন৷

একটি API প্রকল্প তৈরি করতে বিকাশকারী কনসোলে নেভিগেট করুন:

প্রকল্প তৈরির প্রবাহের সময় GCP কনসোল দেখানো হচ্ছে

একবার আপনার একটি প্রকল্প হয়ে গেলে, API লাইব্রেরি পৃষ্ঠায় যান। অনুসন্ধান বাক্সে, "youtube" লিখুন এবং youtube data api v3 নির্বাচন করুন।

GCP কনসোলে YouTube ডেটা API v3 নির্বাচন করা

YouTube ডেটা API v3 বিস্তারিত পৃষ্ঠায়, API সক্ষম করুন৷

5a877ea82b83ae42.png

একবার আপনি API সক্ষম করলে, শংসাপত্র পৃষ্ঠাতে নেভিগেট করুন এবং একটি API কী তৈরি করুন৷

GCP কনসোলে শংসাপত্র তৈরি করা হচ্ছে

কয়েক সেকেন্ড পরে, আপনি আপনার চকচকে নতুন API কী সহ একটি ডায়ালগ দেখতে পাবেন। আপনি শীঘ্রই এই কী ব্যবহার করা হবে.

API কী তৈরি করা পপআপ তৈরি API কী দেখাচ্ছে

কোড যোগ করুন

এই ধাপের বাকি অংশের জন্য আপনি একটি মোবাইল অ্যাপ তৈরি করতে অনেক কোড কাট'এন'পেস্ট করবেন, কোডের কোনো মন্তব্য ছাড়াই। এই কোডল্যাবের উদ্দেশ্য হল মোবাইল অ্যাপটি গ্রহণ করা এবং এটিকে ডেস্কটপ এবং ওয়েব উভয়ের সাথে মানিয়ে নেওয়া। মোবাইলের জন্য ফ্লাটার অ্যাপ তৈরির আরও বিস্তারিত ভূমিকার জন্য, অনুগ্রহ করে আপনার প্রথম ফ্লাটার অ্যাপ লিখুন, পার্ট 1 , পার্ট 2 এবং ফ্লটার দিয়ে সুন্দর UI তৈরি করা দেখুন।

নিম্নলিখিত ফাইলগুলি যোগ করুন, প্রথমে অ্যাপের জন্য স্টেট অবজেক্ট।

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,
        useMaterial3: true,
      ).toTheme,
      darkTheme: FlexColorScheme.dark(
        scheme: FlexScheme.red,
        useMaterial3: true,
      ).toTheme,
      themeMode: ThemeMode.dark, // Or ThemeMode.System if you'd prefer
      debugShowCheckedModeBanner: false,
      routerConfig: _router,
    );
  }
}

আপনি অ্যান্ড্রয়েড এবং iOS এ এই কোডটি চালানোর জন্য প্রায় প্রস্তুত৷ পরিবর্তন করার জন্য শুধু আর একটি জিনিস, আগের ধাপে তৈরি করা YouTube API কী দিয়ে 14 লাইনে youTubeApiKey ধ্রুবক পরিবর্তন করুন।

lib/main.dart

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

macOS-এ এই অ্যাপটি চালানোর জন্য, আপনাকে নিম্নরূপ HTTP অনুরোধগুলি করতে অ্যাপটিকে সক্রিয় করতে হবে। 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>

অ্যাপটি চালান

এখন আপনার কাছে একটি সম্পূর্ণ অ্যাপ্লিকেশন রয়েছে, আপনি এটিকে একটি অ্যান্ড্রয়েড এমুলেটর বা একটি আইফোন সিমুলেটরে সফলভাবে চালাতে সক্ষম হবেন৷ আপনি ফ্লটারের প্লেলিস্টের একটি তালিকা দেখতে পাবেন, যখন আপনি একটি প্লেলিস্ট নির্বাচন করবেন তখন আপনি সেই প্লেলিস্টের ভিডিওগুলি দেখতে পাবেন এবং অবশেষে আপনি প্লে বোতামে ক্লিক করলে, ভিডিওটি দেখার জন্য আপনাকে YouTube অভিজ্ঞতায় চালু করা হবে।

FlutterDev YouTube অ্যাকাউন্টের প্লেলিস্ট দেখানো অ্যাপটি

একটি নির্দিষ্ট প্লেলিস্টে ভিডিও দেখানো হচ্ছে

YouTube প্লেয়ারের ভিতরে একটি নির্বাচিত ভিডিও চলছে৷

যাইহোক, আপনি যদি এই অ্যাপটিকে ডেস্কটপে চালানোর চেষ্টা করেন, তাহলে আপনি দেখতে পাবেন যে লেআউটটি একটি সাধারণ ডেস্কটপ-আকারের উইন্ডোতে প্রসারিত হলে ভুল মনে হচ্ছে। আপনি পরবর্তী ধাপে এটির সাথে মানিয়ে নেওয়ার উপায়গুলি দেখবেন৷

5. ডেস্কটপে মানিয়ে নেওয়া

ডেস্কটপ সমস্যা

আপনি যদি একটি নেটিভ ডেস্কটপ প্ল্যাটফর্ম, উইন্ডোজ, ম্যাকওএস বা লিনাক্সে অ্যাপটি চালান তবে আপনি একটি আকর্ষণীয় সমস্যা লক্ষ্য করবেন। এটা কাজ করে, কিন্তু এটা দেখতে ... অদ্ভুত.

ম্যাকওএস-এ চলমান অ্যাপটি প্লেলিস্টের একটি তালিকা দেখাচ্ছে, অদ্ভুতভাবে আনুপাতিক দেখাচ্ছে

একটি প্লেলিস্টের ভিডিওগুলি, macOS-এ৷

এর জন্য একটি সমাধান হল একটি বিভক্ত দৃশ্য যোগ করা, বামদিকে প্লেলিস্ট এবং ডানদিকে ভিডিওগুলি তালিকাভুক্ত করা৷ যাইহোক, আপনি শুধুমাত্র এই লেআউটটি চালু করতে চান যখন কোডটি Android বা iOS এ চলছে না এবং উইন্ডোটি যথেষ্ট প্রশস্ত। নিম্নলিখিত নির্দেশাবলী এই ক্ষমতা বাস্তবায়ন কিভাবে দেখায়.

প্রথমে, লেআউট তৈরিতে সাহায্য করার জন্য split_view প্যাকেজে যোগ করুন।

$ flutter pub add split_view
Resolving dependencies...
+ split_view 3.1.0
  test_api 0.4.3 (0.4.8 available)
Changed 1 dependency!

অভিযোজিত উইজেট উপস্থাপন করা হচ্ছে

আপনি এই কোডল্যাবে যে প্যাটার্নটি ব্যবহার করতে যাচ্ছেন তা হল অ্যাডাপটিভ উইজেটগুলি প্রবর্তন করা যা স্ক্রীনের প্রস্থ, প্ল্যাটফর্ম থিম এবং এর মতো বৈশিষ্ট্যগুলির উপর ভিত্তি করে বাস্তবায়ন পছন্দ করে। এই ক্ষেত্রে, আপনি একটি 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,
        useMaterial3: true,
      ).toTheme,
      darkTheme: FlexColorScheme.dark(
        scheme: FlexScheme.red,
        useMaterial3: true,
      ).toTheme,
      themeMode: ThemeMode.dark, // Or ThemeMode.System if you'd prefer
      debugShowCheckedModeBanner: false,
      routerConfig: _router,
    );
  }
}

এরপরে, অ্যাডাপটিভ প্লেলিস্ট উইজেটের জন্য ফাইলটি তৈরি করুন:

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);
            },
          ),
        );
      },
    );
  }
}

এই ফাইলে অনেক পরিবর্তন আছে। একটি প্লেলিস্ট সিলেক্টেড কলব্যাক এবং Scaffold উইজেট দূরীকরণের পূর্বোক্ত প্রবর্তন ছাড়াও, _PlaylistsListView উইজেট স্টেটলেস থেকে স্টেটফুল এ রূপান্তরিত হয়। একটি মালিকানাধীন ScrollController প্রবর্তনের কারণে এই পরিবর্তনটি প্রয়োজন যা নির্মাণ এবং ধ্বংস করতে হবে।

একটি ScrollController পরিচিতি আকর্ষণীয় কারণ এটি প্রয়োজনীয় কারণ একটি প্রশস্ত লেআউটে আপনার পাশাপাশি দুটি ListView উইজেট রয়েছে। একটি মোবাইল ফোনে একটি একক ListView থাকা প্রথাগত, এবং এইভাবে একটি একক দীর্ঘস্থায়ী স্ক্রোলকন্ট্রোলার থাকতে পারে যা সমস্ত 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. ওয়েবে মানিয়ে নেওয়া

সেই ছবিগুলোর কি অবস্থা, হা?

ওয়েবে এই অ্যাপটি চালানোর প্রয়াস এখন ওয়েব ব্রাউজারগুলির সাথে খাপ খাইয়ে নিতে আরও কাজ করতে হবে।

কোনো YouTube ছবির থাম্বনেইল ছাড়াই Chrome ব্রাউজারে অ্যাপটি চলছে

আপনি যদি ডিবাগ কনসোলে একটি উঁকিঝুঁকি দেখেন, আপনি পরবর্তীতে কী করতে হবে সে সম্পর্কে একটি মৃদু ইঙ্গিত দেখতে পাবেন।

══╡ 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)
════════════════════════════════════════════════════════════════════════════════════════════════════

একটি CORS প্রক্সি তৈরি করা হচ্ছে

ইমেজ রেন্ডারিং সমস্যাগুলি মোকাবেলা করার একটি উপায় হল প্রয়োজনীয় ক্রস অরিজিন রিসোর্স শেয়ারিং হেডার যোগ করার জন্য একটি প্রক্সি ওয়েব পরিষেবা চালু করা। একটি টার্মিনাল আনুন এবং নিম্নরূপ একটি ডার্ট ওয়েব সার্ভার তৈরি করুন:

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

এরপরে, এই CORS প্রক্সির সুবিধা নিতে ফ্লটার কোডটি পরিবর্তন করুন, কিন্তু শুধুমাত্র যখন একটি ওয়েব ব্রাউজারের ভিতরে চলছে।

একজোড়া অভিযোজিত উইজেট

উইজেটের জোড়ার প্রথমটি হল আপনার অ্যাপ কীভাবে CORS প্রক্সি ব্যবহার করবে।

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. অভিযোজিত প্রমাণীকরণ

এই ধাপে আপনি অ্যাপটিকে ব্যবহারকারীকে প্রমাণীকরণ করার ক্ষমতা দিয়ে প্রসারিত করতে যাচ্ছেন এবং তারপর সেই ব্যবহারকারীর প্লেলিস্টগুলি দেখান। অ্যাপটি চালানো যেতে পারে এমন বিভিন্ন প্ল্যাটফর্ম কভার করতে আপনাকে একাধিক প্লাগইন ব্যবহার করতে হবে, কারণ OAuth পরিচালনা করা Android, iOS, ওয়েব, Windows, macOS এবং Linux-এর মধ্যে খুব আলাদাভাবে করা হয়।

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 প্যাকেজগুলি ব্যবহার করুন৷ দ্বিতীয় প্যাকেজ দুটি প্যাকেজের মধ্যে একটি ইন্টারপ শিম হিসাবে কাজ করে।

কোড আপডেট করুন

একটি নতুন পুনঃব্যবহারযোগ্য বিমূর্ততা, অ্যাডাপটিভলগইন উইজেট তৈরি করে আপডেটটি শুরু করুন। এই উইজেটটি আপনার পুনরায় ব্যবহার করার জন্য ডিজাইন করা হয়েছে, এবং এর জন্য কিছু কনফিগারেশন প্রয়োজন:

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) {
          if (authClient != null) {
            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) {
      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 উভয়কেই কল করে, এই পদ্ধতি রানটাইম প্ল্যাটফর্ম পরীক্ষা করে। অ্যান্ড্রয়েড, আইওএস এবং ওয়েবের জন্য, এটি _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({required this.playlistSelected, super.key});

  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,
        useMaterial3: true,
      ).toTheme,
      darkTheme: FlexColorScheme.dark(
        scheme: FlexScheme.red,
        useMaterial3: true,
      ).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 কনফিগার করা হচ্ছে

প্রমাণীকরণ কনফিগার করার প্রথম ধাপ হল আপনার পূর্বে কনফিগার করা এবং ব্যবহার করা API কী বাদ দেওয়া। আপনার API প্রকল্পের শংসাপত্র পৃষ্ঠাতে নেভিগেট করুন এবং API কী মুছুন:

GCP কনসোলে API প্রকল্পের শংসাপত্রের পৃষ্ঠা

এটি একটি পপ-আপ তৈরি করে যা আপনি মুছুন বোতাম টিপে স্বীকার করেন:

শংসাপত্র মুছুন পপআপ

তারপরে, একটি OAuth ক্লায়েন্ট আইডি তৈরি করুন:

একটি OAuth ক্লায়েন্ট আইডি তৈরি করা হচ্ছে

অ্যাপ্লিকেশন প্রকারের জন্য, ডেস্কটপ অ্যাপ নির্বাচন করুন।

ডেস্কটপ অ্যাপ অ্যাপ্লিকেশনের ধরন নির্বাচন করা হচ্ছে

নামটি গ্রহণ করুন এবং তৈরি করুন ক্লিক করুন।

ক্লায়েন্ট আইডির নামকরণ

এটি ক্লায়েন্ট আইডি এবং ক্লায়েন্ট সিক্রেট তৈরি করে যা আপনাকে googleapis_auth ফ্লো কনফিগার করতে lib/main.dart এ যোগ করতে হবে। একটি গুরুত্বপূর্ণ বাস্তবায়নের বিশদটি হল যে 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 ফাইলে এই সম্পাদনা করতে হবে না কারণ এটিতে ইতিমধ্যেই Hot Reload এবং Dart VM ডিবাগ টুলিং সক্ষম করার জন্য com.apple.security.network.server এর জন্য একটি এনটাইটেলমেন্ট রয়েছে৷

আপনার এখন উইন্ডোজ, ম্যাকওএস বা লিনাক্সে আপনার অ্যাপটি চালাতে সক্ষম হওয়া উচিত (যদি অ্যাপটি সেই লক্ষ্যগুলিতে সংকলিত হয়)।

অ্যাপটি লগ ইন করা ব্যবহারকারীর জন্য প্লেলিস্ট দেখাচ্ছে

Android এর জন্য google_sign_in কনফিগার করা হচ্ছে

আপনার API প্রকল্পের শংসাপত্রের পৃষ্ঠায় ফিরে যান এবং অন্য একটি OAuth ক্লায়েন্ট আইডি তৈরি করুন, এই সময় Android নির্বাচন করুন:

অ্যান্ড্রয়েড অ্যাপ্লিকেশনের ধরন নির্বাচন করা হচ্ছে

বাকি ফর্মের জন্য, android/app/src/main/AndroidManifest.xml এ ঘোষিত প্যাকেজের সাথে প্যাকেজের নামটি পূরণ করুন। আপনি যদি চিঠির নির্দেশাবলী অনুসরণ করে থাকেন তবে এটি com.example.adaptive_app হওয়া উচিত। Google ক্লাউড প্ল্যাটফর্ম কনসোল সহায়তা পৃষ্ঠা থেকে নির্দেশাবলী ব্যবহার করে SHA-1 শংসাপত্রের ফিঙ্গারপ্রিন্ট বের করুন:

অ্যান্ড্রয়েড ক্লায়েন্ট আইডির নামকরণ

অ্যাপটি অ্যান্ড্রয়েডে কাজ করার জন্য এটি যথেষ্ট। আপনার ব্যবহার করা Google API-এর পছন্দের উপর নির্ভর করে, আপনাকে আপনার অ্যাপ্লিকেশন বান্ডেলে জেনারেট করা JSON ফাইল যোগ করতে হতে পারে।

অ্যান্ড্রয়েডে অ্যাপটি চালানো হচ্ছে

iOS এর জন্য google_sign_in কনফিগার করা হচ্ছে

আপনার API প্রকল্পের শংসাপত্র পৃষ্ঠায় ফিরে যান এবং অন্য একটি OAuth ক্লায়েন্ট আইডি তৈরি করুন, এই সময় iOS নির্বাচন ব্যতীত:

. iOS অ্যাপ্লিকেশনের ধরন নির্বাচন করা হচ্ছে

বাকি ফর্মের জন্য, Xcode-এ ios/Runner.xcworkspace খুলে বান্ডেল আইডি পূরণ করুন। প্রজেক্ট নেভিগেটরে নেভিগেট করুন, নেভিগেটরে রানার নির্বাচন করুন, তারপর সাধারণ ট্যাব নির্বাচন করুন এবং বান্ডেল আইডেন্টিফায়ার কপি করুন। আপনি যদি এই কোডল্যাব ধাপে ধাপে অনুসরণ করে থাকেন, তাহলে এটি com.example.adaptiveApp হওয়া উচিত।

বাকি ফর্মের জন্য, বান্ডেল আইডি পূরণ করুন। Xcode-এ ios/Runner.xcworkspace খুলুন। প্রজেক্ট নেভিগেটরে নেভিগেট করুন। রানার > সাধারণ ট্যাবে যান। বান্ডেল আইডেন্টিফায়ার কপি করুন। আপনি যদি এই কোডল্যাব ধাপে ধাপে অনুসরণ করে থাকেন, তাহলে এর মান হওয়া উচিত com.example.adaptiveApp

Xcode এ বান্ডেল শনাক্তকারী কোথায় পাবেন

আপাতত অ্যাপ স্টোর আইডি এবং টিম আইডি উপেক্ষা করুন, কারণ স্থানীয় উন্নয়নের জন্য তাদের প্রয়োজন নেই:

iOS ক্লায়েন্ট আইডি নামকরণ

জেনারেট করা .plist ফাইলটি ডাউনলোড করুন, এটির নাম আপনার জেনারেট করা ক্লায়েন্ট আইডির উপর ভিত্তি করে। ডাউনলোড করা ফাইলটিকে GoogleService-Info.plist এ পুনঃনামকরণ করুন এবং তারপরে বাম হাতের নেভিগেটরে Runner/Runner এর অধীনে Info.plist ফাইলের পাশাপাশি আপনার চলমান Xcode এডিটরে টেনে আনুন। এক্সকোডে বিকল্প ডায়ালগের জন্য, প্রয়োজনে আইটেমগুলি অনুলিপি করুন নির্বাচন করুন, ফোল্ডার রেফারেন্স তৈরি করুন এবং রানার লক্ষ্যে যোগ করুন

Xcode-এ iOS অ্যাপে জেনারেট করা plist ফাইল যোগ করা হচ্ছে

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 ফাইলের এন্ট্রির সাথে মেলে মানটি সম্পাদনা করতে হবে। আপনার অ্যাপটি চালান, এবং লগ ইন করার পরে, আপনার প্লেলিস্টগুলি দেখতে হবে৷

iOS এ চলমান অ্যাপ

ওয়েবের জন্য google_sign_in কনফিগার করা হচ্ছে

আপনার API প্রকল্পের শংসাপত্র পৃষ্ঠায় ফিরে যান এবং অন্য একটি OAuth ক্লায়েন্ট আইডি তৈরি করুন, এই সময় ব্যতীত ওয়েব অ্যাপ্লিকেশন নির্বাচন করুন:

ওয়েব অ্যাপ্লিকেশন প্রকার নির্বাচন করা হচ্ছে

বাকি ফর্মের জন্য, অনুমোদিত জাভাস্ক্রিপ্টের উৎসগুলি নিম্নরূপ পূরণ করুন:

ওয়েব অ্যাপ্লিকেশন ক্লায়েন্ট আইডি নামকরণ

এটি একটি ক্লায়েন্ট আইডি তৈরি করে। 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 ওয়েব অ্যাপ চালাতে হবে।

একটি টার্মিনালে, CORS প্রক্সি সার্ভারটি নিম্নরূপ চালান:

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

অন্য টার্মিনালে, নিম্নরূপ ফ্লাটার অ্যাপ চালান:

$ 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. পরবর্তী পদক্ষেপ

অভিনন্দন!

আপনি কোডল্যাবটি সম্পূর্ণ করেছেন এবং একটি অভিযোজিত ফ্লাটার অ্যাপ তৈরি করেছেন যা ফ্লটার সমর্থন করে এমন ছয়টি প্ল্যাটফর্মে চলে। স্ক্রিনগুলি কীভাবে সাজানো হয়, কীভাবে পাঠ্যের সাথে ইন্টারঅ্যাক্ট করা হয়, কীভাবে চিত্রগুলি লোড করা হয় এবং প্রমাণীকরণ কীভাবে কাজ করে সেগুলির পার্থক্যগুলি পরিচালনা করার জন্য আপনি কোডটি মানিয়েছেন৷

আপনার অ্যাপ্লিকেশনগুলিতে আপনি মানিয়ে নিতে পারেন এমন আরও অনেক জিনিস রয়েছে। আপনার কোডকে বিভিন্ন পরিবেশে মানিয়ে নেওয়ার অতিরিক্ত উপায় শিখতে যেখানে এটি চলবে, দেখুন অভিযোজিত অ্যাপ তৈরি করা