1. Einführung
Flutter ist das UI-Toolkit von Google, mit dem Sie mit einer einzigen Codebasis ansprechende, nativ kompilierte Apps für Mobilgeräte, Web und Desktop erstellen können. In diesem Codelab erfahren Sie, wie Sie eine Flutter-App erstellen, die sich an die Plattform anpasst, auf der sie ausgeführt wird – Android, iOS, das Web, Windows, macOS oder Linux.
Lerninhalte
- Hier erfahren Sie, wie Sie eine Flutter-App entwickeln, die für Mobilgeräte entwickelt wurde und auf allen sechs von Flutter unterstützten Plattformen funktioniert.
- Die verschiedenen Flutter APIs für die Plattformerkennung und wann die einzelnen APIs verwendet werden
- Sich an die Einschränkungen und Erwartungen anpassen, die beim Ausführen einer App im Web mit sich bringen.
- Hier erfahren Sie, wie Sie verschiedene Pakete parallel verwenden, um die gesamte Bandbreite der Flutter-Plattformen zu unterstützen.
Aufgaben
In diesem Codelab erstellen Sie zuerst eine Flutter-App für Android und iOS, mit der Sie sich die YouTube-Playlists von Flutter ansehen können. Anschließend passen Sie diese Anwendung für die drei Desktop-Plattformen (Windows, macOS und Linux) an, indem Sie die Darstellung der Informationen angesichts der Größe des Anwendungsfensters anpassen. Anschließend passen Sie die Anwendung für das Web an, indem Sie den in der App angezeigten Text so anpassen, wie es die Webnutzer erwarten. Zuletzt fügen Sie der App eine Authentifizierung hinzu, damit Sie Ihre eigenen Playlists entdecken können – im Gegensatz zu den Playlists des Flutter-Teams, die andere Authentifizierungsmethoden für Android, iOS und das Web erfordern als die drei Desktop-Plattformen Windows, macOS und Linux.
Hier ist ein Screenshot der Flutter-App für Android und iOS:
Diese App, die unter macOS im Breitbildformat ausgeführt wird, sollte dem folgenden Screenshot ähneln.
In diesem Codelab geht es um die Umwandlung einer mobilen Flutter-App in eine adaptive App, die auf allen sechs Flutter-Plattformen funktioniert. Irrelevante Konzepte und Codeblöcke werden nicht berücksichtigt und können einfach kopiert und eingefügt werden.
Was möchten Sie in diesem Codelab lernen?
<ph type="x-smartling-placeholder">2. Flutter-Entwicklungsumgebung einrichten
Für dieses Lab benötigen Sie zwei Softwareprogramme: das Flutter SDK und einen Editor.
Sie können das Codelab auf jedem dieser Geräte ausführen:
- Ein physisches Android- oder iOS, das mit Ihrem Computer verbunden ist und sich im Entwicklermodus befindet.
- Den iOS-Simulator (erfordert die Installation von Xcode-Tools).
- Android-Emulator (Einrichtung in Android Studio erforderlich)
- Ein Browser (zur Fehlerbehebung wird Chrome benötigt)
- Als Windows-, Linux- oder macOS-Desktopanwendung Die Entwicklung muss auf der Plattform erfolgen, auf der Sie die Bereitstellung planen. Wenn Sie also eine Windows-Desktop-App entwickeln möchten, müssen Sie die Entwicklung unter Windows ausführen, damit Sie auf die entsprechende Build-Kette zugreifen können. Es gibt betriebssystemspezifische Anforderungen, die unter docs.flutter.dev/desktop ausführlich beschrieben werden.
3. Erste Schritte
Entwicklungsumgebung bestätigen
Um sicherzugehen, dass alles bereit für die Entwicklung ist, führen Sie am einfachsten den folgenden Befehl aus:
$ flutter doctor
Wenn etwas ohne Häkchen angezeigt wird, führen Sie den folgenden Befehl aus, um weitere Details zu erhalten:
$ flutter doctor -v
Möglicherweise müssen Sie Entwicklertools für die mobile oder Desktop-Entwicklung installieren. Weitere Informationen zur Konfiguration der Tools je nach Hostbetriebssystem finden Sie in der Dokumentation zur Flutter-Installation.
Flutter-Projekt erstellen
Eine einfache Möglichkeit, Flutter für Desktop-Anwendungen zu schreiben, besteht darin, mit dem Flutter-Befehlszeilentool ein Flutter-Projekt zu erstellen. Alternativ kann Ihre IDE einen Workflow zum Erstellen eines Flutter-Projekts über die Benutzeroberfläche bereitstellen.
$ 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.
Um sicherzugehen, dass alles funktioniert, führen Sie die Boilerplate Flutter-Anwendung wie unten gezeigt als mobile App aus. Alternativ können Sie dieses Projekt in Ihrer IDE öffnen und die Anwendung mit den zugehörigen Tools ausführen. Dank des vorherigen Schritts sollte die Ausführung als Desktop-Anwendung die einzige verfügbare Option sein.
$ 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=/
Sie sollten jetzt sehen, dass die App ausgeführt wird. Der Inhalt muss aktualisiert werden.
Aktualisieren Sie Ihren Code in lib/main.dart
mit dem folgenden Code, um den Inhalt zu aktualisieren. Wenn Sie ändern möchten, was in Ihrer App angezeigt wird, führen Sie einen Hot Refresh durch.
- Wenn Sie die Anwendung über die Befehlszeile ausführen, geben Sie für einen Hot Refresh in der Konsole
r
ein. - Wenn Sie die Anwendung in einer IDE ausführen, wird sie beim Speichern der Datei neu geladen.
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';
}
}
}
Die oben genannte App soll dir ein Gefühl dafür vermitteln, wie verschiedene Plattformen erkannt und angepasst werden können. Hier sehen Sie die App, die nativ unter Android und iOS ausgeführt wird:
Hier ist der gleiche Code, der nativ unter macOS und in Chrome ausgeführt wird und auch unter macOS läuft.
Wichtig dabei ist, dass Flutter auf den ersten Blick alles tut, um die Inhalte an das Display anzupassen, auf dem es läuft. Der Laptop, auf dem diese Screenshots aufgenommen wurden, hat einen hochauflösenden Mac-Bildschirm. Deshalb werden sowohl die macOS- als auch die Webversion der App mit dem Geräte-Pixel-Verhältnis 2 gerendert. Auf dem iPhone 12 hat Pixel 2 ein Verhältnis von 3 und 2,63. In allen Fällen ist der angezeigte Text ungefähr ähnlich, was unsere Arbeit als Entwickler erheblich erleichtert.
Der zweite wichtige Punkt ist, dass die beiden Optionen zum Ermitteln der Plattform, auf der der Code ausgeführt wird, zu unterschiedlichen Werten führen. Mit der ersten Option wird das aus dart:io
importierte Platform
-Objekt geprüft. Die zweite Option (nur in der Methode build
des Widgets verfügbar) ruft das Objekt Theme
aus dem Argument BuildContext
ab.
Der Grund für die unterschiedlichen Ergebnisse dieser beiden Methoden ist, dass sie eine andere Absicht haben. Das aus dart:io
importierte Platform
-Objekt ist für Entscheidungen gedacht, die unabhängig von Rendering-Optionen sind. Ein hervorragendes Beispiel hierfür ist die Entscheidung, welche Plug-ins verwendet werden sollen. Diese Plug-ins haben entweder passende native Implementierungen für eine bestimmte physische Plattform oder nicht.
Das Extrahieren des Theme
aus dem BuildContext
ist für themenbezogene Implementierungsentscheidungen vorgesehen. Ein hervorragendes Beispiel hierfür ist die Entscheidung, ob der Schieberegler „Material“ oder der Schieberegler „Cupertino“ verwendet werden soll, wie in Slider.adaptive
erläutert.
Im nächsten Abschnitt erstellen Sie eine einfache YouTube Playlist Explorer-App, die ausschließlich für Android und iOS optimiert ist. In den folgenden Abschnitten nehmen Sie verschiedene Anpassungen vor, damit die App auf Computern und im Web besser funktioniert.
4. Mobile App erstellen
Pakete hinzufügen
In dieser App verwendest du verschiedene Flutter-Pakete für den Zugriff auf die YouTube Data API, die Statusverwaltung und verschiedene Themen.
$ 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.
Dieser Befehl fügt der Anwendung eine Reihe von Paketen hinzu:
googleapis
: Eine generierte Dart-Bibliothek, die Zugriff auf Google APIs bietet.http
: Eine Bibliothek zum Erstellen von HTTP-Anfragen, in der die Unterschiede zwischen nativen und Webbrowsern ausgeblendet werden.provider
: Bietet Statusverwaltung.url_launcher
: Bietet die Möglichkeit, aus einer Playlist zu einem Video zu springen. Wie die aufgelösten Abhängigkeiten zeigen, verfügturl_launcher
neben den standardmäßigen Android- und iOS-Implementierungen auch für Windows, macOS, Linux und das Web. Wenn Sie dieses Paket verwenden, müssen Sie keine plattformspezifische Funktion für diese Funktion erstellen.flex_color_scheme
: Verleiht der App ein schönes Standardfarbschema. Weitere Informationen finden Sie in der API-Dokumentation zuflex_color_scheme
.go_router
: Implementiert die Navigation zwischen den verschiedenen Bildschirmen. Dieses Paket bietet eine praktische, URL-basierte API für die Navigation mit dem Flutter-Router.
Mobile Apps für url_launcher
werden konfiguriert
Das url_launcher
-Plug-in erfordert die Konfiguration der Android- und iOS-Runner-Apps. Fügen Sie im Flutter-Runner von iOS die folgenden Zeilen dem Wörterbuch plist
hinzu.
ios/Runner/Info.plist
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
<string>http</string>
<string>tel</string>
<string>mailto</string>
</array>
Fügen Sie im Android Flutter-Runner die folgenden Zeilen zum Manifest.xml
hinzu. Fügen Sie diesen queries
-Knoten als direktes untergeordnetes Element des Knotens manifest
und als Peer des Knotens application
hinzu.
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>
Weitere Informationen zu diesen erforderlichen Konfigurationsänderungen finden Sie in der Dokumentation zu url_launcher
.
Zugriff auf die YouTube Data API
Wenn du auf die YouTube Data API zugreifen möchtest, um Playlists aufzulisten, musst du ein API-Projekt zum Generieren der erforderlichen API-Schlüssel erstellen. Für diese Schritte wird vorausgesetzt, dass Sie bereits ein Google-Konto haben. Erstellen Sie also eines, falls Sie noch keines zur Hand haben.
Gehen Sie zur Developer Console, um ein API-Projekt zu erstellen:
Wenn Sie ein Projekt haben, rufen Sie die Seite „API-Bibliothek“ auf. Gib im Suchfeld „youtube“ ein und wähle youtube data api v3 aus.
Aktiviere die API auf der Detailseite der YouTube Data API Version 3.
Nachdem Sie die API aktiviert haben, rufen Sie die Seite Anmeldedaten auf und erstellen Sie einen API-Schlüssel.
Nach einigen Sekunden sollte ein Dialogfeld mit Ihrem neuen API-Schlüssel angezeigt werden. Du wirst diesen Schlüssel in Kürze verwenden.
Code hinzufügen
Für den Rest dieses Schritts werden Sie viel Code ausschneiden und einfügen, um eine mobile App zu erstellen, ohne Kommentare zum Code. In diesem Codelab wird die mobile App an Computer und das Web angepasst. Eine ausführlichere Einführung zum Erstellen von Flutter-Apps für Mobilgeräte finden Sie in Write Your First Flutter App, Teil 1 und Teil 2 und Ansprechende Benutzeroberflächen mit Flutter erstellen.
Fügen Sie die folgenden Dateien hinzu, zuerst das Zustandsobjekt für die App.
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));
}
}
Füge als Nächstes die Detailseite der jeweiligen Playlist hinzu.
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,
),
),
],
);
}
}
Füge als Nächstes die Playlist-Liste hinzu.
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(),
);
},
),
);
},
);
}
}
Und ersetzen Sie den Inhalt der Datei main.dart
so:
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,
);
}
}
Du bist fast bereit, diesen Code auf Android- und iOS-Geräten auszuführen. Nur noch eine Sache zu ändern: Ändere die youTubeApiKey
-Konstante in Zeile 14 mit dem YouTube API-Schlüssel, den du im vorherigen Schritt generiert hast.
lib/main.dart
// TODO: Replace with your YouTube API Key
const youTubeApiKey = 'AIzaNotAnApiKey';
Wenn Sie diese App unter macOS ausführen möchten, müssen Sie sie so aktivieren, dass sie HTTP-Anfragen senden kann. Bearbeiten Sie die Dateien DebugProfile.entitlements
und Release.entitilements
so:
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>
Anwendung ausführen
Jetzt, da Sie über eine vollständige Anwendung verfügen, sollten Sie sie erfolgreich in einem Android-Emulator oder iPhone-Simulator ausführen können. Du siehst eine Liste mit Flutter-Playlists. Wenn du eine Playlist auswählst, werden die Videos in dieser Playlist angezeigt. Wenn du schließlich auf die Wiedergabeschaltfläche klickst, wirst du zu YouTube weitergeleitet, wo du dir das Video ansehen kannst.
Wenn Sie jedoch versuchen, diese App auf dem Desktop auszuführen, werden Sie feststellen, dass das Layout nicht stimmt, wenn Sie es in ein normales Desktop-Fenster erweitern. Im nächsten Schritt werden Sie nach Möglichkeiten suchen, sich daran anzupassen.
5. An den Desktop anpassen
Das Desktop-Problem
Wenn Sie die App auf einer der nativen Desktopplattformen (Windows, macOS oder Linux) ausführen, werden Sie ein interessantes Problem feststellen. Es funktioniert, aber es sieht ... seltsam aus.
Eine Lösung für dieses Problem ist, eine geteilte Ansicht hinzuzufügen, bei der die Playlists auf der linken und die Videos auf der rechten Seite aufgelistet werden. Dieses Layout soll jedoch nur dann zum Tragen kommen, wenn der Code unter Android oder iOS nicht ausgeführt wird und das Fenster groß genug ist. Die folgenden Anweisungen zeigen, wie Sie diese Funktion implementieren können.
Füge zuerst das Paket split_view
hinzu, um das Layout zu erstellen.
$ flutter pub add split_view Resolving dependencies... + split_view 3.1.0 test_api 0.4.3 (0.4.8 available) Changed 1 dependency!
Jetzt neu: adaptive Widgets
Mit dem Muster, das Sie in diesem Codelab verwenden, stellen Sie adaptive Widgets vor, die Implementierungsentscheidungen anhand von Attributen wie Bildschirmbreite, Plattformdesign usw. treffen. In diesem Fall stellen Sie ein AdaptivePlaylists
-Widget vor, das die Interaktion von Playlists
und PlaylistDetails
überarbeitet. Bearbeiten Sie die Datei lib/main.dart
so:
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,
);
}
}
Erstellen Sie als Nächstes die Datei für das AdaptivePlaylist-Widget:
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')),
},
],
),
);
}
}
Diese Datei ist aus mehreren Gründen interessant. Zuerst wird sowohl die Breite des Fensters (mithilfe von MediaQuery.of(context).size.width
) als auch das Design (mit Theme.of(context).platform
) verwendet, um zu entscheiden, ob ein breites Layout mit dem Widget SplitView
oder ein schmales Display ohne das Widget angezeigt werden soll.
In diesem Abschnitt geht es dann um die hartcodierte Navigation. Sie zeigt im Playlists
-Widget ein Callback-Argument an. Dieser Callback benachrichtigt den umgebenden Code, dass der Nutzer eine Playlist ausgewählt hat. Der Code muss dann die erforderlichen Schritte ausführen, um diese Playlist anzuzeigen. Dadurch ändert sich die Notwendigkeit von Scaffold
im Playlists
- und PlaylistDetails
-Widget. Da sie sich nicht mehr auf der obersten Ebene befinden, müssen Sie das Scaffold
aus diesen Widgets entfernen.
Bearbeiten Sie als Nächstes die Datei src/lib/playlists.dart
so:
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);
},
),
);
},
);
}
}
In dieser Datei gibt es viele Änderungen. Abgesehen von der oben erwähnten Einführung eines „playlistSelected“-Callbacks und der Entfernung des Scaffold
-Widgets wird das _PlaylistsListView
-Widget von zustandslos in zustandsorientiert konvertiert. Diese Änderung ist aufgrund der Einführung einer eigenen ScrollController
erforderlich, die gebaut und zerstört werden muss.
Die Einführung eines ScrollController
ist interessant, weil es erforderlich ist, weil in einem breiten Layout zwei ListView
-Widgets nebeneinander vorhanden sind. Bei Mobiltelefonen ist es üblich, einen einzigen ListView
zu verwenden. Daher kann es einen einzigen langlebigen ScrollController geben, an den alle ListView
s während ihres individuellen Lebenszyklus angehängt werden und von diesem getrennt werden. In einer Welt, in der mehrere ListView
s sinnvoll sind, ist der Desktop anders.
Bearbeiten Sie abschließend die lib/src/playlist_details.dart
-Datei so:
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,
),
),
],
);
}
}
Ähnlich wie das Playlists
-Widget oben enthält diese Datei auch Änderungen zur Entfernung des Scaffold
-Widgets und zur Einführung einer eigenen ScrollController
.
Führen Sie die App noch einmal aus.
Ausführen der App auf einem Desktop Ihrer Wahl unter Windows, macOS oder Linux Es sollte jetzt wie erwartet funktionieren.
6. Anpassung an das Web
Was ist mit diesen Bildern los, was?
Beim Versuch, diese App im Web auszuführen, ist jetzt mehr Arbeit erforderlich, um die App an den Webbrowser anzupassen.
Wenn Sie einen Blick in die Debugging-Konsole werfen, erhalten Sie einen sanften Hinweis, was Sie als Nächstes tun müssen.
══╡ 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 erstellen
Eine Möglichkeit, Probleme mit dem Rendern von Bildern zu umgehen, ist die Einführung eines Proxy-Webdienstes, der die erforderlichen Cross-Origin Resource Sharing-Header einfügt. Rufen Sie ein Terminal auf und erstellen Sie einen Dart-Webserver:
$ 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
Wechseln Sie zum Verzeichnis des yt_cors_proxy
-Servers und fügen Sie einige erforderliche Abhängigkeiten hinzu:
$ 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.
Es gibt einige aktuelle Abhängigkeiten, die nicht mehr benötigt werden. So schneidest du sie zu:
$ 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.
Ändern Sie als Nächstes den Inhalt der Datei server.dart wie folgt:
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}');
}
Sie können diesen Server so ausführen:
$ dart run bin/server.dart Server listening on port 8080
Alternativ können Sie es als Docker-Image erstellen und das resultierende Docker-Image so ausführen:
$ 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
Ändern Sie als Nächstes den Flutter-Code, um diesen CORS-Proxy zu nutzen, aber nur, wenn er in einem Webbrowser ausgeführt wird.
Zwei anpassbare Widgets
Über das erste Widget-Paar wird festgelegt, wie Ihre App den CORS-Proxy verwendet.
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);
}
}
Diese App verwendet aufgrund von Laufzeitplattformunterschieden die kIsWeb
-Konstante. Mit dem anderen anpassbaren Widget lässt sich die App wie andere Webseiten anpassen. Browsernutzer erwarten, dass Text ausgewählt werden kann.
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)
};
}
}
Verteilen Sie diese Anpassungen nun auf die gesamte Codebasis:
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,
),
),
],
);
}
}
Im Code oben haben Sie sowohl das Image.network
- als auch das Text
-Widget angepasst. Passen Sie als Nächstes das Playlists
-Widget an.
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);
},
),
);
},
);
}
}
Dieses Mal hast du nur das Image.network
-Widget angepasst, aber die beiden Text
-Widgets unverändert belassen. Das war beabsichtigt, weil die onTap
-Funktion von ListTile
blockiert wird, wenn der Nutzer auf den Text tippt, wenn du die Text-Widgets anpasst.
App richtig im Web ausführen
Wenn der CORS-Proxy ausgeführt wird, sollten Sie die Webversion der Anwendung ausführen können und sie ungefähr so aussehen:
7. Adaptive Authentifizierung
In diesem Schritt erweitern Sie die App, indem Sie ihr die Möglichkeit geben, den Nutzer zu authentifizieren, und zeigen dann die Playlists dieses Nutzers an. Sie müssen mehrere Plug-ins verwenden, um die verschiedenen Plattformen abzudecken, auf denen die App ausgeführt werden kann, da die Verarbeitung von OAuth bei Android, iOS, dem Web, Windows, macOS und Linux sehr unterschiedlich ist.
Plug-ins zum Aktivieren der Google-Authentifizierung hinzufügen
Sie werden drei Pakete für die Google-Authentifizierung installieren.
$ 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.
Verwenden Sie zur Authentifizierung unter Windows, macOS und Linux das googleapis_auth
-Paket. Bei diesen Desktop-Plattformen erfolgt die Authentifizierung über einen Webbrowser. Verwenden Sie zur Authentifizierung unter Android, iOS und im Web die Pakete google_sign_in
und extension_google_sign_in_as_googleapis_auth
. Das zweite Paket fungiert als Interoperabilitäts-Shim zwischen den beiden Paketen.
Code aktualisieren
Starten Sie das Update, indem Sie eine neue wiederverwendbare Abstraktion, das AdaptiveLogin-Widget, erstellen. Dieses Widget ist für eine Wiederverwendung vorgesehen und erfordert daher eine gewisse Konfiguration:
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(),
),
);
}
}
Diese Datei ist sehr nützlich. Die schwierige Aufgabe erfolgt über die build
-Methode von AdaptiveLogin
. Diese Methode prüft die Laufzeitplattform, indem sie sowohl kIsWeb
als auch Platform.isXXX
von dart:io
aufruft. Für Android, iOS und das Web wird das zustandsorientierte Widget _GoogleSignInLogin
instanziiert. Unter Windows, macOS und Linux wird ein zustandsorientiertes _GoogleApisAuthLogin
-Widget instanziiert.
Zur Verwendung dieser Klassen ist eine zusätzliche Konfiguration erforderlich. Diese wird später nach der Aktualisierung der restlichen Codebasis zur Verwendung dieses neuen Widgets ausgeführt. Benennen Sie zuerst die FlutterDevPlaylists
in AuthedUserPlaylists
um, um den neuen Zweck besser widerzuspiegeln. Aktualisieren Sie dann den Code, um zu verdeutlichen, dass die http.Client
jetzt nach der Konstruktion übergeben wird. Außerdem ist die Klasse _ApiKeyClient
nicht mehr erforderlich:
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
Aktualisieren Sie als Nächstes das PlaylistDetails
-Widget mit dem neuen Namen für das angegebene Anwendungsstatusobjekt:
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);
},
);
}
}
Aktualisieren Sie auf ähnliche Weise das Playlists
-Widget:
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,
);
},
);
}
}
Aktualisieren Sie abschließend die Datei main.dart
, damit das neue AdaptiveLogin
-Widget richtig verwendet wird:
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,
);
}
}
Die Änderungen in dieser Datei spiegeln die Änderung wider, dass nicht nur die YouTube-Playlists von Flutter, sondern die Playlists des authentifizierten Nutzers angezeigt werden. Obwohl der Code jetzt vollständig ist, sind noch eine Reihe von Änderungen an dieser Datei und den Dateien unter den jeweiligen Runner-Apps erforderlich, um die google_sign_in
- und googleapis_auth
-Pakete für die Authentifizierung ordnungsgemäß zu konfigurieren.
Die App zeigt nun YouTube-Playlists des authentifizierten Nutzers an. Wenn die Funktionen vollständig sind, müssen Sie die Authentifizierung aktivieren. Konfigurieren Sie dazu die Pakete google_sign_in
und googleapis_auth
. Zum Konfigurieren der Pakete müssen Sie die Datei main.dart
und die Dateien für die Runner-Anwendungen ändern.
googleapis_auth
wird konfiguriert
Der erste Schritt zur Konfiguration der Authentifizierung besteht darin, den API-Schlüssel zu entfernen, den Sie zuvor konfiguriert und verwendet haben. Rufen Sie die Seite Anmeldedaten Ihres API-Projekts auf und löschen Sie den API-Schlüssel:
Daraufhin wird ein Popup-Fenster eingeblendet, das Sie durch Klicken auf die Schaltfläche „Delete“ (Löschen) bestätigen:
Erstellen Sie dann eine OAuth-Client-ID:
Wählen Sie als Anwendungstyp „Desktop-App“ aus.
Übernehmen Sie den Namen und klicken Sie auf Erstellen.
Dadurch werden die Client-ID und der Clientschlüssel erstellt, die du lib/main.dart
hinzufügen musst, um den googleapis_auth
-Ablauf zu konfigurieren. Ein wichtiges Detail der Implementierung ist, dass der googleapis_auth-Ablauf einen temporären Webserver verwendet, der auf localhost ausgeführt wird, um das generierte OAuth-Token zu erfassen. Bei macOS ist dafür eine Änderung an der Datei macos/Runner/Release.entitlements
erforderlich:
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>
Sie müssen die Datei macos/Runner/DebugProfile.entitlements
nicht ändern, da sie bereits die Berechtigung com.apple.security.network.server
hat, Hot Refresh und die Dart VM-Debugging-Tools zu aktivieren.
Sie sollten Ihre App jetzt unter Windows, macOS oder Linux ausführen können, sofern die App für diese Ziele kompiliert wurde.
google_sign_in
für Android wird konfiguriert
Gehen Sie zurück zur Seite "Anmeldedaten" Ihres API-Projekts und erstellen Sie eine weitere OAuth-Client-ID. Wählen Sie diesmal jedoch Android aus:
Geben Sie für den Rest des Formulars den Paketnamen mit dem in android/app/src/main/AndroidManifest.xml
deklarierten Paket ein. Wenn Sie die Anweisungen zum Buchstaben befolgt haben, sollte es com.example.adaptive_app
lauten. Extrahieren Sie den SHA-1-Zertifikatfingerabdruck anhand der Anleitung auf der Hilfeseite der Google Cloud Platform Console:
Das reicht aus, damit die App unter Android funktioniert. Je nachdem, für welche Google APIs Sie sich entscheiden, müssen Sie Ihrem Anwendungs-Bundle möglicherweise die generierte JSON-Datei hinzufügen.
google_sign_in
für iOS konfigurieren
Gehen Sie zurück zur Seite "Anmeldedaten" Ihres API-Projekts und erstellen Sie eine weitere OAuth-Client-ID. Wählen Sie diesmal jedoch iOS aus:
.
Geben Sie für den Rest des Formulars die Bundle-ID ein, indem Sie ios/Runner.xcworkspace
in Xcode öffnen. Navigieren Sie zum Project Navigator, wählen Sie im Navigator den Runner aus, klicken Sie dann auf den Tab Allgemein und kopieren Sie den Bundle Identifier. Wenn Sie dieses Codelab Schritt für Schritt ausgeführt haben, sollte es com.example.adaptiveApp
lauten.
Geben Sie für den Rest des Formulars die Bundle-ID ein. Öffnen Sie ios/Runner.xcworkspace
in Xcode. Gehen Sie zum Project Navigator. Runner aufrufen > Tab „Allgemein“. Kopieren Sie den Paket-Identifikator. Wenn Sie dieses Codelab Schritt für Schritt ausgeführt haben, sollte der Wert com.example.adaptiveApp
lauten.
Ignorieren Sie die App Store-ID und die Team-ID vorerst, da diese für die lokale Entwicklung nicht erforderlich sind:
Laden Sie die generierte .plist
-Datei herunter. Der Name basiert auf der generierten Client-ID. Benennen Sie die heruntergeladene Datei in GoogleService-Info.plist
um und ziehen Sie sie dann im linken Navigationsbereich neben der Datei Info.plist
unter Runner/Runner
in Ihren laufenden Xcode-Editor. Wählen Sie im Dialogfeld „Optionen“ in Xcode bei Bedarf Elemente kopieren, Ordnerreferenzen erstellen und Zum Runner-Ziel hinzufügen aus.
Schließen Sie Xcode und fügen Sie dann in der IDE Ihrer Wahl Folgendes zu Info.plist
hinzu:
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>
Sie müssen den Wert so bearbeiten, dass er mit dem Eintrag in der generierten GoogleService-Info.plist
-Datei übereinstimmt. Führen Sie Ihre App aus. Nach der Anmeldung sollten Ihre Playlists angezeigt werden.
google_sign_in
für das Web konfigurieren
Gehen Sie zurück zur Seite „Anmeldedaten“ Ihres API-Projekts und erstellen Sie eine weitere OAuth-Client-ID. Wählen Sie dieses Mal aber Webanwendung: aus.
Geben Sie für den Rest des Formulars die autorisierten JavaScript-Quellen so an:
Dadurch wird eine Client-ID generiert. Fügen Sie web/index.html
das folgende meta
-Tag hinzu. Es muss die generierte Client-ID enthalten:
web/index.html
<meta name="google-signin-client_id" content="YOUR_GOOGLE_SIGN_IN_OAUTH_CLIENT_ID.apps.googleusercontent.com">
Zum Ausführen dieses Beispiels müssen Sie etwas in der Hand halten. Sie müssen den CORS-Proxy ausführen, den Sie im vorherigen Schritt erstellt haben, und die Flutter-Webanwendung über den Port ausführen, der im OAuth-Client-ID-Formular der Webanwendung angegeben ist. Folgen Sie dazu der folgenden Anleitung.
Führen Sie in einem Terminal den CORS-Proxyserver so aus:
$ dart run bin/server.dart Server listening on port 8080
So führen Sie die Flutter-App in einem anderen Terminal aus:
$ 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".
Nach dem erneuten Anmelden solltest du deine Playlists sehen:
8. Nächste Schritte
Glückwunsch!
Sie haben das Codelab abgeschlossen und eine adaptive Flutter-App erstellt, die auf allen sechs von Flutter unterstützten Plattformen ausgeführt werden kann. Sie haben den Code angepasst, um mit Unterschieden in der Anordnung von Bildschirmen, der Interaktion mit Text, dem Laden von Bildern und der Authentifizierung zu umgehen.
Es gibt noch viele weitere Dinge, die Sie in Ihren Anwendungen anpassen können. Weitere Möglichkeiten zum Anpassen des Codes an verschiedene Umgebungen, in denen er ausgeführt wird, finden Sie unter Adaptive Apps erstellen.