1. مقدمه
Flutter جعبه ابزار UI گوگل برای ساخت برنامه های زیبا و بومی کامپایل شده برای موبایل، وب و دسکتاپ از یک پایگاه کد واحد است. در این لبه کد، یاد خواهید گرفت که چگونه یک برنامه Flutter بسازید که با پلتفرمی که روی آن اجرا می شود، اعم از اندروید، iOS، وب، ویندوز، macOS یا لینوکس، سازگار شود.
چیزی که یاد خواهید گرفت
- چگونه یک برنامه Flutter را که برای موبایل طراحی شده است رشد دهیم تا روی هر شش پلتفرم پشتیبانی شده توسط Flutter کار کند.
- API های مختلف Flutter برای تشخیص پلت فرم و زمان استفاده از هر API.
- انطباق با محدودیت ها و انتظارات اجرای یک برنامه در وب.
- نحوه استفاده از بسته های مختلف در کنار یکدیگر برای پشتیبانی از طیف کامل پلتفرم های فلاتر.
چیزی که خواهی ساخت
در این نرم افزار کد، ابتدا یک برنامه Flutter برای اندروید و iOS خواهید ساخت که لیست های پخش Flutter در یوتیوب را بررسی می کند. سپس با تغییر نحوه نمایش اطلاعات با توجه به اندازه پنجره برنامه، این برنامه را برای کار بر روی سه پلتفرم دسکتاپ (ویندوز، macOS و لینوکس) تطبیق خواهید داد. سپس با ایجاد متن نمایش داده شده در برنامه، همانطور که کاربران وب انتظار دارند، برنامه را برای وب تطبیق دهید. در نهایت، شما احراز هویت را به برنامه اضافه میکنید تا بتوانید لیستهای پخش خود را کاوش کنید، برخلاف مواردی که توسط تیم Flutter ایجاد شده است، که به رویکردهای متفاوتی برای احراز هویت برای Android، iOS و وب نیاز دارد، در مقابل سه پلتفرم دسکتاپ Windows. macOS و Linux.
در اینجا یک اسکرین شات از برنامه Flutter در اندروید و iOS آمده است:
این برنامه در حال اجرا در صفحه گسترده در macOS باید شبیه تصویر زیر باشد.
این آزمایشگاه کد روی تبدیل یک برنامه فلاتر موبایل به یک برنامه تطبیقی که در هر شش پلتفرم فلاتر کار می کند تمرکز دارد. مفاهیم و بلوکهای کد غیرمرتبط محو شدهاند و برای شما ارائه میشوند تا به سادگی کپی و جایگذاری کنید.
دوست دارید از این کد لبه چه چیزی یاد بگیرید؟
2. محیط توسعه Flutter خود را تنظیم کنید
برای تکمیل این آزمایشگاه به دو نرم افزار نیاز دارید - Flutter SDK و یک ویرایشگر .
شما می توانید کدلب را با استفاده از هر یک از این دستگاه ها اجرا کنید:
- یک دستگاه فیزیکی Android یا iOS که به رایانه شما متصل شده و روی حالت Developer تنظیم شده است.
- شبیه ساز iOS (نیاز به نصب ابزار Xcode دارد).
- شبیه ساز اندروید (نیاز به نصب در Android Studio دارد).
- یک مرورگر (Chrome برای اشکال زدایی لازم است).
- به عنوان یک برنامه دسکتاپ Windows ، Linux ، یا macOS . شما باید روی پلتفرمی که قصد استقرار در آن را دارید توسعه دهید. بنابراین، اگر می خواهید یک برنامه دسکتاپ ویندوز توسعه دهید، باید در ویندوز توسعه دهید تا به زنجیره ساخت مناسب دسترسی داشته باشید. الزامات خاص سیستم عامل وجود دارد که به طور مفصل در docs.flutter.dev/desktop پوشش داده شده است.
3. شروع کنید
تایید محیط توسعه شما
ساده ترین راه برای اطمینان از اینکه همه چیز برای توسعه آماده است، لطفا دستور زیر را اجرا کنید:
$ flutter doctor
اگر چیزی بدون علامت تیک نشان داده می شود، لطفاً موارد زیر را اجرا کنید تا جزئیات بیشتر در مورد اشتباه را دریافت کنید:
$ flutter doctor -v
ممکن است نیاز به نصب ابزارهای توسعه دهنده برای توسعه موبایل یا دسکتاپ داشته باشید. برای جزئیات بیشتر در مورد پیکربندی ابزار خود بسته به سیستم عامل میزبان خود، لطفاً به مستندات در مستندات نصب Flutter مراجعه کنید.
ایجاد پروژه فلاتر
یک راه آسان برای شروع نوشتن Flutter برای برنامه های دسکتاپ، استفاده از ابزار خط فرمان Flutter برای ایجاد یک پروژه Flutter است. از طرف دیگر، IDE شما ممکن است یک گردش کار برای ایجاد یک پروژه Flutter از طریق UI آن فراهم کند.
$ 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.
برای اطمینان از اینکه همه چیز کار می کند، برنامه boilerplate Flutter را به عنوان یک برنامه تلفن همراه مانند تصویر زیر اجرا کنید. از طرف دیگر، این پروژه را در 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 و داخل کروم اجرا می شود و دوباره روی macOS اجرا می شود.
نکته مهمی که در اینجا باید به آن اشاره کرد این است که در نگاه اول، Flutter هر کاری را که می تواند انجام می دهد تا محتوا را با صفحه نمایشی که در حال اجرا است تطبیق دهد. لپ تاپی که این اسکرین شات ها روی آن گرفته شده است دارای صفحه نمایش مک با وضوح بالا است، به همین دلیل است که هر دو نسخه macOS و وب برنامه با نسبت پیکسل دستگاه 2 ارائه می شوند. در همین حال، در آیفون 12، نسبت 3 را مشاهده می کنید. و 2.63 در Pixel 2. در همه موارد، متن نمایش داده شده تقریباً مشابه است، و کار ما را به عنوان توسعه دهنده بسیار آسان می کند.
دومین نکته ای که باید به آن توجه کرد این است که دو گزینه برای بررسی اینکه کد روی کدام پلتفرم اجرا می شود، مقادیر متفاوتی دارند. گزینه اول شی Platform
وارد شده از dart:io
را بررسی می کند، در حالی که گزینه دوم (فقط در روش build
ویجت موجود است)، شی Theme
را از آرگومان BuildContext
بازیابی می کند.
دلیل اینکه این دو روش نتایج متفاوتی را ارائه می دهند این است که هدف آنها متفاوت است. شی Platform
وارد شده از dart:io
برای تصمیم گیری مستقل از انتخاب های رندر استفاده می شود. یک مثال برجسته از این موضوع تصمیم گیری از کدام افزونه ها برای استفاده است، که ممکن است پیاده سازی های بومی مشابه برای یک پلت فرم فیزیکی خاص داشته باشند یا نداشته باشند.
استخراج Theme
از BuildContext
برای تصمیمگیریهای پیادهسازی است که موضوع محور هستند. نمونه بارز این موضوع تصمیم گیری در مورد استفاده از نوار لغزنده Material یا لغزنده کوپرتینویی است، همانطور که در Slider.adaptive
بحث شده است.
در بخش بعدی یک برنامه کاوشگر اصلی لیست پخش YouTube میسازید که صرفاً برای اندروید و iOS بهینه شده است. در بخشهای بعدی، انطباقهای مختلفی را اضافه میکنید تا برنامه در دسکتاپ و وب بهتر کار کند.
4. یک اپلیکیشن موبایل بسازید
بسته ها را اضافه کنید
در این برنامه از بستههای فلاتر متنوعی برای دسترسی به YouTube Data API ، مدیریت وضعیت و لمسی از موضوع استفاده خواهید کرد.
$ 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
: یک کتابخانه دارت ایجاد شده که دسترسی به APIهای Google را فراهم می کند. -
http
: کتابخانه ای برای ایجاد درخواست های HTTP که تفاوت های بین مرورگرهای بومی و وب را پنهان می کند. -
provider
: مدیریت دولتی را ارائه می دهد. -
url_launcher
: ابزاری را برای پرش به یک ویدیو از فهرست پخش فراهم می کند. همانطور که از وابستگیهای حلشده نشان داده شده است،url_launcher
علاوه بر اندروید و iOS پیشفرض، پیادهسازیهایی برای ویندوز، macOS، لینوکس و وب دارد. استفاده از این بسته به این معنی است که شما نیازی به ایجاد پلتفرم خاص برای این عملکرد ندارید. -
flex_color_scheme
: یک طرح رنگی پیشفرض زیبا به برنامه میدهد. برای کسب اطلاعات بیشتر، اسنادflex_color_scheme
API را بررسی کنید. -
go_router
: پیمایش بین صفحه های مختلف را پیاده سازی می کند. این بسته یک API راحت و مبتنی بر url برای پیمایش با استفاده از روتر Flutter ارائه میکند.
پیکربندی برنامه های تلفن همراه برای url_launcher
افزونه url_launcher
به پیکربندی برنامههای اجراکننده Android و iOS نیاز دارد. در iOS Flutter runner، خطوط زیر را به فرهنگ لغت plist
اضافه کنید.
ios/Runner/Info.plist
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
<string>http</string>
<string>tel</string>
<string>mailto</string>
</array>
در رانر Android Flutter، خطوط زیر را به 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 برای فهرست کردن لیستهای پخش، باید یک پروژه API ایجاد کنید تا کلیدهای API مورد نیاز را ایجاد کنید. در این مراحل فرض میشود که شما از قبل یک حساب Google دارید، بنابراین اگر قبلاً آن را ندارید، یک حساب کاربری ایجاد کنید.
برای ایجاد یک پروژه API به Developer Console بروید:
هنگامی که یک پروژه دارید، به صفحه کتابخانه API بروید. در کادر جستجو، «youtube» را وارد کرده و youtube data api v3 را انتخاب کنید.
در صفحه جزئیات YouTube Data API v3، API را فعال کنید.
هنگامی که API را فعال کردید، به صفحه اعتبارنامه بروید و یک کلید API ایجاد کنید.
پس از چند ثانیه، باید یک دیالوگ با کلید جدید API براق خود ببینید. به زودی از این کلید استفاده خواهید کرد.
کد اضافه کنید
در ادامه این مرحله، کدهای زیادی را برای ساختن یک اپلیکیشن موبایل، بدون هیچ توضیحی در مورد کد، برش نمی دهید. هدف این کد لبه این است که اپلیکیشن موبایل را بگیرد و آن را هم با دسکتاپ و هم با وب تطبیق دهد. برای آشنایی بیشتر با ساخت اپلیکیشنهای فلاتر برای موبایل، لطفاً اولین برنامه فلاتر را بنویسید، قسمت 1 ، قسمت 2 و ساختن رابطهای کاربری زیبا با فلاتر را ببینید.
فایل های زیر را اضافه کنید، ابتدا شیء حالت برنامه را اضافه کنید.
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 هستید. فقط یک چیز دیگر را تغییر دهید، ثابت youTubeApiKey
در خط 14 با کلید YouTube API ایجاد شده در مرحله قبل تغییر دهید.
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>
برنامه را اجرا کنید
اکنون که یک برنامه کامل دارید، باید بتوانید آن را با موفقیت در شبیه ساز اندروید یا شبیه ساز آیفون اجرا کنید. لیستی از لیست های پخش Flutter را مشاهده خواهید کرد، هنگامی که یک لیست پخش را انتخاب می کنید، ویدیوهای موجود در آن لیست پخش را خواهید دید و در نهایت اگر روی دکمه Play کلیک کنید، برای تماشای ویدیو وارد تجربه YouTube خواهید شد.
با این حال، اگر سعی کنید این برنامه را روی دسکتاپ اجرا کنید، خواهید دید که چیدمان در یک پنجره معمولی به اندازه دسکتاپ باز می شود، اشتباه می شود. در مرحله بعدی به دنبال راه هایی برای سازگاری با این موضوع خواهید بود.
5. سازگاری با دسکتاپ
مشکل دسکتاپ
اگر برنامه را روی یکی از پلتفرمهای دسکتاپ بومی، ویندوز، macOS یا لینوکس اجرا کنید، متوجه مشکل جالبی خواهید شد. این کار می کند، اما به نظر می رسد ... عجیب و غریب.
یک راه حل برای این، اضافه کردن یک نمای تقسیم شده، لیست کردن لیست های پخش در سمت چپ و ویدیوها در سمت راست است. با این حال، شما میخواهید این طرحبندی تنها زمانی فعال شود که کد در اندروید یا 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,
);
}
}
سپس، فایل مربوط به ویجت 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
یک آرگومان پاسخ به تماس را نشان می دهد. این تماس به کد اطراف اطلاع می دهد که کاربر یک لیست پخش را انتخاب کرده است. سپس کد باید کار را برای نمایش آن لیست پخش انجام دهد. این نیاز به Scaffold
در ویجتهای Playlists
و PlaylistDetails
تغییر میدهد. اکنون که آنها در سطح بالایی نیستند، باید 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 callback، و حذف ویجت Scaffold
، ویجت _PlaylistsListView
از حالت بدون حالت به حالت حالت تبدیل می شود. این تغییر به دلیل معرفی یک ScrollController
که باید ساخته و نابود شود، ضروری است.
معرفی ScrollController
جالب است زیرا لازم است زیرا در یک طرح بندی گسترده شما دو ویجت ListView
در کنار هم دارید. در تلفن همراه، داشتن یک ListView
منفرد سنتی است، و بنابراین میتوان یک ScrollController با عمر طولانی وجود داشت که همه ListView
در طول چرخه زندگی فردی خود به آن متصل میشوند و از آن جدا میشوند. دسکتاپ متفاوت است، در دنیایی که چندین ListView
در کنار هم معنی دارند.
و در نهایت فایل lib/src/playlist_details.dart
را به صورت زیر ویرایش کنید:
lib/src/playlist_details.dart
import 'package:flutter/material.dart';
import 'package:googleapis/youtube/v3.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/link.dart';
import 'app_state.dart';
class PlaylistDetails extends StatelessWidget {
const PlaylistDetails(
{required this.playlistId, required this.playlistName, super.key});
final String playlistId;
final String playlistName;
@override
Widget build(BuildContext context) {
return Consumer<FlutterDevPlaylists>(
builder: (context, playlists, _) {
final playlistItems = playlists.playlistItems(playlistId: playlistId);
if (playlistItems.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return _PlaylistDetailsListView(playlistItems: playlistItems);
},
);
}
}
class _PlaylistDetailsListView extends StatefulWidget {
const _PlaylistDetailsListView({required this.playlistItems});
final List<PlaylistItem> playlistItems;
@override
State<_PlaylistDetailsListView> createState() =>
_PlaylistDetailsListViewState();
}
class _PlaylistDetailsListViewState extends State<_PlaylistDetailsListView> {
late ScrollController _scrollController;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _scrollController,
itemCount: widget.playlistItems.length,
itemBuilder: (context, index) {
final playlistItem = widget.playlistItems[index];
return Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Stack(
alignment: Alignment.center,
children: [
if (playlistItem.snippet!.thumbnails!.high != null)
Image.network(playlistItem.snippet!.thumbnails!.high!.url!),
_buildGradient(context),
_buildTitleAndSubtitle(context, playlistItem),
_buildPlayButton(context, playlistItem),
],
),
),
);
},
);
}
Widget _buildGradient(BuildContext context) {
return Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.transparent,
Theme.of(context).colorScheme.surface,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: const [0.5, 0.95],
),
),
),
);
}
Widget _buildTitleAndSubtitle(
BuildContext context, PlaylistItem playlistItem) {
return Positioned(
left: 20,
right: 0,
bottom: 20,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
playlistItem.snippet!.title!,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontSize: 18,
// fontWeight: FontWeight.bold,
),
),
if (playlistItem.snippet!.videoOwnerChannelTitle != null)
Text(
playlistItem.snippet!.videoOwnerChannelTitle!,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 12,
),
),
],
),
);
}
Widget _buildPlayButton(BuildContext context, PlaylistItem playlistItem) {
return Stack(
alignment: AlignmentDirectional.center,
children: [
Container(
width: 42,
height: 42,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(
Radius.circular(21),
),
),
),
Link(
uri: Uri.parse(
'https://www.youtube.com/watch?v=${playlistItem.snippet!.resourceId!.videoId}'),
builder: (context, followLink) => IconButton(
onPressed: followLink,
color: Colors.red,
icon: const Icon(Icons.play_circle_fill),
iconSize: 45,
),
),
],
);
}
}
مشابه ویجت Playlists
در بالا، این فایل همچنین دارای تغییراتی برای حذف ویجت Scaffold
و معرفی یک ScrollController
متعلق به خود است.
دوباره برنامه را اجرا کنید!
اجرای برنامه بر روی دسکتاپ انتخابی شما، خواه ویندوز، macOS یا لینوکس باشد. اکنون باید همانطور که انتظار دارید کار کند.
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) ════════════════════════════════════════════════════════════════════════════════════════════════════
ایجاد یک پروکسی CORS
یکی از راههای مقابله با مشکلات رندر تصویر، معرفی یک وب سرویس پروکسی برای اضافه کردن هدرهای مورد نیاز Cross Origin Resource Sharing است. یک ترمینال بیاورید و یک وب سرور دارت به شرح زیر ایجاد کنید:
$ dart create --template server-shelf yt_cors_proxy Creating yt_cors_proxy using template server-shelf... .gitignore analysis_options.yaml CHANGELOG.md pubspec.yaml README.md Dockerfile .dockerignore test/server_test.dart bin/server.dart Running pub get... 3.9s Resolving dependencies... Changed 53 dependencies! Created project yt_cors_proxy in yt_cors_proxy! In order to get started, run the following commands: cd yt_cors_proxy dart run bin/server.dart
دایرکتوری را به سرور yt_cors_proxy
تغییر دهید و چند وابستگی مورد نیاز را اضافه کنید:
$ cd yt_cors_proxy $ dart pub add shelf_cors_headers http "http" was found in dev_dependencies. Removing "http" and adding it to dependencies instead. Resolving dependencies... http 1.1.2 (from dev dependency to direct dependency) js 0.6.7 (0.7.0 available) lints 2.1.1 (3.0.0 available) + shelf_cors_headers 0.1.5 Changed 2 dependencies! 2 packages have newer versions incompatible with dependency constraints. Try `dart pub outdated` for more information.
برخی از وابستگی های فعلی وجود دارند که دیگر مورد نیاز نیستند. اینها را به صورت زیر برش دهید:
$ dart pub remove args shelf_router Resolving dependencies... args 2.4.2 (from direct dependency to transitive dependency) js 0.6.7 (0.7.0 available) lints 2.1.1 (3.0.0 available) These packages are no longer being depended on: - http_methods 1.1.1 - shelf_router 1.1.4 Changed 3 dependencies! 2 packages have newer versions incompatible with dependency constraints. Try `dart pub outdated` for more information.
در مرحله بعد، محتویات فایل server.dart را برای مطابقت با موارد زیر تغییر دهید:
yt_cors_proxy/bin/server.dart
import 'dart:async';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_cors_headers/shelf_cors_headers.dart';
Future<Response> _requestHandler(Request req) async {
final target = req.url.replace(scheme: 'https', host: 'i.ytimg.com');
final response = await http.get(target);
return Response.ok(response.bodyBytes, headers: response.headers);
}
void main(List<String> args) async {
// Use any available host or container IP (usually `0.0.0.0`).
final ip = InternetAddress.anyIPv4;
// Configure a pipeline that adds CORS headers and proxies requests.
final handler = Pipeline()
.addMiddleware(logRequests())
.addMiddleware(corsHeaders(headers: {ACCESS_CONTROL_ALLOW_ORIGIN: '*'}))
.addHandler(_requestHandler);
// For running in containers, we respect the PORT environment variable.
final port = int.parse(Platform.environment['PORT'] ?? '8080');
final server = await serve(handler, ip, port);
print('Server listening on port ${server.port}');
}
شما می توانید این سرور را به صورت زیر اجرا کنید:
$ dart run bin/server.dart Server listening on port 8080
همچنین، میتوانید آن را بهعنوان یک تصویر Docker بسازید و تصویر Docker حاصل را به صورت زیر اجرا کنید:
$ docker build . -t yt-cors-proxy [+] Building 2.7s (14/14) FINISHED $ docker run -p 8080:8080 yt-cors-proxy Server listening on port 8080
در مرحله بعد، کد Flutter را تغییر دهید تا از این پروکسی 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
را همانطور که بودند رها کردید. این عمدی بود، زیرا، اگر ویجتهای Text را تطبیق دهید، وقتی کاربر روی متن ضربه میزند، قابلیت onTap
ListTile
مسدود میشود.
برنامه را در وب به درستی اجرا کنید
با اجرای پروکسی CORS، باید بتوانید نسخه وب برنامه را اجرا کنید و چیزی شبیه به زیر داشته باشید:
7. احراز هویت تطبیقی
در این مرحله قصد دارید اپلیکیشن را با دادن قابلیت احراز هویت کاربر گسترش دهید و سپس لیست های پخش آن کاربر را نشان دهید. شما مجبور خواهید بود از چندین پلاگین برای پوشش پلتفرمهای مختلف استفاده کنید، زیرا مدیریت OAuth بین اندروید، iOS، وب، ویندوز، macOS و لینوکس بسیار متفاوت است.
افزودن پلاگین برای فعال کردن احراز هویت گوگل
شما قصد دارید سه بسته را برای کنترل احراز هویت گوگل نصب کنید.
$ 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
استفاده کنید. بسته دوم به عنوان یک شیم interop بین دو بسته عمل می کند.
کد را به روز کنید
با ایجاد یک انتزاع قابل استفاده مجدد، ویجت AdaptiveLogin، به روز رسانی را شروع کنید. این ویجت برای استفاده مجدد شما طراحی شده است و به همین دلیل نیاز به پیکربندی دارد:
lib/src/adaptive_login.dart
import 'dart:io' show Platform;
import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:googleapis_auth/auth_io.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/link.dart';
import 'app_state.dart';
typedef _AdaptiveLoginButtonWidget = Widget Function({
required VoidCallback? onPressed,
});
class AdaptiveLogin extends StatelessWidget {
const AdaptiveLogin({
super.key,
required this.clientId,
required this.scopes,
required this.loginButtonChild,
});
final ClientId clientId;
final List<String> scopes;
final Widget loginButtonChild;
@override
Widget build(BuildContext context) {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) {
return _GoogleSignInLogin(
button: _loginButton,
scopes: scopes,
);
} else {
return _GoogleApisAuthLogin(
button: _loginButton,
scopes: scopes,
clientId: clientId,
);
}
}
Widget _loginButton({required VoidCallback? onPressed}) => ElevatedButton(
onPressed: onPressed,
child: loginButtonChild,
);
}
class _GoogleSignInLogin extends StatefulWidget {
const _GoogleSignInLogin({
required this.button,
required this.scopes,
});
final _AdaptiveLoginButtonWidget button;
final List<String> scopes;
@override
State<_GoogleSignInLogin> createState() => _GoogleSignInLoginState();
}
class _GoogleSignInLoginState extends State<_GoogleSignInLogin> {
@override
initState() {
super.initState();
_googleSignIn = GoogleSignIn(
scopes: widget.scopes,
);
_googleSignIn.onCurrentUserChanged.listen((account) {
if (account != null) {
_googleSignIn.authenticatedClient().then((authClient) {
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(),
),
);
}
}
این فایل کارهای زیادی انجام می دهد. روش build
AdaptiveLogin
کارهای سنگین را انجام می دهد. این روش با فراخوانی Platform.isXXX
هر دو kIsWeb
و dart:io
، پلت فرم زمان اجرا را بررسی می کند. برای Android، iOS، و وب، ویجت حالتدهنده _GoogleSignInLogin
را نمونهسازی میکند. برای ویندوز، macOS، و لینوکس، یک ویجت حالتی _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,
);
},
);
}
}
در نهایت، فایل main.dart
را برای استفاده صحیح از ویجت جدید AdaptiveLogin
به روز کنید:
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,
);
}
}
تغییرات در این فایل منعکس کننده تغییر از نمایش فقط لیست های پخش YouTube Flutter به نمایش لیست های پخش تأیید شده کاربر است. در حالی که کد اکنون کامل است، هنوز یک سری تغییرات در این فایل و فایلهای تحت برنامههای Runner مربوطه لازم است تا بستههای google_sign_in
و googleapis_auth
را برای احراز هویت به درستی پیکربندی کنند.
این برنامه اکنون لیست های پخش YouTube را از کاربر تأیید شده نشان می دهد. با تکمیل ویژگی ها، باید احراز هویت را فعال کنید. برای انجام این کار، بسته های google_sign_in
و googleapis_auth
را پیکربندی کنید. برای پیکربندی بسته ها، باید فایل main.dart
و فایل های برنامه های Runner را تغییر دهید.
در حال پیکربندی googleapis_auth
اولین قدم برای پیکربندی احراز هویت، حذف کلید API است که قبلاً پیکربندی و استفاده کردهاید. به صفحه اعتبار پروژه API خود بروید و کلید API را حذف کنید:
این یک پاپ آپ ایجاد می کند که با زدن دکمه Delete آن را تأیید می کنید:
سپس، یک شناسه مشتری OAuth ایجاد کنید:
برای نوع برنامه، برنامه دسکتاپ را انتخاب کنید.
نام را بپذیرید و روی ایجاد کلیک کنید.
با این کار، Client ID و Client Secret ایجاد می شود که باید به lib/main.dart
اضافه کنید تا جریان googleapis_auth
را پیکربندی کنید. یک جزئیات پیاده سازی مهم این است که جریان googleapis_auth از یک وب سرور موقت در حال اجرا در localhost برای گرفتن توکن 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
برای فعال کردن Hot Reload و ابزار اشکال زدایی Dart VM است.
اکنون باید بتوانید برنامه خود را روی ویندوز، macOS یا لینوکس اجرا کنید (اگر برنامه روی آن اهداف کامپایل شده باشد).
در حال پیکربندی google_sign_in
برای Android
به صفحه اعتبار پروژه API خود بازگردید و شناسه مشتری OAuth دیگری ایجاد کنید، به جز این بار Android را انتخاب کنید:
برای بقیه فرم، نام بسته را با بسته اعلام شده در android/app/src/main/AndroidManifest.xml
پر کنید. اگر دستورالعملهای مربوط به نامه را دنبال کردهاید، باید com.example.adaptive_app
باشد. اثر انگشت گواهی SHA-1 را با استفاده از دستورالعملهای صفحه راهنمای کنسول Google Cloud Platform استخراج کنید:
این کافی است تا برنامه روی اندروید کار کند. بسته به انتخاب Google API هایی که استفاده می کنید، ممکن است لازم باشد فایل JSON تولید شده را به بسته نرم افزاری خود اضافه کنید.
در حال پیکربندی google_sign_in
برای iOS
به صفحه اعتبار پروژه API خود بازگردید و شناسه مشتری OAuth دیگری ایجاد کنید، به جز این بار iOS را انتخاب کنید:
.
برای بقیه فرم، ID Bundle را با باز کردن ios/Runner.xcworkspace
در Xcode پر کنید. به Project Navigator بروید، Runner را در Navigator انتخاب کنید، سپس زبانه General را انتخاب کنید و Bundle Identifier را کپی کنید. اگر گام به گام این لبه کد را دنبال کرده اید، باید com.example.adaptiveApp
باشد.
برای بقیه فرم، ID Bundle را پر کنید. ios/Runner.xcworkspace
را در Xcode باز کنید. به Project Navigator بروید. به سربرگ Runner > General بروید. شناسه Bundle را کپی کنید. اگر گام به گام این لبه کد را دنبال کرده اید، مقدار آن باید com.example.adaptiveApp
باشد.
فعلاً شناسه App Store و Team ID را نادیده بگیرید، زیرا برای توسعه محلی مورد نیاز نیستند:
فایل .plist
تولید شده را دانلود کنید، نام آن بر اساس شناسه مشتری تولید شده شما است. نام فایل دانلود شده را به GoogleService-Info.plist
تغییر دهید، و سپس آن را به ویرایشگر Xcode در حال اجرا خود بکشید، در کنار فایل Info.plist
در زیر Runner/Runner
در ناوبری سمت چپ. برای گفتگوی گزینهها در Xcode، در صورت نیاز Copy items ، Create folder references و Add to Runner target را انتخاب کنید.
از 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
برای وب
به صفحه اعتبار پروژه API خود بازگردید و شناسه مشتری OAuth دیگری ایجاد کنید، به جز اینکه این بار برنامه وب را انتخاب کنید:
برای بقیه فرم، مبداهای مجاز جاوا اسکریپت را به صورت زیر پر کنید:
این یک شناسه مشتری تولید می کند. meta
تگ زیر را به web/index.html
اضافه کنید، بهروزرسانی شده تا شامل شناسه مشتری ایجاد شده باشد:
web/index.html
<meta name="google-signin-client_id" content="YOUR_GOOGLE_SIGN_IN_OAUTH_CLIENT_ID.apps.googleusercontent.com">
اجرای این نمونه نیاز به کمی نگه داشتن دست دارد. شما باید پروکسی CORS را که در مرحله قبل ایجاد کردید اجرا کنید، و باید برنامه وب Flutter را در پورت مشخص شده در فرم وب اپلیکیشن OAuth Client ID با استفاده از دستورالعمل های زیر اجرا کنید.
در یک ترمینال، سرور پروکسی CORS را به صورت زیر اجرا کنید:
$ 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. مراحل بعدی
تبریک می گویم!
شما نرم افزار Codelab را تکمیل کرده اید و یک برنامه تطبیقی Flutter ساخته اید که بر روی هر شش پلت فرمی که Flutter پشتیبانی می کند اجرا می شود. شما کد را برای رسیدگی به تفاوتها در نحوه چیدمان صفحهها، نحوه تعامل با متن، نحوه بارگیری تصاویر و نحوه عملکرد احراز هویت تطبیق دادید.
چیزهای زیادی وجود دارد که می توانید در برنامه های خود تطبیق دهید. برای یادگیری روشهای اضافی برای تطبیق کد خود با محیطهای مختلف که در آن اجرا میشود، به ساخت برنامههای تطبیقی مراجعه کنید.