Informationen zu diesem Codelab
1. Einführung
Flutter ist das UI-Toolkit von Google, mit dem ansprechende, nativ kompilierte Anwendungen für Mobilgeräte, Web und Computer auf einer gemeinsamen Codebasis erstellt werden können. In diesem Codelab erfahren Sie, wie Sie eine Flutter-App entwickeln, die sich an die Plattform anpasst, auf der sie ausgeführt wird – ganz gleich ob Android, iOS, das Web, Windows, macOS oder Linux.
Lerninhalte
- Eine Flutter-App, die für Mobilgeräte entwickelt wurde, so erweitern, dass sie auf allen sechs von Flutter unterstützten Plattformen funktioniert
- Die verschiedenen Flutter APIs für die Plattformerkennung und wann die jeweilige API verwendet werden sollte.
- Sie müssen sich an die Einschränkungen und Erwartungen anpassen, die beim Ausführen einer App im Web gelten.
- So verwenden Sie verschiedene Pakete nebeneinander, um die gesamte Palette der Flutter-Plattformen zu unterstützen.
Aufgaben
In diesem Codelab erstellen Sie zuerst eine Flutter-App für Android und iOS, in der die YouTube-Playlists von Flutter vorgestellt werden. Sie passen diese Anwendung dann so an, dass sie auf den drei Desktop-Plattformen (Windows, macOS und Linux) funktioniert, indem Sie die Darstellung der Informationen an die Größe des Anwendungsfensters anpassen. Anschließend passen Sie die Anwendung für das Web an, indem Sie den in der App angezeigten Text auswählbar machen, wie es Webnutzer erwarten. Zum Schluss fügen Sie der App eine Authentifizierung hinzu, damit Sie Ihre eigenen Playlists ansehen können, im Gegensatz zu denen, die vom Flutter-Team erstellt wurden. Für Android, iOS und das Web sind dabei andere Authentifizierungsmethoden erforderlich als für die drei Desktopplattformen Windows, macOS und Linux.
Hier ist ein Screenshot der Flutter-App auf Android und iOS:
Diese App, die unter macOS im Breitbildmodus ausgeführt wird, sollte dem folgenden Screenshot ähneln.
In diesem Codelab geht es darum, eine mobile Flutter-App in eine adaptive App umzuwandeln, die auf allen sechs Flutter-Plattformen funktioniert. Auf irrelevante Konzepte wird nicht genauer eingegangen und entsprechende Codeblöcke können Sie einfach kopieren und einfügen.
Was möchten Sie in diesem Codelab lernen?
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 einem der folgenden Geräte ausführen:
- Ein physisches Android- oder iOS-Gerät, das mit Ihrem Computer verbunden und auf den Entwicklermodus gesetzt ist.
- Der iOS-Simulator (erfordert die Installation der Xcode-Tools).
- Der Android-Emulator (erfordert die Einrichtung in Android Studio).
- Einen Browser (Chrome ist für die Fehlerbehebung erforderlich)
- Als Windows-, Linux- oder macOS-Desktopanwendung Sie müssen die Entwicklung auf der Plattform durchführen, auf der Sie die Bereitstellung planen. Wenn Sie also eine Windows-Desktopanwendung entwickeln möchten, müssen Sie die Entwicklung unter Windows durchführen, um auf die entsprechende Build-Kette zugreifen zu können. Es gibt betriebssystemspezifische Anforderungen, die unter docs.flutter.dev/desktop ausführlich beschrieben werden.
3. Jetzt starten
Entwicklungsumgebung bestätigen
Am einfachsten können Sie prüfen, ob alles für die Entwicklung bereit ist, indem Sie den folgenden Befehl ausführen:
flutter doctor
Wenn etwas ohne Häkchen angezeigt wird, führen Sie Folgendes aus, um weitere Informationen zum Fehler zu erhalten:
flutter doctor -v
Möglicherweise müssen Sie Entwicklertools für die mobile oder Desktop-Entwicklung installieren. Weitere Informationen zur Konfiguration Ihrer Tools je nach Hostbetriebssystem finden Sie in der Dokumentation zur Flutter-Installation.
Flutter-Projekt erstellen
Sie können mit dem Flutter-Befehlszeilentool ein Flutter-Projekt erstellen, um mit der Entwicklung von Flutter-Desktopanwendungen zu beginnen. Alternativ bietet Ihre IDE möglicherweise einen Workflow zum Erstellen eines Flutter-Projekts über die Benutzeroberfläche.
$ 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.
Führen Sie die Flutter-Boilerplate-Anwendung als mobile App aus, um sicherzustellen, dass alles funktioniert. Alternativ können Sie dieses Projekt in Ihrer IDE öffnen und die Anwendung mit den Tools ausführen. Dank des vorherigen Schritts sollte die Ausführung als Desktopanwendung 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=/
Die App sollte jetzt ausgeführt werden. Die Inhalte müssen aktualisiert werden.
Wenn Sie die Inhalte aktualisieren möchten, ersetzen Sie den Code in lib/main.dart
durch den folgenden Code. Wenn Sie ändern möchten, was in Ihrer App angezeigt wird, führen Sie einen Hot Reload aus.
- Wenn Sie die App über die Befehlszeile ausführen, geben Sie
r
in die Konsole ein, um sie per Hot Reload neu zu laden. - Wenn Sie die App mit einer IDE ausführen, wird sie neu geladen, wenn Sie die Datei speichern.
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 App soll Ihnen ein Gefühl dafür vermitteln, wie verschiedene Plattformen erkannt und angepasst werden können. Hier ist die App, die nativ auf Android- und iOS-Geräten ausgeführt wird:
Und hier ist derselbe Code, der nativ unter macOS und in Chrome ausgeführt wird, ebenfalls unter macOS.
Wichtig ist hier, dass Flutter auf den ersten Blick alles tut, um die Inhalte an das Display anzupassen, auf dem sie ausgeführt werden. Der Laptop, auf dem diese Screenshots aufgenommen wurden, hat ein hochauflösendes Mac-Display. Deshalb werden sowohl die macOS- als auch die Webversion der App mit einem Gerätepixelverhältnis von 2 gerendert. Beim iPhone 12 beträgt das Verhältnis 3, beim Google Pixel 2 2,63. In allen Fällen ist der angezeigte Text ungefähr gleich, was unsere Arbeit als Entwickler erheblich erleichtert.
Zweitens: Die beiden Optionen zum Prüfen, auf welcher Plattform der Code ausgeführt wird, führen zu unterschiedlichen Werten. Bei der ersten Option wird das aus dart:io
importierte Platform
-Objekt geprüft. Bei der zweiten Option (nur in der build
-Methode des Widgets verfügbar) wird das Theme
-Objekt aus dem BuildContext
-Argument abgerufen.
Die beiden Methoden liefern unterschiedliche Ergebnisse, da sie unterschiedliche Zwecke haben. Das aus dart:io
importierte Platform
-Objekt soll für Entscheidungen verwendet werden, die unabhängig von den Rendering-Optionen sind. Ein gutes Beispiel hierfür ist die Entscheidung, welche Plug-ins verwendet werden sollen, die möglicherweise mit den nativen Implementierungen für eine bestimmte physische Plattform übereinstimmen oder nicht.
Das Extrahieren der Theme
aus der BuildContext
ist für entscheidungsorientierte Implementierungen gedacht. Ein gutes Beispiel hierfür ist die Entscheidung, ob der Material- oder der Cupertino-Schieberegler 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 fügen Sie verschiedene Anpassungen hinzu, damit die App auf dem Computer und im Web besser funktioniert.
4. Mobile App erstellen
Pakete hinzufügen
In dieser App verwenden Sie eine Vielzahl von Flutter-Paketen, um auf die YouTube Data API, die Zustandsverwaltung und ein wenig Design zuzugreifen.
$ flutter pub add googleapis http provider url_launcher flex_color_scheme go_router Resolving dependencies... Downloading packages... + _discoveryapis_commons 1.0.7 + flex_color_scheme 8.2.0 + flex_seed_scheme 3.5.1 + flutter_web_plugins 0.0.0 from sdk flutter + go_router 15.1.2 + googleapis 14.0.0 + http 1.4.0 + http_parser 4.1.2 leak_tracker 10.0.9 (11.0.1 available) leak_tracker_flutter_testing 3.0.9 (3.0.10 available) leak_tracker_testing 3.0.1 (3.0.2 available) lints 5.1.1 (6.0.0 available) + logging 1.3.0 material_color_utilities 0.11.1 (0.12.0 available) meta 1.16.0 (1.17.0 available) + nested 1.0.0 + plugin_platform_interface 2.1.8 + provider 6.1.5 test_api 0.7.4 (0.7.6 available) + typed_data 1.4.0 + url_launcher 6.3.1 + url_launcher_android 6.3.16 + url_launcher_ios 6.3.3 + url_launcher_linux 3.2.1 + url_launcher_macos 3.2.2 + url_launcher_platform_interface 2.3.2 + url_launcher_web 2.4.1 + url_launcher_windows 3.1.4 vector_math 2.1.4 (2.1.5 available) + web 1.1.1 Changed 22 dependencies! 8 packages have newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information.
Mit diesem Befehl werden der Anwendung mehrere Pakete hinzugefügt:
googleapis
: Eine generierte Dart-Bibliothek, die Zugriff auf Google APIs bietet.http
: Eine Bibliothek zum Erstellen von HTTP-Anfragen, die die Unterschiede zwischen nativen und Webbrowsern ausblendet.provider
: Bietet Statusverwaltung.url_launcher
: Hiermit kannst du in einer Playlist zu einem Video springen. Wie aus den aufgelösten Abhängigkeiten hervorgeht, gibt es fürurl_launcher
neben den Standardimplementierungen für Android und iOS auch Implementierungen für Windows, macOS, Linux und das Web. Wenn Sie dieses Paket verwenden, müssen Sie keine Plattform-spezifischen Funktionen für diese Funktion erstellen.flex_color_scheme
: Sie verleiht der App ein schönes Standardfarbschema. Weitere Informationen finden Sie in derflex_color_scheme
API-Dokumentation.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
konfigurieren
Für das url_launcher
-Plug-in müssen die Android- und iOS-Ausführungsanwendungen konfiguriert werden. Fügen Sie im iOS-Flutter-Runner dem plist
-Wörterbuch die folgenden Zeilen 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 zu Manifest.xml
hinzu. Fügen Sie diesen queries
-Knoten als direkt untergeordnetes Element des manifest
-Knotens und als Peer des application
-Knotens 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 url_launcher
-Dokumentation.
Auf die YouTube Data API zugreifen
Wenn du auf die YouTube Data API zugreifen und Playlists auflisten möchtest, musst du ein API-Projekt erstellen, um die erforderlichen API-Schlüssel zu generieren. Bei diesen Schritten wird davon ausgegangen, dass Sie bereits ein Google-Konto haben. Falls Sie noch keins haben, erstellen Sie eines.
Rufen Sie die Developer Console auf, um ein API-Projekt zu erstellen:
Rufen Sie nach dem Erstellen eines Projekts die Seite API-Bibliothek auf. Geben Sie im Suchfeld „youtube“ ein und wählen Sie die YouTube Data API v3 aus.
Aktiviere die API auf der Detailseite der YouTube Data API v3.
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. Sie verwenden diesen Schlüssel gleich.
Code hinzufügen
Im Rest dieses Schritts schneiden Sie viel Code aus und fügen ihn ein, um eine mobile App zu erstellen, ohne den Code zu kommentieren. In diesem Codelab soll die mobile App sowohl für Computer als auch für das Web angepasst werden. Eine ausführlichere Einführung in die Entwicklung von Flutter-Apps für Mobilgeräte finden Sie unter Erste Flutter-App.
Fügen Sie die folgenden Dateien hinzu, zuerst das Statusobjekt 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 einzelnen 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ügen Sie als Nächstes die Liste der Playlists 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(),
);
},
),
);
},
);
}
}
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).toTheme,
darkTheme: FlexColorScheme.dark(scheme: FlexScheme.red).toTheme,
themeMode: ThemeMode.dark, // Or ThemeMode.System if you'd prefer
debugShowCheckedModeBanner: false,
routerConfig: _router,
);
}
}
Sie sind fast bereit, diesen Code auf Android- und iOS-Geräten auszuführen. Es gibt noch eine Änderung vorzunehmen: Ersetzen Sie die Konstante youTubeApiKey
durch den im vorherigen Schritt generierten YouTube API-Schlüssel.
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 der App wie unten beschrieben erlauben, HTTP-Anfragen zu senden. 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
Da Sie jetzt eine vollständige Anwendung haben, sollten Sie sie in einem Android-Emulator oder einem iPhone-Simulator ausführen können. Sie sehen eine Liste der Playlists von Flutter. Wenn Sie eine Playlist auswählen, werden die Videos in dieser Playlist angezeigt. Wenn Sie auf die Wiedergabeschaltfläche klicken, werden Sie zu YouTube weitergeleitet, um sich das Video anzusehen.
Wenn Sie diese App jedoch auf dem Computer ausführen, wirkt das Layout falsch, wenn es in ein normales Fenster in Desktopgröße maximiert wird. Im nächsten Schritt sehen Sie sich Möglichkeiten an, wie Sie sich darauf einstellen können.
5. An den Computer anpassen
Das Desktopproblem
Wenn Sie die App auf einer der nativen Desktopplattformen Windows, macOS oder Linux ausführen, fällt Ihnen ein interessantes Problem auf. Es funktioniert, sieht aber… seltsam aus.
Eine Lösung dafür ist die Splitscreen-Ansicht, in der die Playlists links und die Videos rechts angezeigt werden. Dieses Layout soll jedoch nur dann aktiviert werden, wenn der Code nicht auf Android- oder iOS-Geräten ausgeführt wird und das Fenster breit genug ist. In der folgenden Anleitung wird beschrieben, wie Sie diese Funktion implementieren.
Fügen Sie zuerst das Paket split_view
hinzu, um das Layout zu erstellen.
$ flutter pub add split_view Resolving dependencies... Downloading packages... leak_tracker 10.0.9 (11.0.1 available) leak_tracker_flutter_testing 3.0.9 (3.0.10 available) leak_tracker_testing 3.0.1 (3.0.2 available) lints 5.1.1 (6.0.0 available) material_color_utilities 0.11.1 (0.12.0 available) meta 1.16.0 (1.17.0 available) + split_view 3.2.1 test_api 0.7.4 (0.7.6 available) vector_math 2.1.4 (2.1.5 available) Changed 1 dependency! 8 packages have newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information.
Einführung von adaptiven Widgets
In diesem Codelab verwenden Sie das Muster für adaptive Widgets, bei dem die Implementierung anhand von Attributen wie Bildschirmbreite und Plattformthema ausgewählt wird. In diesem Fall führen Sie ein AdaptivePlaylists
-Widget ein, mit dem die Interaktion zwischen Playlists
und PlaylistDetails
neu gestaltet wird. 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).toTheme,
darkTheme: FlexColorScheme.dark(scheme: FlexScheme.red).toTheme,
themeMode: ThemeMode.dark, // Or ThemeMode.System if you'd prefer
debugShowCheckedModeBanner: false,
routerConfig: _router,
);
}
}
Erstelle 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. Erstens wird die Breite des Fensters (MediaQuery.of(context).size.width
) verwendet und Sie prüfen das Design (Theme.of(context).platform
), um zu entscheiden, ob ein breites Layout mit dem SplitView
-Widget oder ein schmales Display ohne SplitView
angezeigt werden soll.
Zweitens geht es in diesem Abschnitt um die hartcodierte Navigation. Es wird ein Callback-Argument im Playlists
-Widget angezeigt. Über diesen Callback wird der umgebende Code darüber informiert, dass der Nutzer eine Playlist ausgewählt hat. Der Code muss dann die Arbeit ausführen, um diese Playlist anzuzeigen. Das ändert die Notwendigkeit für die Scaffold
in den Playlists
- und PlaylistDetails
-Widgets. Da sie nicht mehr auf oberster Ebene sind, müssen Sie die Scaffold
aus diesen Widgets entfernen.
Bearbeiten Sie als Nächstes die Datei src/lib/playlists.dart
so, dass sie dem folgenden Code entspricht:
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. Neben der Einführung des bereits erwähnten playlistSelected
-Callbacks und der Entfernung des Scaffold
-Widgets wird das _PlaylistsListView
-Widget von einem zustandslosen zu einem zustandsabhängigen Widget konvertiert. Diese Änderung ist aufgrund der Einführung einer eigenen ScrollController
erforderlich, die erstellt und gelöscht werden muss.
Die Einführung eines ScrollController
ist interessant, weil es erforderlich ist, da in einem breiten Layout zwei ListView
-Widgets nebeneinander angezeigt werden. Auf einem Smartphone gibt es normalerweise nur eine ListView
. Daher kann es auch nur eine langlebige ScrollController
geben, an die sich alle ListView
während ihres individuellen Lebenszyklus anhängen und von der sie sich wieder lösen. Auf dem Computer ist das anders, in einer Welt, in der mehrere ListView
nebeneinander sinnvoll sind.
Bearbeiten Sie abschließend die Datei lib/src/playlist_details.dart
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 beim Playlists
-Widget oben enthält auch diese Datei Änderungen, durch die das Scaffold
-Widget entfernt und ein eigenes ScrollController
eingeführt wird.
Starten Sie die App noch einmal.
Die App kann auf einem beliebigen Desktop-Betriebssystem ausgeführt werden, z. B. Windows, macOS oder Linux. Es sollte jetzt wie erwartet funktionieren.
6. An das Web anpassen
Was ist mit diesen Bildern los?
Wenn Sie versuchen, diese App im Web auszuführen, wird jetzt angezeigt, dass mehr Arbeit erforderlich ist, um sie an Webbrowser anzupassen.
In der Debug-Konsole finden 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, die Probleme beim Bildrendering zu beheben, besteht darin, einen Proxy-Webdienst einzurichten, um die erforderlichen CORS-Header hinzuzufügen. Öffnen Sie ein Terminal und erstellen Sie einen Dart-Webserver so:
$ 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.
Einige aktuelle Abhängigkeiten sind nicht mehr erforderlich. Schneiden Sie sie so 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“ so, dass er Folgendes enthält:
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}');
}
So führen Sie diesen Server aus:
$ 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 so, dass dieser CORS-Proxy verwendet wird, aber nur, wenn er in einem Webbrowser ausgeführt wird.
Zwei anpassbare Widgets
Das erste Widget gibt an, 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);
}
}
In dieser App wird aufgrund von Unterschieden bei der Laufzeitplattform die Konstante kIsWeb
verwendet. Das andere anpassbare Widget ändert die App so, dass sie wie andere Webseiten funktioniert. 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),
};
}
}
Übertragen Sie diese Anpassungen jetzt 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 obigen Code 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 haben Sie nur das Image.network
-Widget angepasst, die beiden Text
-Widgets aber unverändert gelassen. Das ist beabsichtigt, da die onTap
-Funktion von ListTile
blockiert wird, wenn der Nutzer auf den Text tippt.
App im Web korrekt ausführen
Wenn der CORS-Proxy ausgeführt wird, sollten Sie die Webansicht der App starten können. Sie sollte in etwa so aussehen:
7. Adaptive Authentifizierung
In diesem Schritt erweitern Sie die App, damit sie den Nutzer authentifizieren und dann die Playlists dieses Nutzers anzeigen kann. Sie müssen mehrere Plug-ins verwenden, um die verschiedenen Plattformen abzudecken, auf denen die App ausgeführt werden kann, da die OAuth-Verwaltung zwischen Android, iOS, Web, Windows, macOS und Linux sehr unterschiedlich ist.
Plug-ins hinzufügen, um die Google-Authentifizierung zu aktivieren
Sie installieren drei Pakete für die Google-Authentifizierung.
$ 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 das Paket googleapis_auth
, um sich unter Windows, macOS und Linux zu authentifizieren. Auf diesen Desktop-Plattformen erfolgt die Authentifizierung über einen Webbrowser. Verwenden Sie die Pakete google_sign_in
und extension_google_sign_in_as_googleapis_auth
, um sich unter Android, iOS und im Web zu authentifizieren. Das zweite Paket dient als Interoperabilitäts-Shims zwischen den beiden Paketen.
Code aktualisieren
Beginnen Sie mit dem Update, indem Sie eine neue wiederverwendbare Abstraktion erstellen: das AdaptiveLogin-Widget. Dieses Widget kann wiederverwendet werden 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) {
final context = this.context;
if (authClient != null && context.mounted) {
context.read<AuthedUserPlaylists>().authClient = authClient;
context.go('/');
}
});
}
});
}
late final GoogleSignIn _googleSignIn;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: widget.button(
onPressed: () {
_googleSignIn.signIn();
},
),
),
);
}
}
class _GoogleApisAuthLogin extends StatefulWidget {
const _GoogleApisAuthLogin({
required this.button,
required this.scopes,
required this.clientId,
});
final _AdaptiveLoginButtonWidget button;
final List<String> scopes;
final ClientId clientId;
@override
State<_GoogleApisAuthLogin> createState() => _GoogleApisAuthLoginState();
}
class _GoogleApisAuthLoginState extends State<_GoogleApisAuthLogin> {
@override
initState() {
super.initState();
clientViaUserConsent(widget.clientId, widget.scopes, (url) {
setState(() {
_authUrl = Uri.parse(url);
});
}).then((authClient) {
final context = this.context;
if (context.mounted) {
context.read<AuthedUserPlaylists>().authClient = authClient;
context.go('/');
}
});
}
Uri? _authUrl;
@override
Widget build(BuildContext context) {
final authUrl = _authUrl;
if (authUrl != null) {
return Scaffold(
body: Center(
child: Link(
uri: authUrl,
builder: (context, followLink) =>
widget.button(onPressed: followLink),
),
),
);
}
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
}
Diese Datei hat viele Funktionen. Die build
-Methode von AdaptiveLogin
übernimmt den Großteil der Arbeit. Durch das Aufrufen von Platform.isXXX
von kIsWeb
und dart:io
wird die Laufzeitplattform geprüft. Für Android, iOS und das Web wird das _GoogleSignInLogin
-Widget mit Status erstellt. Unter Windows, macOS und Linux wird ein _GoogleApisAuthLogin
-Widget mit Status instanziiert.
Für die Verwendung dieser Klassen ist eine zusätzliche Konfiguration erforderlich, die später erfolgt, nachdem der Rest der Codebasis für die Verwendung dieses neuen Widgets aktualisiert wurde. Beginnen Sie damit, die FlutterDevPlaylists
in AuthedUserPlaylists
umzubenennen, um ihren neuen Zweck besser widerzuspiegeln. Aktualisieren Sie dann den Code, damit die http.Client
nach dem Erstellen ü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 bereitgestellte 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 auch das Playlists
-Widget:
lib/src/playlists.dart
class Playlists extends StatelessWidget {
const Playlists({super.key, required this.playlistSelected});
final PlaylistsListSelected playlistSelected;
@override
Widget build(BuildContext context) {
return Consumer<AuthedUserPlaylists>( // Update this line
builder: (context, flutterDev, child) {
final playlists = flutterDev.playlists;
if (playlists.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return _PlaylistsListView(
items: playlists,
playlistSelected: playlistSelected,
);
},
);
}
}
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).toTheme,
darkTheme: FlexColorScheme.dark(scheme: FlexScheme.red).toTheme,
themeMode: ThemeMode.dark, // Or ThemeMode.System
debugShowCheckedModeBanner: false,
routerConfig: _router,
);
}
}
Die Änderungen in dieser Datei spiegeln die Änderung wider, dass nicht mehr nur die YouTube-Playlists von Flutter angezeigt werden, sondern die Playlists des authentifizierten Nutzers. Der Code ist jetzt zwar fertig, aber an dieser Datei und an den Dateien in den jeweiligen Runner-Apps sind noch einige Änderungen erforderlich, um die google_sign_in
- und googleapis_auth
-Pakete für die Authentifizierung richtig zu konfigurieren.
Die App zeigt jetzt YouTube-Playlists des authentifizierten Nutzers an. Nachdem Sie die Funktionen eingerichtet haben, 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-Apps ändern.
googleapis_auth
konfigurieren
Der erste Schritt zur Konfiguration der Authentifizierung besteht darin, den zuvor konfigurierten und verwendeten API-Schlüssel zu entfernen. Rufen Sie die Anmeldedatenseite Ihres API-Projekts auf und löschen Sie den API-Schlüssel:
Daraufhin wird ein Dialogfeld geöffnet, das Sie durch Klicken auf die Schaltfläche „Löschen“ bestätigen:
Erstellen Sie dann eine OAuth-Client-ID:
Wählen Sie als Anwendungstyp „Desktop-App“ aus.
Bestätigen Sie den Namen und klicken Sie auf Erstellen.
Dadurch werden die Client-ID und der Clientschlüssel erstellt, die Sie lib/main.dart
hinzufügen müssen, um den googleapis_auth
-Vorgang zu konfigurieren. Ein wichtiges Implementierungsdetail ist, dass der googleapis_auth-Vorgang einen temporären Webserver verwendet, der auf localhost ausgeführt wird, um das generierte OAuth-Token zu erfassen. Unter macOS ist 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 diese Änderung nicht an der Datei macos/Runner/DebugProfile.entitlements
vornehmen, da sie bereits eine Berechtigung für com.apple.security.network.server
hat, um Hot Reload und die Dart-VM-Debugging-Tools zu aktivieren.
Sie sollten Ihre App jetzt unter Windows, macOS oder Linux ausführen können, sofern sie für diese Ziele kompiliert wurde.
google_sign_in
für Android konfigurieren
Kehren Sie zur Seite „Anmeldedaten“ Ihres API-Projekts zurück und erstellen Sie eine weitere OAuth-Client-ID. Wählen Sie diesmal Android aus.
Geben Sie im Rest des Formulars den Paketnamen mit dem in android/app/src/main/AndroidManifest.xml
deklarierten Paket ein. Wenn Sie die Anleitung genau befolgt haben, sollte es com.example.adaptive_app
sein. Extrahieren Sie den SHA-1-Zertifikatsfingerabdruck gemäß der Anleitung in der Google Cloud Console-Hilfe:
Das reicht aus, damit die App auf Android-Geräten funktioniert. Je nachdem, welche Google APIs Sie verwenden, müssen Sie die generierte JSON-Datei möglicherweise Ihrem Anwendungsbundle hinzufügen.
google_sign_in
für iOS konfigurieren
Kehren Sie zur Anmeldedatenseite Ihres API-Projekts zurück und erstellen Sie eine weitere OAuth-Client-ID. Wählen Sie diesmal iOS aus.
Füllen Sie den Rest des Formulars aus. Öffnen Sie dazu ios/Runner.xcworkspace
in Xcode und geben Sie die Bundle-ID ein. Rufen Sie den Projektnavigator auf, wählen Sie im Navigator den Runner und dann den Tab „Allgemein“ aus und kopieren Sie die Bundle-ID. Wenn Sie dieses Codelab Schritt für Schritt durchgearbeitet haben, sollte es com.example.adaptiveApp
sein.
Geben Sie im Rest des Formulars die Bundle-ID ein. Öffnen Sie ios/Runner.xcworkspace
in Xcode. Rufen Sie den Projektnavigator auf. Gehen Sie zu „Läufer“ > Tab „Allgemein“. Kopieren Sie die Bundle-ID. Wenn Sie diesem Codelab Schritt für Schritt gefolgt sind, sollte der Wert com.example.adaptiveApp
sein.
Ignorieren Sie die App Store-ID und die Team-ID vorerst, da sie für die lokale Entwicklung nicht erforderlich sind:
Laden Sie die generierte .plist
-Datei herunter. Der Name der Datei basiert auf Ihrer generierten Client-ID. Benennen Sie die heruntergeladene Datei in GoogleService-Info.plist
um und ziehen Sie sie in den laufenden Xcode-Editor neben die Datei Info.plist
unter Runner/Runner
im linken Navigationsbereich. Wählen Sie im Optionsdialogfeld in Xcode bei Bedarf Copy items (Elemente kopieren), Create folder references (Ordnerverweise erstellen) und Add to the Runner (Ziel zum Runner hinzufügen) aus.
Schließen Sie Xcode und fügen Sie in Ihrer bevorzugten IDE 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ühre die App aus und melde dich an. Deine Playlists sollten dann angezeigt werden.
google_sign_in
für das Web konfigurieren
Kehren Sie zur Seite „Anmeldedaten“ Ihres API-Projekts zurück und erstellen Sie eine weitere OAuth-Client-ID. Wählen Sie diesmal Webanwendung aus.
Füllen Sie den Rest des Formulars so aus:
Dadurch wird eine Client-ID generiert. Fügen Sie web/index.html
das folgende meta
-Tag hinzu, das um die generierte Client-ID ergänzt wurde:
web/index.html
<meta name="google-signin-client_id" content="YOUR_GOOGLE_SIGN_IN_OAUTH_CLIENT_ID.apps.googleusercontent.com">
Das Ausführen dieses Beispiels erfordert etwas Einarbeitung. Sie müssen den im vorherigen Schritt erstellten CORS-Proxy ausführen und die Flutter-Webanwendung gemäß der folgenden Anleitung auf dem im Formular für die OAuth-Client-ID der Webanwendung angegebenen Port ausführen.
Führen Sie den CORS-Proxyserver in einem Terminal so aus:
$ dart run bin/server.dart Server listening on port 8080
Führen Sie in einem anderen Terminal die Flutter-App so 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".
Nachdem du dich noch einmal angemeldet hast, 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 so angepasst, dass Unterschiede in der Bildschirmanordnung, der Interaktion mit Text, dem Laden von Bildern und der Authentifizierung berücksichtigt werden.
Es gibt noch viele weitere Dinge, die Sie in Ihren Anwendungen anpassen können. Weitere Möglichkeiten, Ihren Code an verschiedene Umgebungen anzupassen, in denen er ausgeführt wird, finden Sie unter Adaptive Apps erstellen.