1. Pengantar
Flutter adalah toolkit UI Google untuk membangun aplikasi yang menarik dan dikompilasi secara native dari satu codebase untuk perangkat seluler, web, dan desktop. Dalam codelab ini, Anda akan mempelajari cara membangun aplikasi Flutter yang dapat beradaptasi dengan platform yang menjalankannya, baik itu Android, iOS, web, Windows, macOS, ataupun Linux.
Yang akan Anda pelajari
- Cara mengembangkan aplikasi Flutter yang didesain untuk perangkat seluler agar berfungsi di keenam platform yang didukung Flutter.
- Berbagai Flutter API untuk mendeteksi platform dan kapan harus menggunakan setiap API.
- Cara beradaptasi dengan batasan serta ekspektasi saat menjalankan aplikasi di web.
- Cara menggunakan berbagai paket secara bersamaan untuk mendukung semua platform Flutter.
Yang akan Anda bangun
Dalam codelab ini, pertama-tama Anda akan membangun aplikasi Flutter untuk Android dan iOS yang menjelajahi playlist YouTube Flutter. Selanjutnya, Anda akan mengadaptasikan aplikasi ini agar berfungsi di tiga platform desktop (Windows, macOS, dan Linux) dengan mengubah cara informasi ditampilkan berdasarkan ukuran jendela aplikasi. Kemudian, Anda akan mengadaptasikan aplikasi untuk web dengan membuat teks yang ditampilkan di aplikasi dapat dipilih, seperti yang diharapkan pengguna web. Terakhir, Anda akan menambahkan autentikasi ke aplikasi sehingga Anda dapat menjelajahi Playlist Anda sendiri, bukan playlist yang dibuat tim Flutter. Hal ini memerlukan pendekatan autentikasi yang berbeda untuk Android, iOS, dan web, versus tiga platform desktop (Windows, macOS, dan Linux).
Berikut screenshot aplikasi Flutter di Android dan iOS:
Berikut screenshot aplikasi yang berjalan di macOS dalam tata letak layar lebar:
Codelab ini berfokus untuk mentransformasi aplikasi Flutter seluler menjadi aplikasi adaptif yang berfungsi di keenam platform Flutter. Konsep dan blok kode yang tidak relevan akan dibahas sekilas, dan disediakan sehingga Anda cukup menyalin dan menempelkannya.
Apa yang ingin Anda pelajari dari codelab ini?
2. Menyiapkan lingkungan pengembangan Flutter Anda
Anda memerlukan dua software untuk menyelesaikan lab ini—Flutter SDK dan editor.
Anda dapat menjalankan codelab menggunakan perangkat berikut:
- Perangkat Android atau iOS fisik yang terhubung ke komputer dan disetel ke Mode developer.
- Simulator iOS (perlu menginstal alat Xcode).
- Android Emulator (perlu penyiapan di Android Studio).
- Browser (perlu Chrome untuk proses debug).
- Aplikasi desktop Windows, Linux, atau macOS. Anda harus mengembangkan aplikasi di platform tempat Anda berencana untuk men-deploy-nya. Jadi, jika ingin mengembangkan aplikasi desktop Windows, Anda harus mengembangkannya di Windows untuk mengakses rantai build yang sesuai. Ada persyaratan spesifik per sistem operasi yang dibahas secara mendetail di docs.flutter.dev/desktop.
3. Memulai
Mengonfirmasi lingkungan pengembangan Anda
Cara termudah untuk memastikan semuanya siap untuk pengembangan adalah dengan menjalankan perintah berikut:
$ flutter doctor
Jika ada yang muncul tanpa tanda centang, jalankan perintah berikut untuk mendapatkan detail selengkapnya terkait masalah tersebut.
$ flutter doctor -v
Anda mungkin perlu menginstal alat developer untuk pengembangan perangkat seluler atau desktop. Untuk detail selengkapnya terkait cara mengonfigurasi alat berdasarkan sistem operasi host, lihat dokumentasi penginstalan Flutter.
Membuat project Flutter
Cara mudah untuk mulai menulis Flutter bagi aplikasi desktop adalah dengan menggunakan alat command line Flutter untuk membuat project Flutter. Atau, IDE Anda dapat menyediakan alur kerja untuk membuat project Flutter melalui UI-nya.
$ flutter create adaptive_app Creating project adaptive_app [Eliding listing of created files] Running "flutter pub get" in adaptive_app... 2,445ms Wrote 128 files. All done! In order to run your application, type: $ cd adaptive_app $ flutter run Your application code is in adaptive_app/lib/main.dart.
Untuk memastikan semuanya berfungsi, jalankan aplikasi Flutter boilerplate sebagai aplikasi seluler seperti yang ditunjukkan di bawah. Atau, buka project ini di IDE Anda, lalu gunakan alatnya untuk menjalankan aplikasi. Setelah melakukan langkah sebelumnya, satu-satunya opsi yang tersedia adalah menjalankannya sebagai aplikasi desktop.
$ flutter run Launching lib/main.dart on iPhone 12 in debug mode... Running Xcode build... └─Compiling, linking and signing... 15.9s Xcode build done. 41.2s Syncing files to device iPhone 12... 213ms 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). 💪 Running with sound null safety 💪 An Observatory debugger and profiler on iPhone 12 is available at: http://127.0.0.1:60071/t9hy0pnIWgE=/ Activating Dart DevTools... 3.3s The Flutter DevTools debugger and profiler on iPhone 12 is available at: http://127.0.0.1:9101?uri=http://127.0.0.1:60071/t9hy0pnIWgE=/
Sekarang, Anda dapat melihat aplikasi berjalan. Ubah konten di lib/main.dart sebagai berikut, dan lakukan hot reload untuk mengupdate kontennya. Cara melakukan hot reload berubah bergantung pada apakah Anda menjalankan aplikasi melalui command line (ketik "r" di jendela konsol) atau melalui editor (dalam hal ini, Anda mungkin cukup menyimpan file untuk memicu hot reload).
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(
primarySwatch: Colors.blue,
),
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.headline5,
),
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';
}
}
}
Aplikasi di atas didesain untuk membantu Anda memahami cara mendeteksi dan beradaptasi dengan berbagai platform. Berikut aplikasi yang berjalan secara native di Android dan iOS:
Dan berikut adalah kode yang sama yang berjalan secara native di macOS dan Chrome, sekali lagi berjalan di macOS.
Poin penting yang perlu diperhatikan di sini adalah secara sekilas, Flutter berupaya sebaik mungkin untuk mengadaptasikan konten dengan layar yang menjalankannya. Screenshot ini diambil di laptop yang memiliki layar Mac beresolusi tinggi. Itulah sebabnya aplikasi versi web dan macOS dirender pada Rasio Pixel Perangkat 2. Sementara itu, rasio untuk iPhone adalah 3, sedangkan untuk Pixel 2 adalah 2,63. Dalam semua kasus, teks yang ditampilkan kurang lebih sama, sehingga semakin mempermudah pekerjaan developer.
Poin kedua yang perlu diperhatikan adalah bahwa dua opsi untuk memeriksa di platform mana kode dijalankan menghasilkan nilai yang berbeda. Opsi pertama memeriksa objek Platform
yang diimpor dari dart:io
, sedangkan opsi kedua (hanya tersedia dalam metode build
Widget) mengambil objek Theme
dari argumen BuildContext
.
Karena tujuan dari kedua metode ini berbeda, hasil yang ditampilkan akan berbeda pula. Objek Platform
yang diimpor dari dart:io
digunakan untuk membuat keputusan yang tidak bergantung pada pilihan rendering. Contoh terbaiknya adalah memutuskan plugin mana yang akan digunakan, yang mungkin memiliki atau tidak memiliki implementasi native yang cocok untuk platform fisik tertentu.
Pengekstrakan Theme
dari BuildContext
dimaksudkan untuk membuat keputusan implementasi yang berpusat pada Tema. Contoh terbaiknya adalah memutuskan apakah akan menggunakan penggeser Material, atau penggeser Cupertino, seperti yang dijelaskan dalam Slider.adaptive
.
Pada bagian berikutnya, Anda akan membangun aplikasi dasar penjelajah playlist YouTube yang dioptimalkan khusus untuk Android dan iOS. Anda juga akan menambahkan berbagai adaptasi agar aplikasi dapat berfungsi dengan lebih baik di desktop dan web.
4. Membangun aplikasi seluler
Menambahkan paket
Di aplikasi ini, Anda akan menggunakan berbagai paket Flutter untuk mendapatkan akses ke YouTube Data API, pengelolaan status, dan penerapan tema.
$ flutter pub add googleapis Resolving dependencies... + _discoveryapis_commons 1.0.3 async 2.8.2 (2.9.0 available) characters 1.2.0 (1.2.1 available) clock 1.1.0 (1.1.1 available) fake_async 1.3.0 (1.3.1 available) + googleapis 9.1.0 + http 0.13.4 + http_parser 4.0.1 matcher 0.12.11 (0.12.12 available) material_color_utilities 0.1.4 (0.1.5 available) meta 1.7.0 (1.8.0 available) path 1.8.1 (1.8.2 available) source_span 1.8.2 (1.9.0 available) string_scanner 1.1.0 (1.1.1 available) term_glyph 1.2.0 (1.2.1 available) test_api 0.4.9 (0.4.12 available) + typed_data 1.3.1 Changed 5 dependencies!
Paket pertama, googleapis
adalah library Dart yang dihasilkan untuk mengakses Google API.
$ flutter pub add http Resolving dependencies... async 2.8.2 (2.9.0 available) characters 1.2.0 (1.2.1 available) clock 1.1.0 (1.1.1 available) fake_async 1.3.0 (1.3.1 available) matcher 0.12.11 (0.12.12 available) material_color_utilities 0.1.4 (0.1.5 available) meta 1.7.0 (1.8.0 available) path 1.8.1 (1.8.2 available) source_span 1.8.2 (1.9.0 available) string_scanner 1.1.0 (1.1.1 available) term_glyph 1.2.0 (1.2.1 available) test_api 0.4.9 (0.4.12 available) Got dependencies!
Paket http
akan digunakan untuk membangun kemampuan untuk mengakses YouTube Data API menggunakan kunci API.
$ flutter pub add provider Resolving dependencies... async 2.8.2 (2.9.0 available) characters 1.2.0 (1.2.1 available) clock 1.1.0 (1.1.1 available) fake_async 1.3.0 (1.3.1 available) matcher 0.12.11 (0.12.12 available) material_color_utilities 0.1.4 (0.1.5 available) meta 1.7.0 (1.8.0 available) + nested 1.0.0 path 1.8.1 (1.8.2 available) + provider 6.0.3 source_span 1.8.2 (1.9.0 available) string_scanner 1.1.0 (1.1.1 available) term_glyph 1.2.0 (1.2.1 available) test_api 0.4.9 (0.4.12 available) Changed 2 dependencies!
Untuk pengelolaan status, gunakan provider
.
$ flutter pub add url_launcher Resolving dependencies... async 2.8.2 (2.9.0 available) characters 1.2.0 (1.2.1 available) clock 1.1.0 (1.1.1 available) fake_async 1.3.0 (1.3.1 available) + flutter_web_plugins 0.0.0 from sdk flutter + js 0.6.4 matcher 0.12.11 (0.12.12 available) material_color_utilities 0.1.4 (0.1.5 available) meta 1.7.0 (1.8.0 available) path 1.8.1 (1.8.2 available) + plugin_platform_interface 2.1.2 source_span 1.8.2 (1.9.0 available) string_scanner 1.1.0 (1.1.1 available) term_glyph 1.2.0 (1.2.1 available) test_api 0.4.9 (0.4.12 available) + url_launcher 6.1.4 + url_launcher_android 6.0.17 + url_launcher_ios 6.0.17 + url_launcher_linux 3.0.1 + url_launcher_macos 3.0.1 + url_launcher_platform_interface 2.1.0 + url_launcher_web 2.0.12 + url_launcher_windows 3.0.1 Changed 11 dependencies!
Gunakan url_launcher
sebagai cara untuk membuka video langsung dari playlist. Seperti yang Anda lihat dari dependensi yang di-resolve, url_launcher
tidak hanya memiliki implementasi untuk Android dan iOS default, tetapi juga untuk Windows, macOS, Linux, dan web. Untuk kemampuan ini, Anda tidak perlu membuat kode khusus platform.
$ flutter pub add flex_color_scheme Resolving dependencies... async 2.8.2 (2.9.0 available) characters 1.2.0 (1.2.1 available) clock 1.1.0 (1.1.1 available) fake_async 1.3.0 (1.3.1 available) + flex_color_scheme 5.1.0 matcher 0.12.11 (0.12.12 available) material_color_utilities 0.1.4 (0.1.5 available) meta 1.7.0 (1.8.0 available) path 1.8.1 (1.8.2 available) source_span 1.8.2 (1.9.0 available) string_scanner 1.1.0 (1.1.1 available) term_glyph 1.2.0 (1.2.1 available) test_api 0.4.9 (0.4.12 available) Changed 1 dependency!m
Paket ini ditujukan khusus untuk memberikan skema warna default yang menarik bagi aplikasi. Lihat dokumentasi flex_color_scheme
untuk memahami berbagai macam kemampuan.
$ flutter pub add go_router Resolving dependencies... async 2.9.0 (2.10.0 available) boolean_selector 2.1.0 (2.1.1 available) collection 1.16.0 (1.17.0 available) + flutter_web_plugins 0.0.0 from sdk flutter + go_router 5.2.0 + js 0.6.4 (0.6.5 available) + logging 1.1.0 matcher 0.12.12 (0.12.13 available) material_color_utilities 0.1.5 (0.2.0 available) source_span 1.9.0 (1.9.1 available) stack_trace 1.10.0 (1.11.0 available) stream_channel 2.1.0 (2.1.1 available) string_scanner 1.1.1 (1.2.0 available) test_api 0.4.12 (0.4.16 available) vector_math 2.1.2 (2.1.4 available) Changed 4 dependencies!
Untuk mengimplementasikan navigasi di antara berbagai layar, tambahkan go_router ke project.
Paket ini menyediakan API berbasis URL yang mudah digunakan untuk bernavigasi menggunakan Router Flutter.
Mengonfigurasi aplikasi seluler untuk url_launcher
Plugin url_launcher
memerlukan konfigurasi aplikasi runner Android dan iOS. Di runner Flutter iOS, tambahkan baris berikut ke kamus plist
.
ios/Runner/Info.plist
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
<string>http</string>
<string>tel</string>
<string>mailto</string>
</array>
Di runner Flutter Android, tambahkan baris berikut ke Manifest.xml
. Tambahkan node queries
sebagai turunan langsung dari node manifest
dan pembanding dari node application
.
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>
Untuk detail selengkapnya tentang perubahan konfigurasi yang diperlukan ini, lihat dokumentasi url_launcher
.
Mengakses YouTube Data API
Untuk mengakses YouTube Data API guna mencantumkan playlist, Anda perlu membuat project API untuk menghasilkan Kunci API yang diperlukan. Langkah-langkah berikut mengasumsikan bahwa Anda telah memiliki Akun Google. Jika belum punya, Anda harus membuatnya terlebih dahulu.
Buka Konsol Developer untuk membuat project API:
Setelah memiliki project, buka halaman Library API. Di kotak penelusuran, masukkan "youtube", lalu pilih youtube data api v3.
Di halaman detail YouTube Data API v3, aktifkan API tersebut.
Setelah API diaktifkan, buka halaman Kredensial, lalu buat Kunci API.
Setelah beberapa detik, Anda akan melihat dialog dengan Kunci API baru Anda. Anda akan segera menggunakan kunci ini.
Menambahkan kode
Untuk sisa langkah ini, Anda akan memotong dan menempelkan banyak kode untuk membangun aplikasi seluler, tanpa komentar apa pun pada kode tersebut. Tujuan codelab ini adalah untuk mengadaptasikan aplikasi seluler dengan desktop dan web. Untuk pengantar yang lebih mendetail tentang membangun aplikasi Flutter untuk perangkat seluler, lihat Write Your First Flutter App, part 1, part 2, dan Building beautiful UIs with Flutter.
Tambahkan file berikut, dimulai dengan objek status untuk aplikasi.
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();
} 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));
}
}
Berikutnya, tambahkan halaman detail playlist satu per satu.
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).backgroundColor],
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.bodyText1!.copyWith(
fontSize: 18,
// fontWeight: FontWeight.bold,
),
),
if (playlistItem.snippet!.videoOwnerChannelTitle != null)
Text(
playlistItem.snippet!.videoOwnerChannelTitle!,
style: Theme.of(context).textTheme.bodyText2!.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,
),
),
],
);
}
}
Berikutnya, tambahkan daftar playlist.
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(),
);
},
),
);
},
);
}
}
Kemudian, ganti konten file main.dart
sebagai berikut:
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.queryParams['title']!;
final id = state.params['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,
);
}
}
Anda hampir siap untuk menjalankan kode ini di Android dan iOS. Satu hal lagi yang perlu dilakukan, yaitu ubah konstan youTubeApiKey
di baris 14 dengan Kunci YouTube API yang dihasilkan pada langkah sebelumnya.
lib/main.dart
// TODO: Replace with your YouTube API Key
const youTubeApiKey = 'AIzaNotAnApiKey';
Untuk menjalankan aplikasi ini di macOS, Anda harus memungkinkan aplikasi membuat permintaan HTTP sebagai berikut. Edit file DebugProfile.entitlements
dan Release.entitilements
sebagai berikut:
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>
Menjalankan aplikasi
Setelah aplikasi selesai, Anda akan dapat menjalankannya di Android emulator atau simulator iPhone. Anda akan melihat daftar playlist Flutter. Saat Anda memilih playlist, video di playlist tersebut akan ditampilkan. Terakhir, jika Anda mengklik tombol Putar, Anda akan diarahkan ke antarmuka YouTube untuk menonton video tersebut.
Namun, jika Anda mencoba menjalankan aplikasi ini di desktop, tata letak akan terlihat janggal saat diperluas ke jendela dengan ukuran desktop normal. Anda akan mempelajari cara mengadaptasikan hal ini di langkah berikutnya.
5. Beradaptasi dengan desktop
Masalah desktop
Jika Anda menjalankan aplikasi di salah satu platform desktop native seperti Windows, macOS, atau Linux, Anda akan mendapati masalah yang menarik. Aplikasi tersebut akan tetap berfungsi, tetapi terlihat janggal.
Anda dapat memperbaikinya dengan menambahkan tampilan terpisah, yang mencantumkan playlist di sebelah kiri dan video di sebelah kanan. Namun, sebaiknya hanya gunakan tata letak semacam ini saat kode tidak berjalan di Android atau iOS, dan saat jendela cukup lebar. Berikut petunjuk cara mengimplementasikan kemampuan ini.
Pertama, tambahkan paket split_view
untuk membantu dalam penyusunan tata letak.
$ flutter pub add split_view Resolving dependencies... + split_view 3.1.0 test_api 0.4.3 (0.4.8 available) Changed 1 dependency!
Memperkenalkan widget adaptif
Pola yang akan Anda gunakan dalam codelab ini adalah memperkenalkan widget Adaptif yang membuat pilihan implementasi berdasarkan atribut seperti lebar layar, tema platform, dan sejenisnya. Dalam hal ini, Anda akan memperkenalkan widget AdaptivePlaylists
yang mengubah cara Playlists
dan PlaylistDetails
berinteraksi. Edit file lib/main.dart
sebagai berikut:
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';
import 'src/app_state.dart';
import 'src/playlist_details.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 AdaptivePlaylists();
},
routes: <RouteBase>[
GoRoute(
path: 'playlist/:id',
builder: (context, state) {
final title = state.queryParams['title']!;
final id = state.params['id']!;
return Scaffold(
appBar: AppBar(title: Text(title)),
body: 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,
);
}
}
Berikutnya, buat file untuk widget AdaptivePlaylist:
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: selectedPlaylist == null
? const Text('FlutterDev Playlists')
: Text('FlutterDev Playlist: ${selectedPlaylist!.snippet!.title!}'),
),
body: SplitView(
viewMode: SplitViewMode.Horizontal,
children: [
Playlists(playlistSelected: (playlist) {
setState(() {
selectedPlaylist = playlist;
});
}),
if (selectedPlaylist != null)
PlaylistDetails(
playlistId: selectedPlaylist!.id!,
playlistName: selectedPlaylist!.snippet!.title!)
else
const Center(
child: Text('Select a playlist'),
),
],
),
);
}
}
File ini menarik karena berbagai alasan. Pertama, file ini menggunakan lebar jendela (dengan MediaQuery.of(context).size.width
) dan memeriksa tema (dengan Theme.of(context).platform
) untuk menentukan apakah akan menampilkan tata letak lebar dengan widget SplitView
, atau tata letak sempit tanpa widget tersebut.
Poin kedua yang perlu diperhatikan adalah file ini menangani navigasi yang sebelumnya di-hardcode. Hal ini dilakukan dengan menampilkan argumen callback di widget Playlists
yang memberi tahu kode di sekitarnya bahwa pengguna telah memilih playlist, dan perlu melakukan tindakan yang diperlukan untuk menampilkan playlist tersebut. Perlu diperhatikan juga bahwa sekarang Scaffold
telah dikecualikan dari widget Playlists
dan PlaylistDetails
karena widget ini bukan widget level atas.
Berikutnya, edit file src/lib/playlists.dart
sebagai berikut:
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);
},
),
);
},
);
}
}
Ada banyak perubahan pada file ini. Selain pengenalan callback playlistSelected serta penghapusan widget Scaffold
yang disebutkan sebelumnya, widget _PlaylistsListView
dikonversi dari stateless ke stateful. Perubahan ini diperlukan karena pengenalan ScrollController
yang dimiliki harus dibuat dan dihancurkan.
Pengenalan ScrollController
menarik. Pengenalan ini diperlukan karena pada tata letak lebar, Anda memiliki dua widget ListView
yang berdampingan. Di ponsel, biasanya ada satu ListView
. Oleh karena itu, mungkin ada satu ScrollController yang aktif dalam waktu lama di mana semua ListView
dipasang dan dilepas selama siklus prosesnya masing-masing. Sebaliknya, desktop itu berbeda karena dapat memiliki beberapa ListView
yang berdampingan.
Terakhir, edit file lib/src/playlist_details.dart sebagai berikut:
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).backgroundColor],
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.bodyText1!.copyWith(
fontSize: 18,
// fontWeight: FontWeight.bold,
),
),
if (playlistItem.snippet!.videoOwnerChannelTitle != null)
Text(
playlistItem.snippet!.videoOwnerChannelTitle!,
style: Theme.of(context).textTheme.bodyText2!.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,
),
),
],
);
}
}
Serupa dengan widget Playlists
di atas, file ini juga memiliki beberapa perubahan terkait penghapusan widget Scaffold
, dan pengenalan ScrollController
yang dimiliki.
Menjalankan kembali aplikasi
Menjalankan aplikasi di desktop pilihan Anda, baik itu Windows, macOS, atau Linux. Aplikasi tersebut kini seharusnya dapat berfungsi seperti yang diharapkan.
6. Beradaptasi dengan web
Ada apa dengan gambar ini?
Jika Anda mencoba menjalankan aplikasi ini di web, bahkan dengan perubahan tata letak dari langkah sebelumnya, Anda akan menemukan bahwa masih ada pekerjaan yang harus dilakukan untuk beradaptasi dengan lingkungan browser web:
Jika Anda membuka konsol debug, Anda akan melihat petunjuk tentang tindakan yang harus dilakukan berikutnya.
════════ Exception caught by image resource service ════════════════════════════ Failed to load network image. Image URL: https://i.ytimg.com/vi/QIW35-vcA2o/default.jpg Trying to load an image from another domain? Find answers at: https://flutter.dev/docs/development/platform-integration/web-images ═════════════════════════════════════════════════════════════════════════
Membuat Proxy CORS
Salah satu cara untuk menangani masalah rendering gambar adalah dengan memperkenalkan layanan web proxy untuk menambahkan header Cross-Origin Resource Sharing (CORS) yang diperlukan. Buka terminal dan buat server web Dart sebagai berikut:
$ 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
Ubah direktori ke server yt_cors_proxy
, lalu tambahkan beberapa dependensi yang diperlukan:
$ cd yt_cors_proxy $ dart pub add shelf_cors_headers Resolving dependencies... + shelf_cors_headers 0.1.2 Changed 1 dependency! $ dart pub add http "http" was found in dev_dependencies. Removing "http" and adding it to dependencies instead. Resolving dependencies... Got dependencies!
Ada beberapa dependensi yang saat ini tidak lagi diperlukan. Pangkas dependensi tersebut sebagai berikut:
$ dart pub remove args Resolving dependencies... These packages are no longer being depended on: - args 2.2.0 Changed 1 dependency! $ dart pub remove shelf_router Resolving dependencies... These packages are no longer being depended on: - http_methods 1.1.0 - shelf_router 1.1.1 Changed 2 dependencies!
Berikutnya, ubah konten file server.dart agar sesuai dengan berikut ini:
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}');
}
Anda dapat menjalankan server ini sebagai berikut:
$ dart run bin/server.dart Server listening on port 8080
Atau, Anda dapat membangunnya sebagai image Docker, dan menjalankan image Docker yang dihasilkan sebagai berikut:
$ 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
Berikutnya, ubah kode Flutter untuk memanfaatkan proxy CORS ini, tetapi hanya saat kode berjalan di browser web.
Sepasang widget yang dapat diadaptasikan
Widget pertama menentukan cara aplikasi Anda menggunakan proxy CORS.
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);
}
}
Perlu diperhatikan bahwa Anda menggunakan kIsWeb
karena perbedaannya bukan pada tema, melainkan pada platform runtime. Widget lainnya yang dapat diadaptasikan menangani teks agar dapat dipilih, sesuai dengan harapan pengguna browser web.
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) {
switch (Theme.of(context).platform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
return Text(data, style: style);
default:
return SelectableText(data, style: style);
}
}
}
Sekarang, terapkan adaptasi ini ke seluruh codebase:
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).backgroundColor],
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( // This line
playlistItem.snippet!.title!,
style: Theme.of(context).textTheme.bodyText1!.copyWith(
fontSize: 18,
// fontWeight: FontWeight.bold,
),
),
if (playlistItem.snippet!.videoOwnerChannelTitle != null)
AdaptiveText( // And, this line
playlistItem.snippet!.videoOwnerChannelTitle!,
style: Theme.of(context).textTheme.bodyText2!.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,
),
),
],
);
}
}
Pada kode di atas, Anda mengadaptasikan widget Image.network
dan Text
. Berikutnya, adaptasikan widget Playlists
.
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);
},
),
);
},
);
}
}
Kali ini, Anda hanya mengadaptasikan widget Image.network
, dan membiarkan kedua widget Text
seperti sebelumnya. Tindakan ini disengaja karena, jika Anda mengadaptasikan widget Text, fungsi onTap
ListTile
akan diblokir saat pengguna mengetuk teks tersebut.
Menjalankan aplikasi di web dengan benar
Dengan proxy CORS berjalan, Anda seharusnya dapat menjalankan versi web aplikasi dan mendapatkan tampilan seperti ini:
7. Autentikasi Adaptif
Pada langkah ini, Anda akan memperluas kemampuan aplikasi agar dapat mengautentikasi pengguna, dan menampilkan playlist mereka. Anda harus menggunakan beberapa plugin untuk mencakup berbagai platform yang dapat menjalankan aplikasi. Hal ini karena cara penanganan OAuth sangat berbeda antara Android, iOS, web, Windows, macOS, dan Linux.
Menambahkan plugin untuk mengaktifkan autentikasi Google
Anda akan menginstal tiga paket untuk menangani autentikasi Google.
$ flutter pub add googleapis_auth Resolving dependencies... + crypto 3.0.1 + googleapis_auth 1.3.0 test_api 0.4.3 (0.4.8 available) Changed 2 dependencies! $ flutter pub add google_sign_in Resolving dependencies... + google_sign_in 5.2.1 + google_sign_in_platform_interface 2.1.0 + google_sign_in_web 0.10.0+3 + quiver 3.0.1+1 test_api 0.4.3 (0.4.8 available) Changed 4 dependencies! $ flutter pub add extension_google_sign_in_as_googleapis_auth Resolving dependencies... + extension_google_sign_in_as_googleapis_auth 2.0.4 test_api 0.4.3 (0.4.8 available) Changed 1 dependency!
Gunakan plugin googleapis_auth
untuk melakukan autentikasi di Windows, macOS, dan Linux menggunakan browser web pengguna. Di Android, iOS, dan web, gunakan google_sign_in
, dengan extension_google_sign_in_as_googleapis_auth
bertindak sebagai shim interop antara dua paket.
Mengupdate kode
Mulai update dengan membuat abstraksi baru yang dapat digunakan kembali, yaitu widget AdaptiveLogin. Widget ini didesain untuk Anda gunakan kembali, sehingga memerlukan beberapa konfigurasi:
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(),
),
);
}
}
Banyak konfigurasi yang dilakukan pada file ini. Bagian pentingnya adalah di metode build
AdaptiveLogin
, platform runtime diperiksa menggunakan kombinasi panggilan kIsWeb
dan Platform.isXXX
dart:io
, yang membuat instance widget stateful _GoogleSignInLogin
untuk Android, iOS, dan web; dan membuat widget stateful _GoogleApisAuthLogin
untuk Windows, macOS, dan Linux.
Konfigurasi tambahan diperlukan untuk menggunakan beberapa class ini, yang akan dilakukan nanti setelah mengupdate code base lainnya untuk menggunakan widget baru ini. Mulai dengan mengganti nama FlutterDevPlaylists
menjadi AuthedUserPlaylists
agar lebih mencerminkan tujuan barunya, serta mengupdate kode untuk mencerminkan bahwa http.Client
kini diteruskan setelah dibuat. Terakhir, class _ApiKeyClient
tidak lagi diperlukan:
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();
} while (nextPageToken != null);
}
YouTubeApi? _api; // Convert from late final 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
Berikutnya, update widget PlaylistDetails
dengan nama baru untuk objek status aplikasi yang disediakan:
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, flutterDev, _) {
final playlistItems = flutterDev.playlistItems(playlistId: playlistId);
if (playlistItems.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return _PlaylistDetailsListView(playlistItems: playlistItems);
},
);
}
}
Demikian juga, update widget Playlists
:
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,
);
},
);
}
}
Terakhir, update file main.dart
untuk menggunakan widget AdaptiveLogin
baru dengan benar:
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.queryParams['title']!;
final id = state.params['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(
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 if you'd prefer
debugShowCheckedModeBanner: false,
routerConfig: _router,
);
}
}
Perubahan pada file ini mencerminkan perubahan dari hanya menampilkan playlist YouTube Flutter hingga menampilkan playlist pengguna yang telah diautentikasi. Meskipun kode sudah selesai, masih ada serangkaian perubahan yang diperlukan pada file ini, dan file di setiap aplikasi Runner, guna mengonfigurasi paket google_sign_in
dan googleapis_auth
dengan benar untuk autentikasi.
Mengonfigurasi googleapis_auth
Langkah pertama untuk mengonfigurasi autentikasi adalah menghapus Kunci API yang sebelumnya Anda konfigurasi dan gunakan. Buka halaman kredensial project API Anda, lalu hapus kunci API tersebut:
Tindakan ini akan memunculkan pop-up yang Anda konfirmasi dengan menekan tombol Delete:
Kemudian, buat client ID OAuth:
Untuk Application type, pilih Desktop app.
Terima namanya, lalu klik Create.
Tindakan ini akan membuat Client ID dan Rahasia Klien yang harus Anda tambahkan ke lib/main.dart
untuk mengonfigurasi alur googleapis_auth
. Detail implementasi yang penting adalah alur googleapis_auth menggunakan server web sementara yang berjalan di localhost untuk menangkap token OAuth yang dihasilkan. Untuk macOS, Anda perlu mengubah file macos/Runner/Release.entitlements
:
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>
Anda tidak perlu melakukan perubahan ini pada file macos/Runner/DebugProfile.entitlements
karena file tersebut telah memiliki hak untuk com.apple.security.network.server
guna mengaktifkan Hot Reload dan alat debug Dart VM.
Sekarang Anda dapat menjalankan aplikasi di Windows, macOS, atau Linux (jika aplikasi Anda telah dikompilasi untuk target tersebut).
Mengonfigurasi google_sign_in
untuk Android
Kembalilah ke halaman kredensial project API Anda, lalu buat client ID OAuth lainnya. Untuk kali ini, pilih Android:
Untuk kolom lainnya dalam formulir, isi Package name (Nama paket) dengan paket yang dideklarasikan di android/app/src/main/AndroidManifest.xml
. Jika Anda telah mengikuti petunjuk dengan benar, nama paket seharusnya adalah com.example.adaptive_app
. Ekstrak sidik jari sertifikat SHA-1 menggunakan petunjuk dari halaman bantuan Google Cloud Platform Console:
Tindakan ini cukup untuk membuat aplikasi berfungsi di Android. Bergantung pada pilihan Google API yang Anda gunakan, Anda mungkin perlu menambahkan file JSON yang dihasilkan ke paket aplikasi Anda.
Mengonfigurasi google_sign_in
untuk iOS
Kembalilah ke halaman kredensial project API Anda, lalu buat client ID OAuth lainnya. Untuk kali ini, pilih iOS:
.
Untuk kolom lainnya dalam formulir, isi Bundle Identifier (ID Paket) dengan membuka ios/Runner.xcworkspace
di Xcode. Buka Navigator Project, pilih Runner di navigator, lalu pilih tab General, dan salin Bundle Identifier (ID Paket). Jika Anda telah mengikuti codelab ini langkah demi langkah, ID paket seharusnya adalah com.example.adaptiveApp
.
Untuk saat ini, abaikan App Store ID dan Team ID karena ID tersebut tidak diperlukan untuk pengembangan lokal:
Download file .plist
yang dihasilkan. Namanya didasarkan pada client ID yang dihasilkan. Ganti nama file yang didownload menjadi GoogleService-Info.plist
, lalu tarik file tersebut ke editor Xcode yang sedang berjalan, bersama dengan file Info.plist
di bagian Runner/Runner
di sebelah kiri navigator. Untuk dialog opsi di Xcode, pilih Copy items if needed, Create folder references, dan Runner di bagian Add to targets.
Keluar dari Xcode, lalu di pilihan IDE Anda, tambahkan berikut ini ke Info.plist
Anda:
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>
Anda perlu mengedit nilai agar sesuai dengan entri di file GoogleService-Info.plist
yang dihasilkan. Anda juga harus menetapkan versi iOS minimum ke 9. Edit ios/Podfile
sebagai berikut:
ios/Podfile
# iOS 9 for google_sign_in
platform :ios, '9.0' # Uncomment this line
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
Jalankan aplikasi Anda. Setelah login, playlist Anda akan muncul.
Mengonfigurasi google_sign_in
untuk web
Kembalilah ke halaman kredensial project API Anda, lalu buat client ID OAuth lainnya. Untuk kali ini, pilih Web application:
Untuk kolom lainnya dalam formulir, isi Authorized JavaScript origins (origin JavaScript yang Diotorisasi) sebagai berikut:
Tindakan ini akan menghasilkan Client ID. Tambahkan tag meta
berikut ke web/index.html
, yang diupdate agar menyertakan Client ID yang dihasilkan.
web/index.html
<meta name="google-signin-client_id" content="YOUR_GOOGLE_SIGN_IN_OAUTH_CLIENT_ID.apps.googleusercontent.com">
Anda memerlukan sedikit bantuan untuk menjalankan sampel ini. Anda perlu mengikuti petunjuk berikut untuk menjalankan proxy CORS yang Anda buat di langkah sebelumnya, serta untuk menjalankan aplikasi web Flutter pada port yang ditentukan dalam formulir client ID OAuth Aplikasi web.
Di satu terminal, jalankan server Proxy CORS sebagai berikut:
$ dart run bin/server.dart Server listening on port 8080
Di terminal lainnya, jalankan aplikasi Flutter sebagai berikut:
$ 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".
Setelah login sekali lagi, playlist Anda akan muncul:
8. Langkah berikutnya
Selamat!
Anda telah menyelesaikan codelab ini dan membangun aplikasi Flutter adaptif yang berjalan di keenam platform yang didukung Flutter. Anda telah mengadaptasikan kode untuk menangani berbagai perbedaan dalam cara menentukan tata letak layar, cara berinteraksi dengan teks, cara memuat gambar, dan cara kerja autentikasi.
Ada banyak hal lainnya yang dapat diadaptasikan di aplikasi Anda. Untuk mempelajari cara lain guna mengadaptasikan kode Anda dengan berbagai lingkungan tempat kode tersebut dijalankan, lihat Building adaptive apps.