Flutter'da Uyarlanabilir Uygulamalar

Flutter'da Uyarlanabilir Uygulamalar

Bu codelab hakkında

subjectSon güncelleme Haz 3, 2025
account_circleYazan: Brett Morgan

1. Giriş

Flutter, Google'ın tek bir kod tabanından mobil, web ve masaüstü için etkileyici, yerel olarak derlenmiş uygulamalar oluşturmaya yönelik kullanıcı arayüzü araç setidir. Bu codelab'de, Android, iOS, web, Windows, macOS veya Linux gibi çalıştığı platforma uyum sağlayan bir Flutter uygulamasının nasıl oluşturulacağını öğreneceksiniz.

  • Mobil cihazlar için tasarlanmış bir Flutter uygulamasının, Flutter tarafından desteklenen altı platformun tamamında çalışacak şekilde nasıl geliştirileceği.
  • Platform algılama için farklı Flutter API'leri ve her API'nin ne zaman kullanılacağı.
  • Web'de uygulama ç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 öncelikle Android ve iOS için Flutter'ın YouTube oynatma listelerini keşfeden bir Flutter uygulaması oluşturacaksınız. Ardından, uygulama penceresinin boyutuna göre bilgilerin nasıl gösterileceğini değiştirerek bu uygulamayı üç masaüstü platformunda (Windows, macOS ve Linux) çalışacak şekilde uyarlarsınız. Ardından, uygulamada gösterilen metni web kullanıcılarının beklediği gibi seçilebilir hale getirerek uygulamayı web'e uyarlarsınız. Son olarak, Flutter ekibi tarafından oluşturulan oynatma listelerinin aksine kendi oynatma listelerinizi keşfedebilmek için uygulamaya kimlik doğrulama ekleyeceksiniz. Bu işlem, Android, iOS ve web için Windows, macOS ve Linux'daki üç masaüstü platformuna göre farklı kimlik doğrulama yaklaşımları gerektirir.

Android ve iOS'teki Flutter uygulamasının ekran görüntüsü aşağıda verilmiştir:

Android emülatöründe çalışan bitmiş uygulama

iOS simülatöründe çalışan bitmiş uygulama

macOS'te geniş ekranda çalışan bu uygulama aşağıdaki ekran görüntüsüne benzemelidir.

Tamamlanan uygulama macOS'te çalışıyor

Bu codelab, mobil Flutter uygulamasını altı Flutter platformunun tamamında çalışan uyarlanabilir bir uygulamaya dönüştürmeye odaklanır. Alakalı olmayan kavramlar ve kod blokları işaretlenmiştir ve yalnızca kopyalayıp yapıştırmanız için kullanımınıza sunulmuştur.

Bu kod laboratuvarından ne öğrenmek istiyorsunuz?

2. Flutter geliştirme ortamınızı kurma

Bu laboratuvarı tamamlamak için Flutter SDK ve bir düzenleyici yazılımına ihtiyacınız vardır.

