Flutter'da Uyarlanabilir Uygulamalar

1. Giriş

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

Neler öğreneceksiniz?

  • Mobil için tasarlanmış bir Flutter uygulamasını, Flutter'ın desteklediği altı platformun tamamında çalışacak şekilde büyütme.
  • Platform algılamaya yönelik farklı Flutter API'leri ve her API'nin ne zaman kullanılacağı.
  • Bir uygulamayı web'de çalıştırmayla ilgili kısıtlamalara ve beklentilere uyum sağlama.
  • Flutter'ın tüm platformlarını desteklemek için farklı paketleri birlikte kullanma.

Neler oluşturacaksınız?

Bu codelab'de, ilk olarak Android ve iOS için Flutter'ın YouTube oynatma listelerini inceleyen bir 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örüntülenen metni web kullanıcılarının beklediği şekilde seçilebilir hale getirerek uygulamayı web için uyarlayabilirsiniz. Son olarak uygulamaya kimlik doğrulama ekleyerek üç masaüstü platformu Windows, macOS ve Linux için kimlik doğrulama konusunda Android, iOS ve web için farklı yaklaşımlar gerektiren Flutter ekibinin oluşturdukları listelerin yerine kendi şarkı listelerinizi keşfedebilirsiniz.

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

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

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

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

macOS'te çalışan tamamlanmış uygulama

Bu codelab'de, mobil Flutter uygulamasını altı Flutter platformunun tamamında çalışan uyarlanabilir bir uygulamaya dönüştürmeye odaklanılmaktadır. Alakalı olmayan kavramlar ve kod blokları üzerine yazılır ve kolayca kopyalayıp yapıştırmanız için sunulur.

Bu codelab'den ne öğrenmek istersiniz?

Konuyla yeni tanıştım ve iyi bir genel bakış istiyorum. Bu konuyla ilgili bilgim var ancak bilgilerinizi tazelemek istiyorum. Projemde kullanmak için örnek kod arıyorum. Belirli bir konuyla ilgili açıklama arıyorum.

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

Bu laboratuvarı tamamlamak için iki yazılıma ihtiyacınız vardır: Flutter SDK'sı ve düzenleyici.

Codelab'i aşağıdaki cihazlardan 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üklenmesini gerektirir).
  • Android Emülatör (Android Studio'da kurulum gerektirir).
  • Tarayıcı (hata ayıklama için Chrome gereklidir).
  • Windows, Linux veya macOS masaüstü uygulaması olarak Uygulamayı dağıtmayı planladığınız platformda gerçekleştirmeniz gerekir. Bu nedenle, bir Windows masaüstü uygulaması geliştirmek istiyorsanız uygun derleme zincirine erişmek için Windows'da geliştirme yapmanız gerekir. İşletim sistemine özgü gereksinimler docs.flutter.dev/desktop sayfasında ayrıntılı olarak açıklanmıştır.

3. Başlayın

Geliştirme ortamınızı onaylama

Her şeyin geliştirme için hazır olduğundan emin olmanın en kolay yolu lütfen şu komutu çalıştırın:

$ flutter doctor

Onay işareti olmadan gösterilen herhangi bir hata varsa sorunun ne olduğuyla ilgili daha fazla bilgi edinmek için lütfen aşağıdaki komutu çalıştırın:

$ flutter doctor -v

Mobil veya masaüstü geliştirme için geliştirici araçları yüklemeniz gerekebilir. Araçlarınızı ana makine işletim sisteminize bağlı olarak yapılandırma hakkında daha fazla ayrıntı için lütfen Flutter yükleme dokümanlarındaki dokümanlara bakın.

Flutter projesi oluşturma

Masaüstü uygulamaları için Flutter'ı yazmaya başlamanın kolay 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 standart Flutter uygulamasını aşağıda gösterildiği gibi bir mobil uygulama olarak çalıştırın. Alternatif olarak, bu projeyi IDE'nizde açın ve uygulamayı çalıştırmak için projedeki araçları kullanın. Önceki adım sayesinde, masaüstü uygulaması olarak çalıştırılacak tek seçenek olacaktı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=/

Şimdi uygulamanın çalıştığını görmeniz gerekir. İçeriğin güncellenmesi gerekiyor.

İçeriği güncellemek için lib/main.dart uygulamasındaki kodunuzu aşağıdaki kodla güncelleyin. Uygulamanızın görüntülediklerini değiştirmek için çalışır durumda yeniden yükleme işlemi gerçekleştirin.

  • Uygulamayı komut satırını kullanarak çalıştırıyorsanız çalışır durumda yeniden yüklemek için konsolda r yazın.
  • Uygulamayı IDE kullanarak çalıştırırsanız uygulama dosyayı kaydettiğinizde 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';
    }
  }
}

