במדריך הזה מוסבר איך להשתמש ב-YouTube Playables SDK עם אפליקציית אינטרנט של Flutter.
הגדרת Flutter
כדי שאפליקציית האינטרנט שמוגדרת כברירת מחדל ב-Flutter תפעל כ-Playable, צריך לבצע בה כמה שינויים. השינויים הבאים נדרשים החל מגרסה 3.24.5 של Flutter.
כברירת מחדל, Flutter מוגדר לטעון משאבים מספריית הבסיס, ואילו ב-Playables המשאבים נטענים ביחס לנקודת הכניסה. השגיאה הזו תופיע כקוד שגיאה 404, או כקוד שגיאה שמופיע במסוף JavaScript עם הכיתוב: Refused to execute script
from... because its MIME type ('text/html') is not executable.
אחת מהדרכים לשנות את ההגדרה היא להסיר את התג הבא מקובץ index.html:
<base href="$FLUTTER_BASE_HREF">
כברירת מחדל, Flutter טוען ספריות מסוימות באופן דינמי במקום להטמיע אותן באפליקציה. CanvasKit הוא דוגמה לכך. השגיאה תוצג בלוח JavaScript: Refused to connect to '...' because it
violates the following Content Security Policy directive: "connect-src 'self'
blob: data:".
יכול להיות ש-Flutter גם יפיק יומנים נוספים כשהטעינה נכשלת, כמו Failed to download any of the following CanvasKit URLs
. כדי לפתור את הבעיה, אפשר ליצור את אפליקציית האינטרנט עם הדגל הנוסף הבא:
$ flutter build web --no-web-resources-cdn
כברירת מחדל, אפליקציות אינטרנט של Flutter טוענות גופנים באופן דינמי במקום להטמיע אותם באפליקציה. באפליקציית Flutter שמוגדרת כברירת מחדל נעשה שימוש בגופן Roboto, והוא יופיע כשגיאה במסוף JavaScript עם הכיתוב: Refused to connect to '...'
because it violates the following Content Security Policy directive:
"connect-src 'self' blob: data".
כדי לפתור את הבעיה, אפשר לבצע את הפעולות הבאות:
- יוצרים תיקייה בשם 'fonts' בספריית השורש של אפליקציית האינטרנט ב-Flutter.
- מורידים את משפחת הגופנים Roboto מהאתר fonts.google.com.
- לחלץ את הגופנים ולהעתיק את Roboto-Regular.ttf לתיקיית הגופנים
- מוסיפים את השורה הבאה לקובץ pubspec.yaml של אפליקציית האינטרנט ב-Flutter:
fonts:
- family: Roboto
fonts:
- asset: fonts/Roboto-Regular.ttf
מידע נוסף על הגדרת גופנים זמין במסמכי העזרה של Flutter.
שילוב SDK
אפשר להשתמש ב-YouTube Playables SDK ממשחק אינטרנט ב-Flutter באמצעות מעטפת JS-interop דומה לזו:
ytgame.dart
// Copyright 2023 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@JS()
library ytgame_api;
import 'package:js/js.dart';
import 'package:js/js_util.dart' as js_util;
enum PlayablesErrorType {
unknown('UNKNOWN'),
apiUnavailable('API_UNAVAILABLE'),
invalidParams('INVALID_PARAMS'),
sizeLimitExceeded('SIZE_LIMIT_EXCEEDED');
const PlayablesErrorType(String type) : _type = type;
final String _type;
@override
String toString() => _type;
static PlayablesErrorType fromString(String errorType) {
return values.firstWhere(
(element) => element._type == errorType,
);
}
}
@JS()
@staticInterop
class PlayablesError {}
extension PlayablesErrorExtension on PlayablesError {
@JS('errorType')
external String get _errorType;
PlayablesErrorType get errorType {
return PlayablesErrorType.fromString(_errorType);
}
}
@JS()
@anonymous
@staticInterop
class PlayablesScore {
external factory PlayablesScore({
required num value,
});
}
extension PlayablesScoreExtension on PlayablesScore {
external num get value;
}
// engagement
@JS()
@staticInterop
class PlayablesEngagement {}
extension PlayablesEngagementExtension on PlayablesEngagement {
@JS('sendScore')
external Object _sendScore(PlayablesScore score);
Future<void> sendScore(PlayablesScore score) {
return js_util.promiseToFuture(_sendScore(score));
}
}
// game
@JS()
@staticInterop
class PlayablesGame {}
extension PlayablesGameExtension on PlayablesGame {
external void firstFrameReady();
external void gameReady();
@JS('loadData')
external Object _loadData();
Future<String?> loadData() {
return js_util.promiseToFuture<String?>(_loadData());
}
@JS('saveData')
external Object _saveData(String? data);
Future<void> saveData(String? data) {
return js_util.promiseToFuture<void>(_saveData(data));
}
}
// health
@JS()
@staticInterop
class PlayablesHealth {}
extension PlayablesHealthExtension on PlayablesHealth {
external void logError();
external void logWarning();
}
// system
typedef PlayablesUnsetCallback = void Function();
typedef PlayablesOnAudioEnabledChange = void Function(bool isAudioEnabled);
typedef PlayablesOnPause = void Function();
typedef PlayablesOnResume = void Function();
@JS()
@staticInterop
class PlayablesSystem {}
extension PlayablesSystemExtension on PlayablesSystem {
external bool isAudioEnabled();
@JS('onAudioEnabledChange')
external PlayablesUnsetCallback _onAudioEnabledChange(
PlayablesOnAudioEnabledChange callback);
PlayablesUnsetCallback onAudioEnabledChange(
PlayablesOnAudioEnabledChange callback) {
return _onAudioEnabledChange(allowInterop(callback));
}
@JS('onPause')
external PlayablesUnsetCallback _onPause(PlayablesOnPause callback);
PlayablesUnsetCallback onPause(PlayablesOnPause callback) {
return _onPause(allowInterop(callback));
}
@JS('onResume')
external PlayablesUnsetCallback _onResume(PlayablesOnResume callback);
PlayablesUnsetCallback onResume(PlayablesOnResume callback) {
return _onResume(allowInterop(callback));
}
}
@JS()
@staticInterop
class Playables {}
extension PlayablesExtension on Playables {
@JS('SDK_VERSION')
external String get sdkVersion;
external PlayablesEngagement get engagement;
external PlayablesGame get game;
external PlayablesHealth get health;
external PlayablesSystem get system;
}
@JS('ytgame')
external Playables get ytgame;
שימוש
- פועלים לפי ההוראות להגדרה והפעלה של ה-SDK לאינטרנט.
- משנים את
web/index.html
של אפליקציית Flutter ל-Web כך שיכלול את תגי הסקריפט הנדרשים.
- משנים את
- מוסיפים עותק של
ytgame.dart
לקובץsrc
של אפליקציית האינטרנט ב-Flutter. - מוודאים שחבילת ה-js נוספה ל-Flutter, למשל על ידי הפעלת הפקודה
flutter pub add js
. - מוסיפים
import 'src/location/of/ytgame.dart';
לפי הצורך. - משתמשים באובייקט
ytgame
כדי לגשת ל-SDK.
דוגמאות
בהמשך מפורטות כמה דוגמאות ל-Dart API בפעולה.
firstFrameReady
וגם gameReady
כדי שהמשחק יתחיל כראוי, צריך להפעיל את firstFrameReady
כשהפריים הראשון מוכן להצגה, ואת gameReady
כשהמשחק מוכן לאפשר אינטראקציה. אחת מהדרכים לעשות זאת באפליקציית Flutter שמוגדרת כברירת מחדל היא להוסיף את הקריאות לקובץ main.dart:
...
import 'src/ytgame.dart';
...
void main() {
ytgame.game.firstFrameReady();
ytgame.game.gameReady();
runApp(const MyApp());
}
חשוב לזכור שזו רק דוגמה להפעלת המשחק – תצטרכו למקם את הקריאות האלה בצורה נכונה כדי לבצע הטמעה תקינה.
sendScore
בדוגמה הזו מוצגת הטמעה של sendScore(int points)
ב-Dart:
...
import 'src/ytgame.dart';
...
/// Sends [points] as score to YT Playables
Future<void> sendScore(int points) async {
// Create a score object...
final PlayablesScore score = PlayablesScore(
value: points,
);
// Submit the score to ytgame
await ytgame.engagement.sendScore(score);
}
onPause
זו דוגמה לאופן שבו משחק יכול להאזין לאירועים מסוג Pause
שמגיעים מ-YT Playables, כדי להשהות את המנוע שלו במקרה הצורך:
...
import 'src/ytgame.dart';
...
/// The instance of your game.
late Game myGame;
/// Callback to stop listening for Pause events (cleanup).
PlayablesUnsetCallback? _unsetOnPause;
...
/// Sets a [callback] to respond to Pause events.
void registerOnPauseListener(PlayablesOnPause callback) {
_unsetOnPause = ytgame.system.onPause(callback);
}
/// Stops listening to Pause events, and clears [_unsetOnPause].
void unsetOnPauseListener() {
if (_unsetOnPause != null) {
_unsetOnPause!();
_unsetOnPause = null;
}
}
...
/// Initialization for your game
void initGame() {
...
myGame = ...
registerOnPauseListener(_onPause);
}
void _onPause() {
// This is called when a Pause event is received from YT Playables.
myGame.pauseEngine();
}
/// Teardown for your game
void disposeGame() {
...
unsetOnPauseListener();
}