Aşağıdaki cihazlardan herhangi birini kullanarak kod laboratuvarını çalıştırabilirsiniz:

  • Bilgisayarınıza bağlı ve Geliştirici moduna ayarlanmış fiziksel bir Android veya iOS cihaz.
  • iOS simülasyon aracı (Xcode araçlarının yüklenmesi gerekir).
  • Android Emülatörü (Android Studio'da kurulum gerektirir).
  • Tarayıcı (Hata ayıklama için Chrome gerekir).
  • Windows, Linux veya macOS masaüstü uygulaması olarak. Uygulamayı dağıtmayı planladığınız platformda geliştirme yapmanız gerekir. Bu nedenle, Windows masaüstü uygulaması geliştirmek istiyorsanız uygun derleme zincirine erişmek için Windows'da geliştirme yapmanız gerekir. docs.flutter.dev/desktop adresinde işletim sistemine özgü gereksinimler ayrıntılı olarak açıklanmıştır.

3. Başlayın

Geliştirme ortamınızı onaylama

Her şeyin geliştirmeye hazır olduğundan emin olmanın en kolay yolu aşağıdaki komutu çalıştırmaktır:

flutter doctor

İşaretlenmemiş bir öğe varsa sorunla ilgili daha fazla bilgi edinmek 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 dokümanları başlıklı makaleyi inceleyin.

Flutter projesi oluşturma

Masaüstü uygulamaları için Flutter yazmaya başlamanın bir yolu, Flutter komut satırı aracını kullanarak Flutter projesi oluşturmaktır. Alternatif olarak, IDE'niz kullanıcı arayüzü üzerinden Flutter projesi oluşturmak için bir 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 şablon Flutter uygulamasını aşağıda gösterildiği gibi mobil uygulama olarak çalıştırın. Alternatif olarak, bu projeyi IDE'nizde açıp uygulamayı çalıştırmak için araçlarını kullanın. Önceki adım sayesinde, masaüstü uygulaması olarak çalıştırma seçeneği tek seçenektir.

$ 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öreceksiniz. İç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örüntülediklerini değiştirmek için sıcak yeniden yükleme yapın.

  • Uygulamayı komut satırını kullanarak çalıştırırsanı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 bunlara nasıl uyum sağlanabileceği hakkında fikir edinmenizi sağlamak için tasarlanmıştır. Android ve iOS'te yerel olarak çalışan uygulamayı burada bulabilirsiniz:

Android emulatöründe pencere özelliklerini gösterme

iOS simülatöründe pencere özelliklerini gösterme

Burada, macOS'te ve Chrome'un içinde doğal olarak çalışan aynı kod, yine macOS'te gösterilmektedir.

macOS&#39;te pencere özelliklerini gösterme

Chrome tarayıcıda pencere özelliklerini gösterme

Burada dikkat edilmesi gereken önemli nokta, Flutter'ın ilk bakışta içeriği çalıştığı ekrana uyarlamak için elinden geleni yaptığıdır. Bu ekran görüntülerinin alındığı dizüstü bilgisayarda yüksek çözünürlüklü bir Mac ekranı bulunduğundan uygulamanın hem macOS hem de web sürümleri 2 cihaz piksel oranında oluşturulmuştur. iPhone 12'de 3, Pixel 2'de ise 2,63 oranını görürsünüz. Her durumda, gösterilen metin yaklaşık olarak benzerdir. Bu da geliştirici olarak işimizi çok daha kolaylaştırır.

İkinci olarak, kodun hangi platformda çalıştığını kontrol etmek için kullanılan iki seçeneğin farklı değerler döndürdüğünü belirtmek isteriz. İlk seçenek, dart:io kaynağından içe aktarılan Platform nesnesini inceler. İkinci seçenek (yalnızca widget'ın build yönteminde kullanılabilir) ise BuildContext bağımsız değişkeninden Theme nesnesini alır.

Bu iki yöntemin farklı sonuçlar döndürmesinin nedeni, niyetlerinin farklı olmasıdır. dart:io kaynağından içe aktarılan Platform nesnesi, oluşturma seçeneklerinden bağımsız kararlar almak için kullanılır. Bunun en iyi örneği, hangi eklentilerin kullanılacağına karar vermedir. Bu eklentiler, belirli bir fiziksel platform için yerel uygulamalarla eşleşebilir veya eşleşmeyebilir.

BuildContext'dan Theme'yi çıkarmak, tema odaklı uygulama kararları için tasarlanmıştır. Bunun en iyi örneği, Slider.adaptive bölümünde ele alınan 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 erişmek için çeşitli Flutter paketlerini, durum yönetimini ve tema oluşturma özelliklerini 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
+ flex_color_scheme 8.2.0
+ flex_seed_scheme 3.5.1
+ flutter_web_plugins 0.0.0 from sdk flutter
+ go_router 15.1.2
+ googleapis 14.0.0
+ http 1.4.0
+ http_parser 4.1.2
  leak_tracker 10.0.9 (11.0.1 available)
  leak_tracker_flutter_testing 3.0.9 (3.0.10 available)
  leak_tracker_testing 3.0.1 (3.0.2 available)
  lints 5.1.1 (6.0.0 available)
+ logging 1.3.0
  material_color_utilities 0.11.1 (0.12.0 available)
  meta 1.16.0 (1.17.0 available)
+ nested 1.0.0
+ plugin_platform_interface 2.1.8
+ provider 6.1.5
  test_api 0.7.4 (0.7.6 available)
+ typed_data 1.4.0
+ url_launcher 6.3.1
+ url_launcher_android 6.3.16
+ url_launcher_ios 6.3.3
+ url_launcher_linux 3.2.1
+ url_launcher_macos 3.2.2
+ url_launcher_platform_interface 2.3.2
+ url_launcher_web 2.4.1
+ url_launcher_windows 3.1.4
  vector_math 2.1.4 (2.1.5 available)
+ web 1.1.1
Changed 22 dependencies!
8 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

Bu komut, uygulamaya çeşitli paketler ekler:

  • googleapis: Google API'lerine erişim sağlayan oluşturulmuş bir Dart kitaplığı.
  • http: Yerleşik 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 atlamanızı sağlar. Çözüme ulaştırılan bağımlılıklarda gösterildiği gibi, url_launcher varsayılan Android ve iOS'in yanı sıra Windows, macOS, Linux ve web için de uygulanabilir. Bu paketi kullandığınızda bu işleve özel bir platform oluşturmanız gerekmez.
  • flex_color_scheme: Uygulamaya güzel bir varsayılan renk şeması verir. Daha fazla bilgi edinmek için flex_color_scheme API belgelerine göz atın.
  • go_router: Farklı ekranlar arasında gezinme işlevini uygular. Bu paket, Flutter'ın Yönlendiricisi'ni kullanarak gezinmek için kullanışlı, URL tabanlı 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 dosyasına 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şleniğ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ılmaktadır. Henüz hesabınız yoksa bir hesap oluşturun.

API projesi oluşturmak için Developer Console'a gidin:

Proje oluşturma akışı sırasında GCP Console gösteriliyor

Projeniz oluşturulduktan sonra API Kitaplığı sayfasına gidin. Arama kutusuna "youtube" yazın ve youtube data api v3'ü seçin.

GCP Console&#39;da YouTube Data API v3&#39;ü seçme

YouTube Data API v3 ayrıntılar sayfasında API'yi etkinleştirin.

5a877ea82b83ae42.png

API'yi etkinleştirdikten sonra Kimlik bilgileri sayfasına gidin ve bir API anahtarı oluşturun.

GCP Console&#39;da kimlik bilgileri oluşturma

Birkaç saniye sonra, yeni API anahtarınızı içeren bir iletişim kutusu görürsünüz. Bu anahtarı kısa süre içinde kullanacaksınız.

Oluşturulan API anahtarını gösteren API anahtarı oluşturuldu pop-up&#39;ı

Kod ekle

Bu adımın geri kalanında, mobil uygulama oluşturmak için kod hakkında herhangi bir yorum yapmadan çok sayıda kodu kesip yapıştıracaksınız. Bu kod laboratuvarının amacı, mobil uygulamayı hem masaüstüne hem de web'e uyarlamaktır. Mobil cihazlar için Flutter uygulamaları oluşturmayla ilgili daha ayrıntılı bir giriş için İlk Flutter uygulamanız başlıklı makaleyi inceleyin.

Öncelikle uygulamanın durum nesnesini 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 listeleri 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 bir şey daha var. youTubeApiKey sabit değerini ö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 isteği gönderebilmesini aşağıdaki şekilde etkinleştirmeniz gerekir. Hem DebugProfile.entitlements hem de Release.entitilements dosyalarını aşağıdaki gibi 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

Uygulamanız tamamlandığında, Android emülatöründe veya iPhone simülatöründe başarıyla çalıştırabilirsiniz. Flutter'ın oynatma listelerinin listesini görürsünüz. Bir oynatma listesi seçtiğinizde, ilgili oynatma listesindeki videoları görürsünüz. Son olarak, Oynat düğmesini tıkladığınızda videoyu izlemek için YouTube'a yönlendirilirsiniz.

FlutterDev YouTube hesabının oynatma listelerini gösteren uygulama

Belirli bir oynatma listesindeki videoları gösterme

YouTube oynatıcısında oynatılan seçili bir video

Ancak bu uygulamayı masaüstünde çalıştırmayı denerseniz normal masaüstü boyutunda bir pencereye genişletildiğinde düzenin yanlış olduğunu görürsünüz. Sonraki adımda buna uyum sağlamanın yollarını inceleyeceksiniz.

5. Masaüstüne uyum sağlama

Masaüstü sorunu

Uygulamayı Windows, macOS veya Linux gibi yerel masaüstü platformlarından birinde çalıştırırsanız ilginç bir sorunla karşılaşırsınız. Çalışıyor ama tuhaf görünüyor.

macOS&#39;te çalışan uygulamada oynatma listelerinin listesi gösteriliyor ve oranlar garip görünüyor

macOS&#39;te oynatma listesindeki videolar

Bu sorunu çözmek için oynatma listelerini solda, videoları ise sağda listeleyen bölünmüş bir 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 istersiniz. 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...
  leak_tracker 10.0.9 (11.0.1 available)
  leak_tracker_flutter_testing 3.0.9 (3.0.10 available)
  leak_tracker_testing 3.0.1 (3.0.2 available)
  lints 5.1.1 (6.0.0 available)
  material_color_utilities 0.11.1 (0.12.0 available)
  meta 1.16.0 (1.17.0 available)
+ split_view 3.2.1
  test_api 0.7.4 (0.7.6 available)
  vector_math 2.1.4 (2.1.5 available)
Changed 1 dependency!
8 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

Uyarlanabilir widget'ları kullanıma sunma

Bu codelab'de kullanacağınız kalıp, ekran genişliği, platform teması gibi özelliklere göre uygulama seçimleri yapan uyarlanabilir widget'ları tanıtmaktır. Bu durumda, Playlists ve PlaylistDetails'nin etkileşim şeklini yeniden işleyen bir AdaptivePlaylists widget'ı ekleyeceksiniz. 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 birkaç nedenden dolayı ilgi çekicidir. Öncelikle, hem pencerenin genişliğini (MediaQuery.of(context).size.width kullanarak) hem de temayı (Theme.of(context).platform kullanarak) incelersiniz. Ardından, SplitView widget'ı içeren geniş bir düzen mi yoksa widget'ı içermeyen dar bir ekran mı gösterileceğine karar verirsiniz.

İkinci olarak, bu bölümde gezinmenin sabit kodla işlenmesiyle ilgili bilgiler verilmektedir. 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östermek için gerekli işlemleri yapması gerekir. Bu durum, Playlists ve PlaylistDetails widget'larındaki Scaffold ihtiyacını değiştirir. Üst düzey olmadıkları için bu widget'lardan Scaffold öğesini 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 bahsedilen playlistSelected geri çağırma işlevinin eklenmesi ve Scaffold widget'ının kaldırılmasının yanı sıra _PlaylistsListView widget'ı da durumsuzdan durumlu hale getirildi. Bu değişiklik, oluşturulması ve kaldırılması gereken sahipli bir ScrollController'ün kullanıma sunulması nedeniyle gereklidir.

Geniş bir düzende yan yana iki ListView widget'ınız olduğu için ScrollController'ün kullanılması gereklidir. Bu nedenle ScrollController'ün kullanıma sunulması ilgi çekicidir. Mobil telefonlarda tek bir ListView olması yaygındır. Bu nedenle, tüm ListView'lerin kendi yaşam döngüleri sırasında bağlanıp ayrıldığı tek bir uzun ömürlü ScrollController olabilir. Masaüstü, yan yana birden fazla ListView'ün anlamlı olduğu bir dünyada farklıdır.

Son olarak lib/src/playlist_details.dart dosyasını aşağıdaki gibi 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'in eklenmesi için değişiklikler yapılmıştır.

Uygulamayı tekrar çalıştırın.

Uygulamayı Windows, macOS veya Linux masaüstünüzde çalıştırın. Artık beklediğiniz gibi çalışacaktır.

macOS&#39;te bölünmüş görünümde çalışan uygulama

6. Web&#39;e uyum sağlama

Bu resimlerle ilgili sorun nedir?

Bu uygulamayı web'de çalıştırmaya çalıştığınızda artık web tarayıcılarına uyum sağlamak için daha fazla çalışma yapılması gerektiği ortaya çıkıyor.

Chrome tarayıcıda çalışan uygulama (YouTube resim küçük resimleri olmadan)

Hata ayıklama konsoluna göz atarsanız bir sonraki adımda ne yapmanız gerektiğine dair hafif 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ını çözmenin 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

Dizin olarak yt_cors_proxy sunucusunu seçin 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...
  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.

Artık gerekli olmayan bazı mevcut bağımlılıklar vardır. Bunları aşağıdaki gibi kırpın:

$ 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.

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 şekilde çalıştırabilirsiniz:

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

Alternatif olarak, Docker görüntüsü olarak oluşturabilir ve ortaya çıkan 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 bu CORS proxy'sinden yararlanacak şekilde değiştirin ancak yalnızca bir web tarayıcısında çalışırken.

Uyarlanabilir widget'lar

İki widget'tan ilki, uygulamanızın CORS proxy'sini nasıl kullanacağını belirtir.

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 sabit değerini kullanıyor. Diğer uyarlanabilir widget, uygulamayı diğer web sayfaları gibi çalışacak şekilde değiştirir. 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),
   
};
 
}
}