Yukarıdaki uygulama, farklı platformların nasıl algılanabileceği ve bunlara nasıl uyarlanabileceği konusunda size fikir vermek için tasarlanmıştır. Android ve iOS'te yerel olarak çalışan uygulama şu şekildedir:

Pencere özelliklerini Android emülatöründe gösterme

iOS simülasyon aracında pencere özelliklerini gösterme

Burada da aynı kod macOS'te ve Chrome'un içinde yerel olarak, yine macOS'te de çalıştırılıyor.

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

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

Burada dikkat edilmesi gereken önemli nokta, Flutter'ın içeriği çalıştığı ekrana uyarlamak için ilk bakışta elinden geleni yapmasıdır. Bu ekran görüntülerinin alındığı dizüstü bilgisayarın yüksek çözünürlüklü Mac ekranı vardır. Bu nedenle uygulamanın hem macOS hem web sürümü, Cihaz Piksel Oranı 2 olarak oluşturulur. Bu arada, iPhone 12'de Pixel 2'nin 3 ve 2,63 oranını görüyorsunuz. Her durumda, görüntülenen metin aşağı yukarı birbirine benzer. Bu da geliştiriciler olarak işimizi çok daha kolay hale getirir.

Unutulmaması gereken ikinci nokta, kodun hangi platformda çalıştırıldığını kontrol etmek için kullanılan iki seçeneğin farklı değerlere sahip olduğudur. İlk seçenek, dart:io öğesinden içe aktarılan Platform nesnesini denetler, ikinci seçenek ise (yalnızca Widget'ın build yönteminin içinde kullanılabilir) BuildContext bağımsız değişkeninden Theme nesnesini alır.

Bu iki yöntemin farklı sonuçlar döndürmesinin nedeni, amaçların 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ılmak üzere tasarlanmıştır. Bunun en iyi örneği, belirli bir fiziksel platform için eşleşen yerel uygulamalara sahip olan veya olmayan hangi eklentilerin kullanılacağına karar vermektir.

BuildContext öğesinden Theme çıkarılması, tema merkezli uygulama kararları almaya yöneliktir. Bunun en iyi örneği, Slider.adaptive bölümünde açıklandığı gibi Malzeme kaydırma çubuğunun mı yoksa Kaplumbağa kaydırma çubuğunun mı kullanılacağına karar vermektir.

Sonraki bölümde Android ve iOS için optimize edilmiş temel bir YouTube oynatma listesi gezgini uygulaması geliştireceksiniz. 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 ekle

Bu uygulamada YouTube Data API'ye erişmek, durum yönetimine ve tema oluşturmak için çeşitli Flutter paketlerini kullanacaksınız.

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

Bu komut, uygulamaya bir dizi paket ekler:

  • googleapis: Google API'lerine erişim sağlayan, üretilmiş bir Dart kitaplığıdır.
  • http: Yerel tarayıcılar ile web tarayıcıları arasındaki farkları gizleyen HTTP istekleri oluşturma kitaplığı.
  • provider: Eyalet yönetimi sağlar.
  • url_launcher: Videoya oynatma listesinden atlama olanağı tanır. Çözülmüş bağımlılıklardan gösterildiği gibi url_launcher, varsayılan Android ve iOS'in yanı sıra Windows, macOS, Linux ve web'de de kullanım alanına sahiptir. Bu paketi kullanırsanız işlev için platforma özel bir platform oluşturmanız gerekmez.
  • flex_color_scheme: Uygulamaya güzel bir varsayılan renk şeması verir. Daha fazla bilgi için flex_color_scheme API belgelerini inceleyin.
  • go_router: Farklı ekranlar arasında gezinmeyi uygular. Bu paket, Flutter'ın Yönlendirici'sini kullanarak gezinmek için URL tabanlı, kullanışlı bir API sunar.

url_launcher için mobil uygulamalar yapılandırılıyor

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, aşağıdaki satırları plist sözlüğüne ekleyin.

ios/Runner/Info.plist

<key>LSApplicationQueriesSchemes</key>
<array>
        <string>https</string>
        <string>http</string>
        <string>tel</string>
        <string>mailto</string>
</array>

Android Flutter Runner'da aşağıdaki satırları Manifest.xml bölümüne 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 gerekli yapılandırma değişiklikleri hakkında daha fazla bilgi için lütfen url_launcher dokümanlarına bakın.

YouTube Data API'ye erişme

Oynatma listelerini listelemek üzere YouTube Data API'ye erişmek için bir API projesi oluşturarak gerekli API Anahtarlarını oluşturmanız gerekir. Bu adımlarda zaten bir Google Hesabınız olduğu varsayılır. Bu nedenle, henüz bir Google Hesabınız yoksa yeni bir hesap oluşturun.

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

Proje oluşturma akışı sırasında GCP Console&#39;u gösterme

Proje oluşturduktan sonra API Kitaplığı sayfasına gidin. Arama kutusuna "youtube" yazıp youtube data API v3'ü seçin.

GCP konsolunda YouTube Data API v3 seçimi

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 bilgisi oluşturma

Birkaç saniye sonra, yepyeni API Anahtarınızın yer aldığı bir iletişim kutusu göreceksiniz. 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, kod hakkında herhangi bir açıklama yapmadan mobil uygulama oluşturmak için çok fazla kod yapıştıracaksınız. Bu codelab'in amacı, mobil uygulamayı alıp hem masaüstüne hem de web'e uyarlamaktır. Mobil cihazlar için Flutter uygulamaları geliştirmeye daha ayrıntılı bir giriş yapmak üzere lütfen İlk Flutter Uygulamanızı Yazma, 1. bölüm, 2. bölüm ve Flutter ile güzel kullanıcı arayüzleri oluşturma başlıklı makaleleri inceleyin.

Aşağıdaki dosyaları, öncelikle uygulamanın durum nesnesini 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));
  }
}

