1. Giriş
Flutter, Google'ın kullanıcı arayüzü araç setidir. Tek bir kod tabanından mobil, web ve masaüstü için etkileyici, yerel olarak derlenmiş uygulamalar oluşturmaya yarar. Bu codelab'de, Android, iOS, web, Windows, macOS veya Linux gibi üzerinde çalıştığı platforma uyum sağlayan bir Flutter uygulaması oluşturmayı öğreneceksiniz.
Öğrenecekleriniz
- Mobil için tasarlanmış bir Flutter uygulamasını, Flutter'ın desteklediği altı platformda da çalışacak şekilde büyütme
- Platform algılama için farklı Flutter API'leri ve her API'nin ne zaman kullanılacağı.
- Uygulamayı web'de çalıştırmanın kısıtlamalarına ve beklentilerine uyum sağlama
- Flutter'ın tüm platformlarını desteklemek için farklı paketleri birlikte kullanma
Ne oluşturacaksınız?
Bu codelab'de, başlangıçta Flutter'ın YouTube oynatma listelerini keşfeden bir Android ve iOS Flutter uygulaması oluşturacaksınız. Ardından, uygulama penceresinin boyutuna göre bilgilerin görüntülenme şeklini değiştirerek bu uygulamayı üç masaüstü platformunda (Windows, macOS ve Linux) çalışacak şekilde uyarlayacaksınız. Ardından, uygulamada gösterilen metni web kullanıcılarının beklentisi doğrultusunda seçilebilir hale getirerek uygulamayı web için uyarlarsınız. Son olarak, uygulamaya kimlik doğrulama ekleyeceksiniz. Böylece, Flutter ekibi tarafından oluşturulanlar yerine kendi oynatma listelerinizi keşfedebileceksiniz. Bu, Android, iOS ve web için kimlik doğrulamaya yönelik farklı yaklaşımlar gerektirir. Windows, macOS ve Linux olmak üzere üç masaüstü platformu için ise farklı bir yaklaşım gerekir.
Android ve iOS'te Flutter uygulamasının ekran görüntüsünü aşağıda bulabilirsiniz:
macOS'te geniş ekranda çalışan bu uygulama, aşağıdaki ekran görüntüsüne benzemelidir.
Bu codelab, mobil Flutter uygulamasını altı Flutter platformunda da çalışan uyarlanabilir bir uygulamaya dönüştürmeye odaklanmaktadır. Alakalı olmayan kavramlar ve kod blokları işaretlenmiştir ve kopyalayıp yapıştırmanız için kullanımınıza sunulmuştur.
Bu codelab'den ne öğrenmek istiyorsunuz?
2. Flutter geliştirme ortamınızı kurma
Bu laboratuvarı tamamlamak için iki yazılıma ihtiyacınız var: Flutter SDK ve bir düzenleyici.
Codelab'i aşağıdaki cihazlardan herhangi birini kullanarak çalıştırabilirsiniz:
- Bilgisayarınıza bağlı ve Geliştirici moduna ayarlanmış fiziksel bir Android veya iOS cihaz.
- iOS simülatörü (Xcode araçlarının yüklenmesi gerekir).
- Android Emulator (Android Studio'da kurulum gerektirir).
- Tarayıcı (hata ayıklama için Chrome gereklidir).
- Windows, Linux veya macOS masaüstü uygulaması olarak. Dağıtmayı planladığınız platformda geliştirme yapmanız gerekir. Bu nedenle, bir Windows masaüstü uygulaması geliştirmek istiyorsanız uygun derleme zincirine erişmek için Windows'ta geliştirme yapmanız gerekir. docs.flutter.dev/desktop adresinde ayrıntılı olarak açıklanan işletim sistemine özgü gereksinimler vardır.
3. Başlayın
Geliştirme ortamınızı onaylama
Geliştirmeye başlamadan önce her şeyin hazır olduğundan emin olmanın en kolay yolu aşağıdaki komutu çalıştırmaktır:
flutter doctor
Onay işareti olmadan gösterilen bir şey varsa neyin yanlış olduğuna dair daha fazla ayrıntı almak için aşağıdakileri çalıştırın:
flutter doctor -v
Mobil veya masaüstü geliştirme için geliştirici araçlarını yüklemeniz gerekebilir. Araçlarınızı ana makine işletim sisteminize göre yapılandırma hakkında daha fazla bilgi için Flutter yükleme belgelerindeki dokümanları inceleyin.
Flutter projesi oluşturma
Masaüstü uygulamaları için Flutter yazmaya başlamanın bir yolu, Flutter projesi oluşturmak üzere Flutter komut satırı aracını kullanmaktır. Alternatif olarak, IDE'niz kullanıcı arayüzü üzerinden Flutter projesi oluşturma iş akışı sağlayabilir.
$ 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.
Her şeyin çalıştığından emin olmak için aşağıdaki şekilde gösterildiği gibi standart Flutter uygulamasını mobil uygulama olarak çalıştırın. Alternatif olarak, bu projeyi IDE'nizde açıp uygulamayı çalıştırmak için IDE'nin araçlarını kullanabilirsiniz. Önceki adım sayesinde, masaüstü uygulaması olarak çalıştırma tek seçenek olmalıdır.
$ 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=/
Uygulamanın çalıştığını görmeniz gerekir. İçeriğin güncellenmesi gerekiyor.
İçeriği güncellemek için lib/main.dart
içindeki kodunuzu aşağıdaki kodla güncelleyin. Uygulamanızın gösterdiği bilgileri değiştirmek için hızlı yeniden yükleme işlemi yapın.
- Uygulamayı komut satırını kullanarak çalıştırıyorsanız anında yeniden yüklemek için konsola
r
yazın. - Uygulamayı bir IDE kullanarak çalıştırırsanız dosyayı kaydettiğinizde uygulama yeniden yüklenir.
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';
}
}
}
Uygulama, farklı platformların nasıl algılanıp uyarlanabileceği konusunda fikir edinmenizi sağlamak için tasarlanmıştır. Uygulamanın Android ve iOS'te yerel olarak çalıştığı durum:
Aynı kodun macOS'te ve Chrome'da yerel olarak çalıştırıldığı bir örnek de aşağıda verilmiştir. Chrome da macOS'te çalışmaktadır.
Burada dikkat edilmesi gereken önemli nokta, Flutter'ın ilk bakışta içeriği üzerinde çalıştığı ekrana uyarlamak için elinden geleni yapmasıdır. Bu ekran görüntülerinin alındığı dizüstü bilgisayarda yüksek çözünürlüklü bir Mac ekranı bulunuyor. Bu nedenle, uygulamanın hem macOS hem de web sürümleri 2 cihaz piksel oranıyla oluşturuluyor. Bu sırada iPhone 12'de 3, Pixel 2'de ise 2,63 oranını görüyorsunuz. Her durumda gösterilen metin yaklaşık olarak aynıdır. Bu da geliştiriciler olarak işimizi çok daha kolay hale getirir.
Dikkat edilmesi gereken ikinci nokta, kodun hangi platformda çalıştığını kontrol etme seçeneklerinin farklı değerler vermesidir. İlk seçenek, dart:io
öğesinden içe aktarılan Platform
nesnesini incelerken ikinci seçenek (yalnızca widget'ın build
yöntemi içinde kullanılabilir) Theme
nesnesini BuildContext
bağımsız değişkeninden alır.
Bu iki yöntemin farklı sonuçlar döndürmesinin nedeni, amaçlarının farklı olmasıdır. dart:io
kaynağından içe aktarılan Platform
nesnesi, oluşturma tercihlerinden bağımsız kararlar vermek için kullanılır. Bunun en iyi örneği, belirli bir fiziksel platform için yerel uygulamalarla eşleşen veya eşleşmeyen eklentilerin hangilerinin kullanılacağına karar vermektir.
Theme
öğesini BuildContext
öğesinden çıkarma işlemi, tema odaklı uygulama kararları için tasarlanmıştır. Bunun en iyi örneklerinden biri, Slider.adaptive
bölümünde ele alındığı gibi Material kaydırma çubuğunu mu yoksa Cupertino kaydırma çubuğunu mu kullanacağınıza karar vermektir.
Bir sonraki bölümde, yalnızca Android ve iOS için optimize edilmiş temel bir YouTube oynatma listesi gezgini uygulaması oluşturacaksınız. Aşağıdaki bölümlerde, uygulamanın masaüstünde ve web'de daha iyi çalışmasını sağlamak için çeşitli uyarlamalar ekleyeceksiniz.
4. Mobil uygulama oluşturma
Paket ekleme
Bu uygulamada, YouTube Data API'ye, durum yönetimine ve temalandırmaya erişmek için çeşitli Flutter paketleri kullanacaksınız.
$ flutter pub add googleapis http provider url_launcher flex_color_scheme go_router Resolving dependencies... Downloading packages... + _discoveryapis_commons 1.0.7 characters 1.4.0 (1.4.1 available) + flex_color_scheme 8.3.0 + flex_seed_scheme 3.5.1 > flutter_lints 6.0.0 (was 5.0.0) + flutter_web_plugins 0.0.0 from sdk flutter + go_router 16.2.0 + googleapis 14.0.0 + http 1.5.0 + http_parser 4.1.2 > lints 6.0.0 (was 5.1.1) + logging 1.3.0 material_color_utilities 0.11.1 (0.13.0 available) meta 1.16.0 (1.17.0 available) + nested 1.0.0 + plugin_platform_interface 2.1.8 + provider 6.1.5 test_api 0.7.6 (0.7.7 available) + typed_data 1.4.0 + url_launcher 6.3.2 + url_launcher_android 6.3.17 + url_launcher_ios 6.3.4 + url_launcher_linux 3.2.1 + url_launcher_macos 3.2.3 + url_launcher_platform_interface 2.3.2 + url_launcher_web 2.4.1 + url_launcher_windows 3.1.4 + web 1.1.1 Changed 24 dependencies! 4 packages have newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information.
Bu komut, uygulamaya bir dizi paket ekler:
googleapis
: Google API'lerine erişim sağlayan, oluşturulmuş bir Dart kitaplığı.http
: Yerel ve web tarayıcıları arasındaki farkları gizleyen HTTP istekleri oluşturmaya yönelik bir kitaplık.provider
: Durum yönetimi sağlar.url_launcher
: Oynatma listesindeki bir videoya geçmenizi sağlar. Çözülen bağımlılıklardan da görüleceği gibi,url_launcher
, varsayılan Android ve iOS'e ek olarak Windows, macOS, Linux ve web için uygulamalara sahiptir. Bu paketi kullandığınızda, bu işlev için platforma özel bir kod oluşturmanız gerekmez.flex_color_scheme
: Uygulamaya güzel bir varsayılan renk şeması verir. Daha fazla bilgi edinmek içinflex_color_scheme
API belgelerini inceleyin.go_router
: Farklı ekranlar arasında gezinmeyi sağlar. Bu paket, Flutter'ın yönlendiricisini kullanarak gezinmek için URL tabanlı kullanışlı bir API sağlar.
url_launcher
için mobil uygulamaları yapılandırma
url_launcher
eklentisi için Android ve iOS çalıştırıcı uygulamalarının yapılandırılması gerekir. iOS Flutter çalıştırıcısında, plist
sözlüğüne aşağıdaki satırları ekleyin.
ios/Runner/Info.plist
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
<string>http</string>
<string>tel</string>
<string>mailto</string>
</array>
Android Flutter çalıştırıcısında Manifest.xml
bölümüne aşağıdaki satırları ekleyin. Bu queries
düğümünü, manifest
düğümünün doğrudan alt öğesi ve application
düğümünün eşi olarak ekleyin.
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>
Bu zorunlu yapılandırma değişiklikleri hakkında daha fazla bilgi için url_launcher
belgelerine bakın.
YouTube Data API'ye erişme
Oynatma listelerini listelemek için YouTube Data API'ye erişmek üzere gerekli API anahtarlarını oluşturmak için bir API projesi oluşturmanız gerekir. Bu adımlarda, Google Hesabınızın olduğu varsayılır. Hesabınız yoksa oluşturun.
API projesi oluşturmak için Developer Console'a gidin:
Projeniz olduğunda API Kitaplığı sayfasına gidin. Arama kutusuna "youtube" yazın ve youtube data api v3'ü seçin.
YouTube Data API v3 ayrıntılar sayfasında API'yi etkinleştirin.
API'yi etkinleştirdikten sonra Kimlik bilgileri sayfasına gidin ve bir API anahtarı oluşturun.
Birkaç saniye sonra, yeni API anahtarınızın bulunduğu bir iletişim kutusu görürsünüz. Bu anahtarı kısa süre içinde kullanacaksınız.
Kod ekle
Bu adımın geri kalanında, kodla ilgili herhangi bir açıklama yapmadan mobil uygulama oluşturmak için çok sayıda kodu kesip yapıştıracaksınız. Bu kod laboratuvarının amacı, mobil uygulamayı alıp hem masaüstü hem de web için uyarlamaktır. Mobil için Flutter uygulamaları oluşturma hakkında daha ayrıntılı bir giriş için İlk Flutter uygulamanız başlıklı makaleyi inceleyin.
Öncelikle uygulama için durum nesnesi olmak üzere aşağıdaki dosyaları ekleyin.
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));
}
}
Ardından, oynatma listesi ayrıntıları sayfasını ekleyin.
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,
),
),
],
);
}
}
Ardından, oynatma listelerinin listesini ekleyin.
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
dosyasının içeriğini aşağıdaki gibi değiştirin:
lib/main.dart
import 'dart:io';
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'src/app_state.dart';
import 'src/playlist_details.dart';
import 'src/playlists.dart';
// From https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw
const flutterDevAccountId = 'UCwXdFgeE9KYzlDdR7TG9cMw';
// TODO: Replace with your YouTube API Key
const youTubeApiKey = 'AIzaNotAnApiKey';
final _router = GoRouter(
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (context, state) {
return const Playlists();
},
routes: <RouteBase>[
GoRoute(
path: 'playlist/:id',
builder: (context, state) {
final title = state.uri.queryParameters['title']!;
final id = state.pathParameters['id']!;
return PlaylistDetails(playlistId: id, playlistName: title);
},
),
],
),
],
);
void main() {
if (youTubeApiKey == 'AIzaNotAnApiKey') {
print('youTubeApiKey has not been configured.');
exit(1);
}
runApp(
ChangeNotifierProvider<FlutterDevPlaylists>(
create: (context) => FlutterDevPlaylists(
flutterDevAccountId: flutterDevAccountId,
youTubeApiKey: youTubeApiKey,
),
child: const PlaylistsApp(),
),
);
}
class PlaylistsApp extends StatelessWidget {
const PlaylistsApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'FlutterDev Playlists',
theme: FlexColorScheme.light(scheme: FlexScheme.red).toTheme,
darkTheme: FlexColorScheme.dark(scheme: FlexScheme.red).toTheme,
themeMode: ThemeMode.dark, // Or ThemeMode.System if you'd prefer
debugShowCheckedModeBanner: false,
routerConfig: _router,
);
}
}
Bu kodu Android ve iOS'te çalıştırmaya neredeyse hazırsınız. Değiştirmeniz gereken son bir şey kaldı. youTubeApiKey
sabitini, önceki adımda oluşturulan YouTube API anahtarıyla değiştirin.
lib/main.dart
// TODO: Replace with your YouTube API Key
const youTubeApiKey = 'AIzaNotAnApiKey';
Bu uygulamayı macOS'te çalıştırmak için uygulamanın HTTP istekleri yapmasına izin vermeniz gerekir. Hem DebugProfile.entitlements
hem de Release.entitilements
dosyalarını aşağıdaki şekilde düzenleyin:
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>
Uygulamayı çalıştırma
Artık eksiksiz bir uygulamanız olduğuna göre, uygulamayı Android emülatöründe veya iPhone simülatöründe başarıyla çalıştırabilirsiniz. Flutter'ın oynatma listelerinin bir listesini görürsünüz. Bir oynatma listesini seçtiğinizde bu oynatma listesindeki videoları görürsünüz. Son olarak, Oynat düğmesini tıkladığınızda videoyu izlemek için YouTube deneyimine yönlendirilirsiniz.
Ancak bu uygulamayı masaüstünde çalıştırmaya çalıştığınızda, normal masaüstü boyutlu bir pencereye genişletildiğinde düzenin yanlış olduğunu görürsünüz. Bir sonraki adımda buna nasıl uyum sağlayacağınızı öğreneceksiniz.
5. Masaüstüne uyum sağlama
Masaüstü sorunu
Uygulamayı yerel masaüstü platformlarından birinde (Windows, macOS veya Linux) çalıştırırsanız ilginç bir sorunla karşılaşırsınız. Çalışıyor ancak ... tuhaf görünüyor.
Bu sorunu düzeltmek için oynatma listelerini solda, videoları sağda listeleyen bir bölünmüş görünüm ekleyebilirsiniz. Ancak bu düzenin yalnızca kod Android veya iOS'te çalışmadığında ve pencere yeterince geniş olduğunda etkinleşmesini istiyorsunuz. Aşağıdaki talimatlarda bu özelliğin nasıl uygulanacağı gösterilmektedir.
Öncelikle, düzenin oluşturulmasına yardımcı olması için split_view
paketini ekleyin.
$ flutter pub add split_view Resolving dependencies... Downloading packages... characters 1.4.0 (1.4.1 available) material_color_utilities 0.11.1 (0.13.0 available) meta 1.16.0 (1.17.0 available) + split_view 3.2.1 test_api 0.7.6 (0.7.7 available) Changed 1 dependency! 4 packages have newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information.
Uyarlanabilir widget'ları kullanıma sunma
Bu codelab'de kullanacağınız kalıp, ekran genişliği ve platform teması gibi özelliklere göre uygulama seçenekleri sunan uyarlanabilir widget'ları tanıtmak olacaktır. Bu durumda, AdaptivePlaylists
ve Playlists
öğelerinin etkileşim şeklini yeniden düzenleyen bir PlaylistDetails
widget'ı tanıtacaksınız. lib/main.dart
dosyasını aşağıdaki gibi düzenleyin:
lib/main.dart
import 'dart:io';
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'src/adaptive_playlists.dart'; // Add this import
import 'src/app_state.dart';
import 'src/playlist_details.dart';
// Remove the src/playlists.dart import
// From https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw
const flutterDevAccountId = 'UCwXdFgeE9KYzlDdR7TG9cMw';
// TODO: Replace with your YouTube API Key
const youTubeApiKey = 'AIzaNotAnApiKey';
final _router = GoRouter(
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (context, state) {
return const AdaptivePlaylists(); // Modify this line
},
routes: <RouteBase>[
GoRoute(
path: 'playlist/:id',
builder: (context, state) {
final title = state.uri.queryParameters['title']!;
final id = state.pathParameters['id']!;
return Scaffold( // Modify from here
appBar: AppBar(title: Text(title)),
body: PlaylistDetails(playlistId: id, playlistName: title),
); // To here.
},
),
],
),
],
);
void main() {
if (youTubeApiKey == 'AIzaNotAnApiKey') {
print('youTubeApiKey has not been configured.');
exit(1);
}
runApp(
ChangeNotifierProvider<FlutterDevPlaylists>(
create: (context) => FlutterDevPlaylists(
flutterDevAccountId: flutterDevAccountId,
youTubeApiKey: youTubeApiKey,
),
child: const PlaylistsApp(),
),
);
}
class PlaylistsApp extends StatelessWidget {
const PlaylistsApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'FlutterDev Playlists',
theme: FlexColorScheme.light(scheme: FlexScheme.red).toTheme,
darkTheme: FlexColorScheme.dark(scheme: FlexScheme.red).toTheme,
themeMode: ThemeMode.dark, // Or ThemeMode.System if you'd prefer
debugShowCheckedModeBanner: false,
routerConfig: _router,
);
}
}
Ardından, AdaptivePlaylist widget'ı için dosyayı oluşturun:
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')),
},
],
),
);
}
}
Bu dosya, çeşitli nedenlerden dolayı ilgi çekicidir. İlk olarak, pencerenin genişliği (MediaQuery.of(context).size.width
kullanılarak) ve temanın incelenmesi (Theme.of(context).platform
kullanılarak) ile SplitView
widget'ı içeren geniş bir düzenin mi yoksa bu widget'ı içermeyen dar bir düzenin mi görüntüleneceğine karar verilir.
İkinci olarak, bu bölümde gezinmenin sabit kodlanmış şekilde işlenmesi ele alınmaktadır. Playlists
widget'ında bir geri çağırma bağımsız değişkeni gösterir. Bu geri çağırma, kullanıcının bir oynatma listesi seçtiğini çevreleyen koda bildirir. Ardından, kodun bu oynatma listesini gösterme işlemini yapması gerekir. Bu değişiklik, Playlists
ve PlaylistDetails
widget'larında Scaffold
ihtiyacını ortadan kaldırır. Artık üst düzey olmadıkları için bu widget'lardan Scaffold
simgesini kaldırmanız gerekir.
Ardından, src/lib/playlists.dart
dosyasını aşağıdaki kodla eşleşecek şekilde düzenleyin:
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);
},
),
);
},
);
}
}
Bu dosyada çok fazla değişiklik var. Yukarıda belirtilen playlistSelected
geri arama özelliğinin kullanıma sunulması ve Scaffold
widget'ının kaldırılmasının yanı sıra _PlaylistsListView
widget'ı durumsuzdan durumluya dönüştürülür. Bu değişiklik, inşa edilmesi ve yıkılması gereken bir ScrollController
özelliğinin kullanıma sunulması nedeniyle gereklidir.
Geniş bir düzende iki ListView
widget'ı yan yana olduğundan ScrollController
eklenmesi gereklidir ve bu nedenle ilgi çekicidir. Cep telefonlarında tek bir ListView
olması yaygındır. Bu nedenle, tüm ListView
'ların kendi yaşam döngüleri sırasında bağlandığı ve ayrıldığı tek bir uzun ömürlü ScrollController
olabilir. Masaüstü, yan yana birden fazla ListView
'ın mantıklı olduğu bir dünyada farklıdır.
Son olarak, lib/src/playlist_details.dart
dosyasını aşağıdaki şekilde düzenleyin:
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,
),
),
],
);
}
}
Yukarıdaki Playlists
widget'ına benzer şekilde, bu dosyada da Scaffold
widget'ının kaldırılması ve sahip olunan bir ScrollController
widget'ının kullanıma sunulmasıyla ilgili değişiklikler yapıldı.
Uygulamayı tekrar çalıştırın.
Uygulamayı Windows, macOS veya Linux gibi istediğiniz masaüstünde çalıştırma Artık beklendiği gibi çalışıyor olmalıdır.
6. Web'e uyum sağlama
Bu resimlerde ne var?
Bu uygulamayı web'de çalıştırma girişimi, web tarayıcılarına uyum sağlamak için daha fazla çalışma gerektiğini gösteriyor.
Hata ayıklama konsoluna göz atarsanız bir sonraki adımda ne yapmanız gerektiğiyle ilgili küçük bir ipucu görürsünüz.
══╡ 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 proxy'si oluşturma
Resim oluşturma sorunlarıyla başa çıkmanın bir yolu, gerekli merkezler arası kaynak paylaşımı üst bilgilerini eklemek için bir proxy web hizmeti kullanmaktır. Bir terminal açın ve aşağıdaki gibi bir Dart web sunucusu oluşturun:
$ 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
Dizini yt_cors_proxy
sunucusuna değiştirin ve birkaç gerekli bağımlılık ekleyin:
$ cd yt_cors_proxy $ dart pub add shelf_cors_headers http "http" was found in dev_dependencies. Removing "http" and adding it to dependencies instead. Resolving dependencies... Downloading packages... http 1.5.0 (from dev dependency to direct dependency) + shelf_cors_headers 0.1.5 Changed 2 dependencies!
Artık gerekli olmayan bir bağımlılık var. Şu şekilde kırpın:
$ dart pub remove shelf_router Resolving dependencies... Downloading packages... These packages are no longer being depended on: - http_methods 1.1.1 - shelf_router 1.1.4 Changed 2 dependencies!
Ardından, server.dart dosyasının içeriğini aşağıdakiyle eşleşecek şekilde değiştirin:
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}');
}
Bu sunucuyu aşağıdaki gibi çalıştırabilirsiniz:
$ dart run bin/server.dart Server listening on port 8080
Alternatif olarak, Docker görüntüsü olarak oluşturabilir ve sonuçtaki Docker görüntüsünü aşağıdaki gibi çalıştırabilirsiniz:
$ 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
Ardından, Flutter kodunu yalnızca web tarayıcısında çalışırken bu CORS proxy'den yararlanacak şekilde değiştirin.
Bir çift uyarlanabilir widget
Widget çiftinin ilki, uygulamanızın CORS proxy'sini nasıl kullanacağını gösterir.
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);
}
}
Bu uygulama, çalışma zamanı platformu farklılıkları nedeniyle kIsWeb
sabitini kullanıyor. Diğer uyarlanabilir widget, uygulamanın diğer web sayfaları gibi çalışmasını sağlar. Tarayıcı kullanıcıları, metnin seçilebilir olmasını bekler.
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),
};
}
}
Şimdi bu uyarlamaları kod tabanına yayın:
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,
),
),
],
);
}
}
Yukarıdaki kodda hem Image.network
hem de Text
widget'larını uyarladınız. Ardından Playlists
widget'ını uyarlayın.
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);
},
),
);
},
);
}
}
Bu kez yalnızca Image.network
widget'ını uyarladınız ancak iki Text
widget'ını olduğu gibi bıraktınız. Metin widget'larını uyarlarsanız kullanıcı metne dokunduğunda ListTile
'nın onTap
işlevi engellendiğinden bu durum kasıtlı olarak yapılmıştır.
Uygulamayı web'de düzgün şekilde çalıştırma
CORS proxy'si çalışırken uygulamanın web sürümünü çalıştırabilir ve aşağıdaki gibi görünmesini sağlayabilirsiniz:
7. Uyarlanabilir Kimlik Doğrulama
Bu adımda, uygulamayı kullanıcının kimliğini doğrulayabilme ve ardından kullanıcının oynatma listelerini gösterebilme özelliğiyle genişleteceksiniz. OAuth'un işlenmesi Android, iOS, web, Windows, macOS ve Linux arasında çok farklı şekilde yapıldığından uygulamanın çalışabileceği farklı platformları kapsamak için birden fazla eklenti kullanmanız gerekir.
Google kimlik doğrulamayı etkinleştirmek için eklentiler ekleme
Google kimlik doğrulamasını işlemek için üç paket yükleyeceksiniz.
$ flutter pub add googleapis_auth google_sign_in \ extension_google_sign_in_as_googleapis_auth logging Resolving dependencies... Downloading packages... + args 2.7.0 characters 1.4.0 (1.4.1 available) + crypto 3.0.6 + extension_google_sign_in_as_googleapis_auth 3.0.0 + google_identity_services_web 0.3.3+1 + google_sign_in 7.1.1 + google_sign_in_android 7.0.3 + google_sign_in_ios 6.1.0 + google_sign_in_platform_interface 3.0.0 + google_sign_in_web 1.0.0 + googleapis_auth 2.0.0 logging 1.3.0 (from transitive dependency to direct dependency) material_color_utilities 0.11.1 (0.13.0 available) meta 1.16.0 (1.17.0 available) test_api 0.7.6 (0.7.7 available) Changed 11 dependencies! 4 packages have newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information.
Windows, macOS ve Linux'ta kimlik doğrulamak için googleapis_auth
paketini kullanın. Bu masaüstü platformları, web tarayıcısı kullanarak kimlik doğrular. Android, iOS ve web'de kimlik doğrulamak için google_sign_in
ve extension_google_sign_in_as_googleapis_auth
paketlerini kullanın. İkinci paket, iki paket arasında birlikte çalışabilirlik için ara katman görevi görür.
Kodu güncelleme
Yeni bir yeniden kullanılabilir soyutlama olan AdaptiveLogin widget'ını oluşturarak güncellemeyi başlatın. Bu widget, yeniden kullanmanız için tasarlanmıştır ve bu nedenle bazı yapılandırmalar gerektirir:
lib/src/adaptive_login.dart
import 'dart:async';
import 'dart:io' show Platform;
import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:googleapis_auth/auth_io.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/link.dart';
import 'app_state.dart';
final _log = Logger('AdaptiveLogin');
typedef _AdaptiveLoginButtonWidget =
Widget Function({required VoidCallback? onPressed});
class AdaptiveLogin extends StatelessWidget {
const AdaptiveLogin({
super.key,
required this.clientId,
required this.scopes,
required this.loginButtonChild,
});
final ClientId clientId;
final List<String> scopes;
final Widget loginButtonChild;
@override
Widget build(BuildContext context) {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) {
return _GoogleSignInLogin(button: _loginButton, scopes: scopes);
} else {
return _GoogleApisAuthLogin(
button: _loginButton,
scopes: scopes,
clientId: clientId,
);
}
}
Widget _loginButton({required VoidCallback? onPressed}) =>
ElevatedButton(onPressed: onPressed, child: loginButtonChild);
}
class _GoogleSignInLogin extends StatefulWidget {
const _GoogleSignInLogin({required this.button, required this.scopes});
final _AdaptiveLoginButtonWidget button;
final List<String> scopes;
@override
State<_GoogleSignInLogin> createState() => _GoogleSignInLoginState();
}
class _GoogleSignInLoginState extends State<_GoogleSignInLogin> {
@override
initState() {
super.initState();
_googleSignIn = GoogleSignIn.instance;
_googleSignIn.initialize();
_authEventsSubscription = _googleSignIn.authenticationEvents.listen((
event,
) async {
_log.fine('Google Sign-In authentication event: $event');
if (event is GoogleSignInAuthenticationEventSignIn) {
final googleSignInClientAuthorization = await event
.user
.authorizationClient
.authorizationForScopes(widget.scopes);
if (googleSignInClientAuthorization == null) {
_log.warning('Google Sign-In authenticated client creation failed');
return;
}
_log.fine('Google Sign-In authenticated client created');
final context = this.context;
if (context.mounted) {
context.read<AuthedUserPlaylists>().authClient =
googleSignInClientAuthorization.authClient(scopes: widget.scopes);
context.go('/');
}
}
});
// Check if user is already authenticated
_log.fine('Attempting lightweight authentication');
_googleSignIn.attemptLightweightAuthentication();
}
@override
dispose() {
_authEventsSubscription.cancel();
super.dispose();
}
late final GoogleSignIn _googleSignIn;
late final StreamSubscription _authEventsSubscription;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: widget.button(
onPressed: () {
_googleSignIn.authenticate();
},
),
),
);
}
}
class _GoogleApisAuthLogin extends StatefulWidget {
const _GoogleApisAuthLogin({
required this.button,
required this.scopes,
required this.clientId,
});
final _AdaptiveLoginButtonWidget button;
final List<String> scopes;
final ClientId clientId;
@override
State<_GoogleApisAuthLogin> createState() => _GoogleApisAuthLoginState();
}
class _GoogleApisAuthLoginState extends State<_GoogleApisAuthLogin> {
@override
initState() {
super.initState();
clientViaUserConsent(widget.clientId, widget.scopes, (url) {
setState(() {
_authUrl = Uri.parse(url);
});
}).then((authClient) {
final context = this.context;
if (context.mounted) {
context.read<AuthedUserPlaylists>().authClient = authClient;
context.go('/');
}
});
}
Uri? _authUrl;
@override
Widget build(BuildContext context) {
final authUrl = _authUrl;
if (authUrl != null) {
return Scaffold(
body: Center(
child: Link(
uri: authUrl,
builder: (context, followLink) =>
widget.button(onPressed: followLink),
),
),
);
}
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
}
Bu dosya çok şey yapıyor. İşin zor kısmını AdaptiveLogin
'nın build
yöntemi yapar. Hem kIsWeb
hem de dart:io
'nin Platform.isXXX
'sini çağıran bu yöntem, çalışma zamanı platformunu kontrol eder. Android, iOS ve web için _GoogleSignInLogin
durum bilgisi olan widget'ı oluşturur. Windows, macOS ve Linux için _GoogleApisAuthLogin
durum bilgisi olan bir widget oluşturur.
Bu sınıfları kullanmak için ek yapılandırma gerekir. Bu yapılandırma, kod tabanının geri kalanını bu yeni widget'ı kullanacak şekilde güncelledikten sonra yapılır. FlutterDevPlaylists
öğesini AuthedUserPlaylists
olarak yeniden adlandırarak başlayın. Böylece, yeni yaşam amacını daha iyi yansıtabilirsiniz. Ardından, http.Client
öğesinin artık inşaattan sonra iletildiğini yansıtacak şekilde kodu güncelleyin. Son olarak, _ApiKeyClient
sınıfı artık gerekli değildir:
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
Ardından, sağlanan uygulama durumu nesnesinin yeni adıyla PlaylistDetails
widget'ını güncelleyin:
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);
},
);
}
}
Benzer şekilde, Playlists
widget'ını güncelleyin:
lib/src/playlists.dart
class Playlists extends StatelessWidget {
const Playlists({super.key, required this.playlistSelected});
final PlaylistsListSelected playlistSelected;
@override
Widget build(BuildContext context) {
return Consumer<AuthedUserPlaylists>( // Update this line
builder: (context, flutterDev, child) {
final playlists = flutterDev.playlists;
if (playlists.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return _PlaylistsListView(
items: playlists,
playlistSelected: playlistSelected,
);
},
);
}
}
Son olarak, yeni AdaptiveLogin
widget'ını doğru şekilde kullanmak için main.dart
dosyasını güncelleyin:
lib/main.dart
// Drop dart:io import
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:googleapis_auth/googleapis_auth.dart'; // Add this line
import 'package:provider/provider.dart';
import 'src/adaptive_login.dart'; // Add this line
import 'src/adaptive_playlists.dart';
import 'src/app_state.dart';
import 'src/playlist_details.dart';
// Drop flutterDevAccountId and youTubeApiKey
// Add from this line
// From https://developers.google.com/youtube/v3/guides/auth/installed-apps#identify-access-scopes
final scopes = ['https://www.googleapis.com/auth/youtube.readonly'];
// TODO: Replace with your Client ID and Client Secret for Desktop configuration
final clientId = ClientId(
'TODO-Client-ID.apps.googleusercontent.com',
'TODO-Client-secret',
);
// To this line
final _router = GoRouter(
routes: <RouteBase>[
GoRoute(
path: '/',
builder: (context, state) {
return const AdaptivePlaylists();
},
// Add redirect configuration
redirect: (context, state) {
if (!context.read<AuthedUserPlaylists>().isLoggedIn) {
return '/login';
} else {
return null;
}
},
// To this line
routes: <RouteBase>[
// Add new login Route
GoRoute(
path: 'login',
builder: (context, state) {
return AdaptiveLogin(
clientId: clientId,
scopes: scopes,
loginButtonChild: const Text('Login to YouTube'),
);
},
),
// To this line
GoRoute(
path: 'playlist/:id',
builder: (context, state) {
final title = state.uri.queryParameters['title']!;
final id = state.pathParameters['id']!;
return Scaffold(
appBar: AppBar(title: Text(title)),
body: PlaylistDetails(playlistId: id, playlistName: title),
);
},
),
],
),
],
);
void main() {
runApp(
ChangeNotifierProvider<AuthedUserPlaylists>( // Modify this line
create: (context) => AuthedUserPlaylists(), // Modify this line
child: const PlaylistsApp(),
),
);
}
class PlaylistsApp extends StatelessWidget {
const PlaylistsApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Your Playlists', // Change FlutterDev to Your
theme: FlexColorScheme.light(scheme: FlexScheme.red).toTheme,
darkTheme: FlexColorScheme.dark(scheme: FlexScheme.red).toTheme,
themeMode: ThemeMode.dark, // Or ThemeMode.System
debugShowCheckedModeBanner: false,
routerConfig: _router,
);
}
}
Bu dosyadaki değişiklikler, yalnızca Flutter'ın YouTube oynatma listelerini göstermekten kimliği doğrulanmış kullanıcının oynatma listelerini göstermeye geçişi yansıtır. Kod tamamlanmış olsa da google_sign_in
ve googleapis_auth
paketlerinin kimlik doğrulama için düzgün şekilde yapılandırılması amacıyla bu dosyada ve ilgili Runner uygulamalarındaki dosyalarda bir dizi değişiklik yapılması gerekir.
Uygulama artık kimliği doğrulanmış kullanıcının YouTube oynatma listelerini gösteriyor. Özellikler tamamlandıktan sonra kimlik doğrulamayı etkinleştirmeniz gerekir. Bunun için google_sign_in
ve googleapis_auth
paketlerini yapılandırın. Paketleri yapılandırmak için main.dart
dosyasını ve Runner uygulamalarına ait dosyaları değiştirmeniz gerekir.
googleapis_auth
'i yapılandırın
Kimlik doğrulamayı yapılandırmanın ilk adımı, daha önce yapılandırdığınız ve kullandığınız API anahtarını kaldırmaktır. API projenizin kimlik bilgileri sayfasına gidin ve API anahtarını silin:
Bu işlem, Sil düğmesine basarak onaylayacağınız bir iletişim kutusu oluşturur:
Ardından, bir OAuth istemci kimliği oluşturun:
Uygulama türü için Masaüstü uygulaması'nı seçin.
Adı kabul edin ve Oluştur'u tıklayın.
Bu işlem, googleapis_auth
akışını yapılandırmak için lib/main.dart
'ya eklemeniz gereken istemci kimliğini ve istemci gizli anahtarını oluşturur. Önemli bir uygulama ayrıntısı, googleapis_auth akışının oluşturulan OAuth jetonunu yakalamak için localhost'ta çalışan geçici bir web sunucusu kullanmasıdır. Bu işlem için macOS'te macos/Runner/Release.entitlements
dosyasında değişiklik yapılması gerekir:
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
dosyası, Hot Reload ve Dart VM hata ayıklama araçlarını etkinleştirmek için com.apple.security.network.server
yetkisine sahip olduğundan bu düzenlemeyi yapmanız gerekmez.
Uygulamanızı artık Windows, macOS veya Linux'ta çalıştırabilirsiniz (uygulama bu hedeflerde derlendiyse).
Android için google_sign_in
yapılandırma
API projenizin kimlik bilgileri sayfasına geri dönün ve başka bir OAuth istemci kimliği oluşturun. Ancak bu kez Android: seçeneğini belirleyin.
Formun geri kalanında, android/app/src/main/AndroidManifest.xml
içinde belirtilen paketle birlikte Paket adı'nı doldurun. Talimatları eksiksiz uyguladıysanız com.example.adaptive_app
olmalıdır. Google Cloud Console yardım sayfasındaki talimatları kullanarak SHA-1 sertifika parmak izini ayıklayın:
Bu, uygulamanın Android'de çalışması için yeterlidir. Kullandığınız Google API'lerine bağlı olarak, oluşturulan JSON dosyasını uygulama paketinize eklemeniz gerekebilir.
iOS için google_sign_in
yapılandırma
API projenizin kimlik bilgileri sayfasına geri dönün ve başka bir OAuth istemci kimliği oluşturun. Ancak bu kez iOS'i seçin.
Formun geri kalanında, Xcode'da ios/Runner.xcworkspace
dosyasını açarak paket kimliğini girin. Proje Gezgini'ne gidin, gezginde Runner'ı seçin, ardından Genel sekmesini seçin ve paket tanımlayıcıyı kopyalayın. Bu codelab'i adım adım uyguladıysanız com.example.adaptiveApp
olmalıdır.
Formun geri kalanında paket kimliğini girin. ios/Runner.xcworkspace
dosyasını Xcode'da açın. Proje Gezgini'ne gidin. Runner > Genel sekmesine gidin. Paket Kimliğini kopyalayın. Bu codelab'i adım adım uyguladıysanız değeri com.example.adaptiveApp
olmalıdır.
Şimdilik App Store Kimliği ve Ekip Kimliği'ni yoksayın. Bunlar yerel geliştirme için gerekli değildir:
Oluşturulan .plist
dosyasını indirin. Dosyanın adı, oluşturulan istemci kimliğinize göre belirlenir. İndirilen dosyayı GoogleService-Info.plist
olarak yeniden adlandırın ve ardından sol taraftaki gezginde Runner/Runner
altında bulunan Info.plist
dosyasıyla birlikte çalışan Xcode düzenleyicinize sürükleyin. Xcode'daki seçenekler iletişim kutusunda gerekirse Öğeleri kopyala, Klasör referansları oluştur ve Runner hedefine ekle'yi seçin.
Xcode'dan çıkın ve tercih ettiğiniz IDE'de Info.plist
bölümüne aşağıdakileri ekleyin:
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>
Değeri, oluşturulan GoogleService-Info.plist
dosyanızdaki girişle eşleşecek şekilde düzenlemeniz gerekir. Uygulamanızı çalıştırın. Giriş yaptıktan sonra oynatma listelerinizi görmeniz gerekir.
Web için google_sign_in
'u yapılandırma
API projenizin kimlik bilgileri sayfasına geri dönün ve başka bir OAuth istemci kimliği oluşturun. Ancak bu kez Web uygulaması'nı seçin.
Formun geri kalanında, Yetkilendirilmiş JavaScript kaynakları bölümünü aşağıdaki şekilde doldurun:
Bu işlem, bir istemci kimliği oluşturur. Oluşturulan istemci kimliğini içerecek şekilde güncellenen web/index.html
öğesine aşağıdaki meta
etiketini ekleyin:
web/index.html
<meta
name="google-signin-client_id"
content="YOUR_GOOGLE_SIGN_IN_OAUTH_CLIENT_ID.apps.googleusercontent.com"
/>
Bu örneğin çalıştırılması için biraz yardıma ihtiyacınız olabilir. Önceki adımda oluşturduğunuz CORS proxy'sini çalıştırmanız ve aşağıdaki talimatları kullanarak Flutter web uygulamasını Web uygulaması OAuth istemci kimliği formunda belirtilen bağlantı noktasında çalıştırmanız gerekir.
Bir terminalde CORS proxy sunucusunu aşağıdaki gibi çalıştırın:
$ dart run bin/server.dart Server listening on port 8080
Başka bir terminalde Flutter uygulamasını aşağıdaki gibi çalıştırın:
$ 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".
Bir kez daha giriş yaptıktan sonra oynatma listelerinizi görmeniz gerekir:
8. Sonraki adımlar
Tebrikler!
Codelab'i tamamladınız ve Flutter'ın desteklediği altı platformun tamamında çalışan uyarlanabilir bir Flutter uygulaması oluşturdunuz. Kodu, ekranların düzenlenme şekli, metinle etkileşim şekli, resimlerin yüklenme şekli ve kimlik doğrulamanın çalışma şeklindeki farklılıkları işleyecek şekilde uyarladınız.
Uygulamalarınızda uyarlayabileceğiniz daha birçok şey vardır. Kodunuzu çalışacağı farklı ortamlara uyarlamanın diğer yollarını öğrenmek için Uyarlanabilir uygulamalar geliştirme başlıklı makaleyi inceleyin.