Ardından bu uyarlamaları kod tabanına dağıtı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 sefer 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'nin onTap işlevi engellenir. Bu nedenle bu değişiklik kasıtlı olarak yapılmıştır.

Uygulamayı web'de düzgün şekilde çalıştırın.

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:

Chrome tarayıcıda çalışan ve YouTube resim küçük resimlerinin doldurulduğu uygulama

7. Uyarlanabilir Kimlik Doğrulama

Bu adımda, kullanıcının kimliğini doğrulama ve ardından kullanıcının oynatma listelerini gösterme özelliğini ekleyerek uygulamayı genişleteceksiniz. OAuth'un Android, iOS, web, Windows, macOS ve Linux'ta kullanımı çok farklı olduğundan, uygulamanın çalışabileceği farklı platformları kapsayacak şekilde birden fazla eklenti kullanmanız gerekir.

Google kimlik doğrulamayı etkinleştirmek için eklenti ekleme

Google kimlik doğrulamasını yönetmek için üç paket yükleyeceksiniz.

$ 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 ve Linux'te kimlik doğrulama yapmak için googleapis_auth paketini kullanın. Bu masaüstü platformlar, kimlik doğrulamasını web tarayıcısı kullanarak yapar. 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 köprüsü 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:io' show Platform;

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