Daha sonra, ilgili oynatma listesinin 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,
          ),
        ),
      ],
    );
  }
}

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

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

Bu kodu Android ve iOS'te çalıştırmak için neredeyse hazırsınız. Değiştirilmesi gereken son bir şey de, 14. satırdaki youTubeApiKey sabit değerini, önceki adımda oluşturduğunuz YouTube API Anahtarı ile 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 aşağıdaki şekilde HTTP isteklerinde bulunabilmesini sağlamanız gerekir. DebugProfile.entitlements ve Release.entitilements dosyalarını şu ş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ırın

Artık eksiksiz bir uygulamanız olduğuna göre, bu uygulamayı bir Android emülatöründe veya iPhone simülatöründe başarıyla çalıştırabilmeniz gerekir. Flutter'ın oynatma listelerinin listesini görürsünüz. Bir oynatma listesi seçtiğinizde bu oynatma listesindeki videoları görürsünüz. Son olarak, Oynat düğmesini tıklarsanız videoyu izleyebileceğiniz YouTube deneyimi açılır.

FlutterDev YouTube hesabına ait oynatma listelerini gösteren uygulama

Belirli bir oynatma listesindeki videoları gösterme

YouTube oynatıcısının içinde oynatılan seçili bir video

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

5. Masaüstüne uyarlama

Masaüstü sorunu

Uygulamayı yerel masaüstü platformlarından birinde (Windows, macOS veya Linux) çalıştırıyorsanız ilginç bir sorun olduğunu fark edeceksiniz. İşe yarıyor ama garip görünüyor.

macOS&#39;te çalışan uygulamadaki oynatma listelerinin listesi garip şekilde görünüyor

macOS&#39;te oynatma listesindeki videolar

Bu sorunun çözümü, oynatma listelerini solda ve videoları sağda listeleyerek bölünmüş görünüm eklemektir. Bununla birlikte, bu düzenin yalnızca kod Android veya iOS'te çalışmadığında ve pencere yeterince geniş olduğunda devreye girmesini istersiniz. Aşağıdaki talimatlar, bu özelliğin nasıl uygulanacağını gösterir.

İlk olarak, düzeni oluşturmanıza yardımcı olması için split_view paketini ekleyin.

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

Uyarlanabilir widget'larla tanışın