import 'app_state.dart';

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

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

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

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

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

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

 
final _AdaptiveLoginButtonWidget button;
 
final List<String> scopes;

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

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

 
late final GoogleSignIn _googleSignIn;

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

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

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

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

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

 
Uri? _authUrl;

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

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

Bu dosya çok şey yapar. İşin zor kısmını AdaptiveLogin'ın build yöntemi üstlenir. Hem kIsWeb hem de dart:io'un Platform.isXXX işlevini çağıran bu yöntem, çalışma zamanı platformunu kontrol eder. Android, iOS ve web için _GoogleSignInLogin durum bilgisine sahip widget'ı oluşturur. Windows, macOS ve Linux için _GoogleApisAuthLogin durum bilgisine sahip 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ı bu yeni widget'ı kullanacak şekilde güncellendikten sonra yapılır. Yeni amacını daha iyi yansıtması için FlutterDevPlaylists öğesini AuthedUserPlaylists olarak yeniden adlandırın ve kodu, http.Client öğesinin artık inşaattan sonra iletildiğini yansıtacak şekilde 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, PlaylistDetails widget'ını, sağlanan uygulama durumu nesnesinin yeni adıyla 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ı da 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 dosyada yapılan değişiklikler, Flutter'ın YouTube oynatma listelerinin gösterilmesinden kimliği doğrulanmış kullanıcının oynatma listelerinin gösterilmesine geçişi yansıtır. Kod tamamlanmış olsa da google_sign_in ve googleapis_auth paketlerinin kimlik doğrulama için doğru şekilde yapılandırılması amacıyla bu dosyada ve ilgili Runner uygulamalarının altı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ığında 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ının dosyalarını değiştirmeniz gerekir.

googleapis_auth'i yapılandırın

Kimlik doğrulamasını 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:

GCP Console&#39;da API projesinin kimlik bilgileri sayfası

Bu işlem, Sil düğmesine basarak onayladığınız bir iletişim kutusu oluşturur:

Kimlik bilgisini sil pop-up&#39;ı

Ardından bir OAuth istemci kimliği oluşturun:

OAuth istemci kimliği oluşturma

Uygulama türü için Masaüstü uygulaması'nı seçin.

Masaüstü uygulaması uygulama türünü seçme

Adı kabul edin ve Oluştur'u tıklayın.

İstemci kimliğini adlandırma

Bu işlem, googleapis_auth akışını yapılandırmak için lib/main.dart'e eklemeniz gereken istemci kimliğini ve istemci gizli anahtarını oluşturur. googleapis_auth akışının, oluşturulan OAuth jetonunu yakalamak için localhost'te çalışan geçici bir web sunucusu kullanması önemli bir uygulama ayrıntısıdır. Bu işlem macOS'te macos/Runner/Release.entitlements dosyasında bir değişiklik yapılmasını gerektirir:

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>

Sıcak yeniden yükleme ve Dart VM hata ayıklama araçlarını etkinleştirmek için com.apple.security.network.server'a yönelik bir hakkınız olduğu için macos/Runner/DebugProfile.entitlements dosyasında bu düzenlemeyi yapmanız gerekmez.