Bu codelab'de kullanacağınız kalıp ekran genişliği ve platform teması gibi özelliklere göre uygulama seçimleri yapan Uyarlanabilir widget'ları kullanıma sunmaktır. Bu örnekte, Playlists ve PlaylistDetails arasındaki etkileşim biçimini yeniden çalışan bir AdaptivePlaylists widget'ı sunacaksınız. lib/main.dart dosyasını şu şekilde 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,
        useMaterial3: true,
      ).toTheme,
      darkTheme: FlexColorScheme.dark(
        scheme: FlexScheme.red,
        useMaterial3: true,
      ).toTheme,
      themeMode: ThemeMode.dark, // Or ThemeMode.System if you'd prefer
      debugShowCheckedModeBanner: false,
      routerConfig: _router,
    );
  }
}

Sonra, AdaptivePlaylist widget'ı için dosya 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ı ilginçtir. Öncelikle, hem pencerenin genişliğini kullanır (MediaQuery.of(context).size.width ile) hem de SplitView widget'ıyla geniş bir düzen mi, yoksa widget içermeyen dar bir ekran mı gösterileceğine karar vermek için temayı (Theme.of(context).platform ile) inceliyorsunuz.

İkinci olarak, bu bölümde navigasyonun sabit kodlu olarak 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, etrafındaki koda kullanıcının bir oynatma listesi seçtiğini bildirir. Ardından, kodun bu oynatma listesini görüntülemek için gereken işlemi yapması gerekir. Bu işlem, Playlists ve PlaylistDetails widget'larında Scaffold ihtiyacını değiştirir. Artık üst düzey olmadıklarına göre, bu widget'lardan Scaffold öğesini kaldırmanız gerekir.

Sonra, src/lib/playlists.dart dosyasını şu ş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 bir playlistSelected geri çağırması ile Scaffold widget'ının kullanımdan kaldırılmasının dışında, _PlaylistsListView widget'ı durum bilgisizden durum bilgiliye dönüştürülür. Bu değişiklik, inşa edilmesi ve imha edilmesi gereken, sahip olunan bir ScrollController kullanıma sunulduğu için gereklidir.

Geniş bir düzende yan yana iki ListView widget'ı bulundurduğunuz için gerekli olduğu için bir ScrollController'nın kullanıma sunulması ilginçtir. Cep telefonlarında tek bir ListView olması gelenekseldir. Bu nedenle, tüm ListView'lerin kendi yaşam döngüleri sırasında bağlı olduğu ve ayrıldığı tek bir uzun ömürlü ScrollController olabilir. Birden fazla ListView ürününün yan yana olduğu bir dünyada masaüstü de farklıdır.

Son olarak, lib/src/playlist_details.dart dosyasını şu ş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 ScrollController özelliğinin kullanıma sunulmasıyla ilgili değişiklikler var.

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

Uygulamayı Windows, macOS veya Linux gibi tercih ettiğiniz masaüstünde çalıştırma. Artık beklediğiniz gibi çalışacaktır.

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

6. Web'e uyum sağlama

Peki o resimler neden oluyor?

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ı gerekiyor.

YouTube küçük resimleri olmadan, Chrome tarayıcıda çalışan uygulama

Hata ayıklama konsoluna göz atarsanız bir sonraki adımda ne yapmanız gerektiğine dair 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ını çözmenin bir yolu, gerekli Merkezler Arası Kaynak Paylaşımı üstbilgilerine eklenecek bir proxy web hizmeti sunmaktır. Aşağıdaki gibi bir terminal penceresi açın ve 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 gerekli birkaç bağımlılığı 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 gerekmeyen bazı bağımlılıklar mevcuttur. Bunları şu şekilde 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.

Sonra, 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, görüntüyü bir Docker görüntüsü olarak derleyebilir ve elde edilen Docker görüntüsünü aşağıdaki şekilde ç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, sadece web tarayıcısında çalışırken bu CORS proxy'sinden yararlanmak için Flutter kodunu değiştirin.

Bir çift uyarlanabilir widget

Widget çiftinden ilki, uygulamanızın CORS proxy'sini nasıl kullanacağıdır.

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ı platform farklılıkları nedeniyle kIsWeb sabitini 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)
    };
  }
}

Ş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. Sonra, 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ı uyarlayıp iki Text widget'ını olduğu gibi bıraktınız. Bu bilinçli bir uygulamadır. Metin widget'larını uyarlarsanız kullanıcı metne dokunduğunda ListTile ürününün onTap işlevi engellenir.

Uygulamayı web'de düzgün bir şekilde çalıştırma

CORS proxy'si çalışırken uygulamanın web sürümünü çalıştırabilmeniz ve aşağıdakine benzer görünmesini sağlayabilmeniz gerekir:

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

7. Uyarlanabilir Kimlik Doğrulama

Bu adımda, kullanıcının kimliğini doğrulamasına olanak tanıyarak uygulamayı genişletir ve ardından kullanıcının oynatma listelerini gösterirsiniz. OAuth'un işlenmesi Android, iOS, web, Windows, macOS ve Linux arasında çok farklı şekilde yapıldığından, uygulamanın çalıştırılabileceği farklı platformları kapsamak için birden fazla eklenti kullanmanız gerekecek.

Google kimlik doğrulamasını etkinleştirmek için eklenti 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
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'ta kimlik doğrulamak için googleapis_auth paketini kullanın. Bu masaüstü platformları, web tarayıcısı aracılığıyla kimlik doğrulaması yapar. Android, iOS ve web üzerinde kimlik doğrulaması yapmak 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 dolgusu görevi görür.

Kodu güncelleme

Yeniden kullanılabilir yeni bir soyutlama olan AdaptiveLogin widget'ı oluşturarak güncellemeyi başlatın. Bu widget, yeniden kullanmanız için tasarlanmıştır ve bu nedenle bazı yapılandırma 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) {
          if (authClient != null) {
            context.read<AuthedUserPlaylists>().authClient = authClient;
            context.go('/');
          }
        });
      }
    });
  }

  late final GoogleSignIn _googleSignIn;

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

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

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

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

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

  Uri? _authUrl;

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

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

Bu dosya çok işe yarar. İşin büyük bir kısmını AdaptiveLogin'ın build yöntemi üstlenir. Bu yöntem hem kIsWeb hem de dart:io Platform.isXXX öğesi çağırarak çalışma zamanı platformunu kontrol eder. Android, iOS ve web için _GoogleSignInLogin durum bilgili widget'ı örneklendirir. Windows, macOS ve Linux için _GoogleApisAuthLogin durum bilgili bir widget oluşturur.

Bu sınıfları kullanmak için ek yapılandırma gerekir. Bu yapılandırmalar, yeni widget'ı kullanmak için kod tabanının geri kalanını güncelledikten sonra gelir. Kullanımdaki yeni amacını daha iyi yansıtmak için FlutterDevPlaylists öğesini AuthedUserPlaylists olarak yeniden adlandırın ve kodu, http.Client artık inşa edildikten sonra sona ereceğ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

Sonra 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ı güncelleyin:

lib/src/playlists.dart

class Playlists extends StatelessWidget {
  const Playlists({required this.playlistSelected, super.key});

  final PlaylistsListSelected playlistSelected;

  @override
  Widget build(BuildContext context) {
    return Consumer<AuthedUserPlaylists>(               // Update this line
      builder: (context, flutterDev, child) {
        final playlists = flutterDev.playlists;
        if (playlists.isEmpty) {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }

        return _PlaylistsListView(
          items: playlists,
          playlistSelected: playlistSelected,
        );
      },
    );
  }
}

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

Bu dosyadaki değişiklikler sadece Flutter'ın YouTube oynatma listelerini görüntülemekten kimliği doğrulanmış kullanıcının oynatma listelerini görüntülemeye kadar yapılan değişikliği yansıtmaktadır. Kod şu anda tamamlanmış olsa da kimlik doğrulama için google_sign_in ve googleapis_auth paketlerini doğru şekilde yapılandırmak üzere bu dosyada ve ilgili Çalıştırıcı uygulamaları altındaki dosyalarda bir dizi değişiklik yapılması gerekir.

Uygulama, kimliği doğrulanmış kullanıcının YouTube oynatma listelerini görüntüler. Özellikler tamamlandıktan sonra kimlik doğrulamayı etkinleştirmeniz gerekir. Bunu yapmak 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 yapılandırılıyor

Kimlik doğrulamayı yapılandırmanın ilk adımı, daha önce yapılandırıp kullandığınız API anahtarını kaldırmaktır. API projenizin kimlik bilgileri sayfasına gidin ve API anahtarını silin:

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

Bunu yaptığınızda, Sil düğmesine basarak onayladığınız bir pop-up oluşturulur:

Kimlik bilgisini silin pop-up&#39;ı

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

OAuth istemci kimliği oluşturma

Uygulama türü bölümünde 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 sonucunda, googleapis_auth akışını yapılandırmak için lib/main.dart ürününe eklemeniz gereken İstemci Kimliği ve İstemci Sırrı oluşturulur. Uygulamayla ilgili önemli bir ayrıntı, 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 jeton macOS'te, macos/Runner/Release.entitlements dosyasında 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>