Artık uygulamanızı Windows, macOS veya Linux'ta (uygulama bu hedeflerde derlendiyse) çalıştırabilirsiniz.

Oturum açmış kullanıcının oynatma listelerini gösteren uygulama

Android için google_sign_in'ü yapılandırma

API projenizin kimlik bilgileri sayfasına geri dönün ve bu kez Android'i seçerek başka bir OAuth istemci kimliği oluşturun:

Android uygulama türünü seçme

Formun geri kalanında, Paket adını android/app/src/main/AndroidManifest.xml içinde belirtilen paketle doldurun. Talimatları harfi harfine uyguladıysanız com.example.adaptive_app olmalıdır. Google Cloud Console yardım sayfasındaki talimatları kullanarak SHA-1 sertifika parmak izini çıkarın:

Android istemci kimliğini adlandırma

Bu, uygulamanın Android'de çalışmasını sağlamak için yeterlidir. Kullandığınız Google API'lerine bağlı olarak, oluşturulan JSON dosyasını uygulama paketinize eklemeniz gerekebilir.

Uygulamayı Android&#39;de çalıştırma

iOS için google_sign_in'ü yapılandırma

API projenizin kimlik bilgileri sayfasına geri dönün ve bu kez iOS'u seçerek başka bir OAuth istemci kimliği oluşturun:

iOS uygulama türünü seçme

Formun geri kalanı için Xcode'da ios/Runner.xcworkspace'ü açarak paket kimliğini girin. Proje Gezgini'ne gidin, Gezgin'de Runner'ı, 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. Koşucu > Genel sekmesine gidin. Paket kimliğini kopyalayın. Bu codelab'i adım adım uyguladıysanız değerinin com.example.adaptiveApp olması gerekir.

Xcode&#39;da paket tanımlayıcıyı bulma

Yerel geliştirme için gerekli olmadığından App Store kimliğini ve ekip kimliğini şimdilik yoksayın:

iOS istemci kimliğini adlandırma

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 gezinme menüsündeki Runner/Runner altındaki Info.plist dosyasının yanına, çalışan Xcode düzenleyicinize sürükleyin. Xcode'daki seçenekler iletişim kutusunda, gerekirse Öğeleri kopyala, Klasör referansı oluştur ve Çalıştırıcıya ekle hedefini seçin.

Oluşturulan plist dosyasını Xcode&#39;taki iOS uygulamasına ekleme

Xcode'dan çıkın ve tercih ettiğiniz IDE'de Info.plist dosyanıza 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 ve giriş yaptıktan sonra oynatma listelerinizi görebilirsiniz.

iOS&#39;te çalışan uygulama

google_sign_in'ü web için yapılandırma

API projenizin kimlik bilgileri sayfasına dönün ve bu kez Web uygulaması'nı seçerek başka bir OAuth istemci kimliği oluşturun:

Web uygulaması türünü seçme

Formun geri kalanında, Yetkilendirilmiş JavaScript kaynaklarını aşağıdaki gibi doldurun:

Web uygulaması istemci kimliğini adlandırma

Bu işlem bir istemci kimliği oluşturur. Oluşturulan istemci kimliğini içerecek şekilde güncellenmiş aşağıdaki meta etiketini web/index.html'a ekleyin:

web/index.html

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

Bu örneği çalıştırmak için biraz yardıma ihtiyacınız olacak. Önceki adımda oluşturduğunuz CORS proxy'sini ve Flutter web uygulamasını, aşağıdaki talimatları uygulayarak 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".

Tekrar giriş yaptıktan sonra oynatma listelerinizi görürsünüz:

Chrome Tarayıcı&#39;da çalışan uygulama

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 nasıl düzenlendiği, metinle nasıl etkileşim kurulacağı, resimlerin nasıl yüklendiği ve kimlik doğrulamanın nasıl çalıştığıyla ilgili farklılıkları ele alacak ş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 oluşturma başlıklı makaleyi inceleyin.