Zaten com.apple.security.network.server engelleme özelliğini ve Dart sanal makine hata ayıklama aracını etkinleştirmek için gerekli yetkiye sahip olduğundan macos/Runner/DebugProfile.entitlements dosyasında bu düzenlemeyi yapmanız gerekmez.

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

Giriş yapmış kullanıcının oynatma listelerini gösteren uygulama

google_sign_in uygulamasını Android için yapılandırma

API projenizin kimlik bilgileri sayfasına dönün ve başka bir OAuth istemci kimliği oluşturun. Bu sefer yalnızca Android'i seçin:

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

Formun geri kalanı için Paket adını, android/app/src/main/AndroidManifest.xml öğesinde belirtilen paketi girerek doldurun. Mektuptaki talimatları uyguladıysanız com.example.adaptive_app olmalıdır. Google Cloud Platform Console yardım sayfasındaki talimatları uygulayarak SHA-1 sertifikası parmak izini çıkarın:

Android istemci kimliğini adlandırma

Bu, uygulamanın Android'de çalışması için yeterli olur. Kullandığınız Google API'leri seçimine bağlı olarak, oluşturulan JSON dosyasını uygulama paketinize eklemeniz gerekebilir.

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

google_sign_in uygulamasını iOS için yapılandırma

API projenizin kimlik bilgileri sayfasına dönün ve başka bir OAuth istemci kimliği oluşturun. Bu sefer iOS'i seçmeniz gerekir.

, iOS uygulama türünü seçme.

Formun geri kalanı için ios/Runner.xcworkspace kodunu Xcode'da açarak Paket kimliğini doldurun. Project Navigator'a gidin, kılavuzda Çalıştırıcı'yı seçin, ardından Genel sekmesini seçin ve Bundle Identifier'ı kopyalayın. Bu codelab'i adım adım uyguladıysanız değer com.example.adaptiveApp olmalıdır.

Formun geri kalanı için paket kimliğini doldurun. ios/Runner.xcworkspace öğesini Xcode'da açın. Project Navigator'a gidin. Çalıştırıcı'ya gidin > Genel sekmesi. Paket Tanımlayıcı'yı kopyalayın. Bu codelab'i adım adım takip ettiyseniz değeri com.example.adaptiveApp olmalıdır.

Xcode&#39;da paket tanımlayıcısı nerede bulunur?

App Store kimliği ve Ekip Kimliği'ni şimdilik yoksayın çünkü bunlar yerel geliştirme için gerekli değildir:

iOS istemci kimliğini adlandırma

Oluşturulan .plist dosyasını indirin. Dosyanın adı, oluşturulan istemci kimliğinizi temel alır. İndirilen dosyayı GoogleService-Info.plist olarak yeniden adlandırın ve ardından sol taraftaki gezinme panelinde Runner/Runner altındaki Info.plist dosyasının yanında bulunan Xcode düzenleyicinize sürükleyin. Xcode'daki seçenekler iletişim kutusu için gerekirse Öğeleri kopyala'yı, Klasör başvuruları oluştur'u ve Çalıştırıcıya ekle'yi seçin.

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

Xcode'dan çıkın, ardından seçtiğiniz IDE'de Info.plist'ını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. Giriş yaptıktan sonra oynatma listelerinizi göreceksiniz.

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

google_sign_in web için yapılandırılıyor

API projenizin kimlik bilgileri sayfasına dönün ve başka bir OAuth istemci kimliği oluşturun. Bu sefer yalnızca Web uygulaması'nı seçin.

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

Formun geri kalanı için Yetkilendirilmiş JavaScript kaynaklarını aşağıdaki gibi doldurun:

Web uygulaması istemci kimliğini adlandırma

Böylece bir Client-ID oluşturulur. Aşağıdaki meta etiketini web/index.html kampanyasına ekleyin. Oluşturulan Client-ID'yi içerecek şekilde güncellenmiştir:

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 el ele tutmak gerekir. Bir ö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 şekilde ç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öreceksiniz:

Chrome tarayıcıda çalışan uygulama

8. Sonraki adımlar

Tebrikler!

Codelab'i tamamladınız ve Flutter'ın desteklediği altı platformun hepsinde çalışan uyarlanabilir bir Flutter uygulaması derlediniz. Kodu, ekranların düzenlenme, metinle etkileşim kurma, resimlerin yüklenme ve kimlik doğrulamanın çalışma şekli arasındaki 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.