本トピックでは、AliPlayerKit イベントシステムのコアコンセプト、イベントカテゴリ、使用方法、ベストプラクティスについて説明します。
コンセプト
イベントとは
イベントとは、プレーヤーの実行中に発生する状態変化または動作の通知です。各イベントには、その発生元を識別し、複数のプレーヤーインスタンス間でイベントを隔離するために、プレーヤー ID (playerId) が含まれます。
AliPlayerKit のイベントは、主に次の 2 つのカテゴリに分類されます:
タイプ | 説明 | 例 |
ステータスイベント | プレーヤー内部で生成される、プレーヤーの状態変化の通知です。 |
|
コマンドイベント | UI または外部コンポーネントによって生成される、プレーヤーの動作を制御するための命令です。 |
|
すべてのイベントは、イベント構造の一貫性を確保するために、基底クラス PlayerEvent を継承します。
イベントシステムとは
イベントシステムとは、イベントのサブスクリプションとパブリッシュを管理するための通信アーキテクチャです。コアコンポーネントは PlayerEventBus です。
次の機能を備えています:
型安全なサブスクリプション: イベントタイプでサブスクライブできます。型安全性はコンパイル時に保証されます。
スレッドセーフ: マルチスレッド環境でのイベントのパブリッシュとサブスクリプションをサポートします。
弱い参照の管理: 弱い参照を使用してリスナーを保持し、ガベージコレクションされたリスナーを自動的にクリーンアップします。
イベントの隔離:
playerIdを使用して、複数のプレーヤーインスタンス間でイベントを隔離します。
イベントシステムは、UI コンポーネント (Slots) とコントローラーを完全にデカップリングします。UI はコマンドを送信し、状態変化をサブスクライブするだけで、コマンドがどのように実行されるかを意識する必要はありません。コントローラーはコマンドを処理し、状態変化をパブリッシュするだけで、誰がリッスンしているかを意識する必要はありません。
機能
課題の解決
コンポーネントの疎結合:コンポーネント間の直接的な依存関係を排除し、カップリングを削減することで、交換可能性とテスト容易性を向上させます。
イベントトレーサビリティ:マルチプレイヤーシナリオにおけるイベントソースの混乱を解消します。
責務の分離:UI コンポーネントとビジネスロジックを分離し、責務の境界を明確にします。
コアバリュー
イベントシステムは、コンポーネント間の通信を標準化します。開発者は次の方法で使用できます:
使用方法 | 説明 | 利点 |
イベントをサブスクライブする | プレーヤーの状態変化をリッスンします。 | UI をビジネスロジックから疎結合化し、リアクティブに UI を更新します。 |
コマンドを送信する | プレーヤーの動作をコントロールします。 | UI はコントローラー参照を保持する必要がなく、コマンドベースで対話します。 |
イベントをカスタマイズする | イベントタイプを拡張します。 | アーキテクチャの一貫性を保ちながら、特定のビジネスシナリオに対応します。 |
アーキテクチャ上の利点:
疎結合:コンポーネントはイベントを通じて通信し、直接的な依存関係を持ちません。
テスト容易性:イベントでやり取りをシミュレートすることで、コンポーネントを独立してテストできます。
拡張性:フレームワーク コードを変更することなく、カスタムイベントタイプを追加できます。
スレッドセーフ:追加の同期なしで、マルチスレッド環境で動作します。
コア機能
機能 | 説明 |
型安全なサブスクリプション | イベントタイプごとにサブスクライブします。型チェックはコンパイル時に行われます。 |
弱い参照の管理 | ガベージコレクションされたリスナーを自動的にクリーンアップし、メモリリークを防ぎます。 |
イベントの継承のサポート | 親イベントタイプをサブスクライブすると、すべての子イベントタイプを受信します。 |
マルチプレイヤーの隔離 |
|
イベントカテゴリ
プレーヤーイベント (PlayerEvents)
これらは、プレーヤーの状態を外部コンポーネントに通知するためにプレーヤー内部で生成される状態変化イベントです。
イベント | 説明 | 付随データ |
| プレーヤーの準備が完了しました。 |
|
| 最初のフレームがレンダリングされました。 | - |
| 再生状態が変更されました。 |
|
| 動画サイズが変更されました。 |
|
| 再生進捗が更新されました。 |
|
| 再生エラーが発生しました。 |
|
| バッファリングが開始されました。 | - |
| バッファリングの進捗。 |
|
| バッファリングが終了しました。 | - |
| 再生速度の設定が完了しました。 |
|
| スクリーンショットが完了しました。 |
|
| ループ設定が完了しました。 |
|
| ミュート設定が完了しました。 |
|
| フィルモードの設定が完了しました。 |
|
| ミラー設定が完了しました。 |
|
| 回転設定が完了しました。 |
|
| 画質リストが更新されました。 |
|
| 画質の選択が完了しました。 |
|
プレーヤーコマンド (PlayerCommand)
これらは、プレーヤーの動作を制御するために使用されるコマンドイベントです。UI コンポーネントまたは外部コードが送信します。
コマンド | 説明 | パラメーター |
| 再生を開始します。 | - |
| 再生を一時停止します。 | - |
| 再生と一時停止を切り替えます。 | - |
| 再生を停止します。 | - |
| もう一度再生します。 | - |
| 指定した位置にシークします。 |
|
| 再生速度を設定します。 |
|
| スクリーンショットを撮ります。 | - |
| ループ再生を設定します。 |
|
| ミュートを設定します。 |
|
| フィルモードを設定します。 |
|
| ミラーを設定します。 |
|
| 回転を設定します。 |
|
| 画質を切り替えます。 |
|
ジェスチャーイベント (GestureEvents)
ユーザーのジェスチャー操作によって生成されるイベントです。これらのイベントはジェスチャー自体を記述し、ビジネスロジックは含みません。
イベント | 説明 | 付随データ |
| シングルタップが発生しました。 |
|
| ダブルタップが発生しました。 |
|
| 長押しが開始されました。 |
|
| 長押しが終了しました。 | - |
| 水平ドラッグが開始されました。 |
|
| 水平ドラッグが更新されました。 |
|
| 水平ドラッグが終了しました。 | - |
| 左側の垂直ドラッグが開始されました。 |
|
| 左側の垂直ドラッグが更新されました。 |
|
| 左側の垂直ドラッグが終了しました。 | - |
| 右側の垂直ドラッグが開始されました。 |
|
| 右側の垂直ドラッグが更新されました。 |
|
| 右側の垂直ドラッグが終了しました。 | - |
コントロールバーイベント (ControlBarEvents)
コントロールバーの表示状態を同期させるイベントです。
イベント | 説明 |
| コントロールバーを表示します。 |
| コントロールバーを非表示にします。 |
| 自動非表示タイマーをリセットします。 |
| 設定インターフェイスを表示します。 |
フルスクリーンイベント (FullscreenEvents)
これらは、フルスクリーンモードへの切り替え、またはフルスクリーンモードからの復帰のためのイベントです。
イベント | 説明 |
| フルスクリーンモードを切り替えます。 |
ライフサイクルイベント (PlayerLifecycleEvents)
これらは、プレーヤーのライフサイクルポリシーに関連するイベントです。これらを使用して、プレーヤーインスタンスの作成、再利用、破棄を監視できます。
イベント | 説明 |
| 新しいプレーヤーインスタンスが作成されました。 |
| プレーヤーインスタンスが破棄されました。 |
| アイドル状態のプレーヤーインスタンスが再利用されました。 |
| アクティブなプレーヤーインスタンスがヒットしました。 |
| プレーヤーインスタンスがエビクトされました。 |
スロットイベント (SlotEvents)
これらはスロット内部で生成されるイベントです。
イベント | 説明 |
| トップバーの戻るボタンがクリックされました。 |
基本的な使用法
イベントシステムは、次の 2 つの使用戦略をサポートします:
戦略 | 説明 | ユースケース |
イベントをサブスクライブ | プレーヤーの状態変化をリッスン。 | UI の更新、ビジネスロジックの応答。 |
コマンドを送信 | プレーヤーの動作をコントロール。 | ユーザー操作、外部からのコントロール。 |
戦略 1:イベントをサブスクライブ
イベントをサブスクライブするには、次の 3 つの手順を実行します:イベントバスを取得し、リスナーを作成して、イベントをサブスクライブします。
public class MyActivity extends AppCompatActivity {
private PlayerEventBus mEventBus;
private PlayerEventBus.EventListener<PlayerEvents.Info> mInfoListener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 1. シングルトンのイベントバスを取得します
mEventBus = PlayerEventBus.getInstance();
// 2. リスナーを作成します
mInfoListener = event -> {
// 再生進捗の更新を処理します
long position = event.currentPosition;
long duration = event.duration;
updateProgressUI(position, duration);
};
// 3. イベントをサブスクライブします
mEventBus.subscribe(PlayerEvents.Info.class, mInfoListener);
}
@Override
protected void onDestroy() {
super.onDestroy();
// 4. メモリリークを防ぐためにアンサブスクライブします
if (mEventBus != null && mInfoListener != null) {
mEventBus.unsubscribe(PlayerEvents.Info.class, mInfoListener);
}
}
} 戦略 2:コマンドを送信
イベントバスの post() メソッドでコマンドを送信できます:
// 1. シングルトンのイベントバスを取得します
PlayerEventBus eventBus = PlayerEventBus.getInstance();
// 2. ターゲットのプレーヤー ID を取得します
// [シナリオ A:スロット内] 組み込みの仕組みを使用します
// String playerId = getPlayerId();
// [シナリオ B:Activity またはフラグメント内] コントローラーから取得します
String playerId = mAliPlayerKitController.getPlayer().getPlayerId();
// 3. コマンドをポストします (この playerId に関連付けられたコントローラーが実行します)
eventBus.post(new PlayerCommand.Play(playerId));
// その他の一般的なコマンド例:
eventBus.post(new PlayerCommand.Pause(playerId)); // 再生を一時停止します
eventBus.post(new PlayerCommand.Toggle(playerId)); // 再生/一時停止を切り替えます
eventBus.post(new PlayerCommand.Seek(playerId, 30000)); // 30 秒へシークします
eventBus.post(new PlayerCommand.SetSpeed(playerId, 1.5f)); // 再生速度を 1.5 倍に設定します 高度な使用法
スロット内でのイベントの使用方法
スロット内でイベントを使用するには、イベントをサブスクライブする方法と、コマンドを送信する方法の2通りがあります。
方法1:イベントのサブスクライブ (推奨:observedEvents() の使用)
BaseSlot クラスには observedEvents() および onEvent() メソッドが用意されています。フレームワークがサブスクリプションのライフサイクルを自動的に管理します。
public class MySlot extends BaseSlot {
// 1. サブスクライブするイベントタイプを宣言します
@Override
protected List<Class<? extends PlayerEvent>> observedEvents() {
return Arrays.asList(
PlayerEvents.StateChanged.class,
PlayerEvents.Info.class
);
}
// 2. イベントコールバックを処理します
@Override
protected void onEvent(@NonNull PlayerEvent event) {
if (event instanceof PlayerEvents.StateChanged) {
PlayerEvents.StateChanged stateChanged = (PlayerEvents.StateChanged) event;
updatePlayPauseIcon(stateChanged.newState);
} else if (event instanceof PlayerEvents.Info) {
PlayerEvents.Info info = (PlayerEvents.Info) event;
updateProgress(info.currentPosition, info.duration);
}
}
} 方法2:コマンドの送信
スロット内では、postEvent() メソッドでコマンドを送信できます。
public class MySlot extends BaseSlot {
private void onPlayPauseClick() {
// playerId を取得します (onAttach の後に利用可能です)
String playerId = getPlayerId();
if (playerId != null) {
// 再生/一時停止を切り替えるコマンドを送信します
postEvent(new PlayerCommand.Toggle(playerId));
}
}
private void onSeekTo(long position) {
String playerId = getPlayerId();
if (playerId != null) {
// シークコマンドを送信します
postEvent(new PlayerCommand.Seek(playerId, position));
}
}
} スロットの使用方法の詳細については、「Slot system」をご参照ください。
ストラテジー内でのイベントの使用方法
ストラテジー内でのイベントの使用は、スロット内での使用と同様です。observedEvents() と onEvent() を使用して、イベントをサブスクライブし、処理できます。ストラテジーには次の特徴があります。
特徴 | 説明 |
主にモニタリング向け | ストラテジーは通常、分析のためにイベントをサブスクライブするのみで、コマンドは送信しません。 |
イベントのフィルタリング | マルチプレイヤーのシナリオでは、 |
コンテキストへのアクセス |
|
コード例:
public class MyAnalyticsStrategy extends BaseStrategy {
@Nullable @Override protected List<Class<? extends PlayerEvent>> observedEvents() {
return Arrays.asList(
PlayerEvents.StateChanged.class,
PlayerEvents.Info.class
);
}
@Override public void onEvent(@NonNull PlayerEvent event) {
// マルチプレイヤーのシナリオでクロストークを回避するため、イベントソースをフィルタリングする必要があります
if (!isCurrentPlayer(event)) return;
if (event instanceof PlayerEvents.StateChanged) {
// 状態変化を処理します
PlayerEvents.StateChanged stateChanged = (PlayerEvents.StateChanged) event;
logState(stateChanged.newState);
} else if (event instanceof PlayerEvents.Info) {
// 再生進捗を処理します
PlayerEvents.Info info = (PlayerEvents.Info) event;
trackProgress(info.currentPosition, info.duration);
}
}
}ストラテジーの使用方法の詳細については、「Strategy system」をご参照ください。
カスタムイベントの実装
ビジネス要件でカスタムイベントが必要な場合は、PlayerEvent を拡張して新しいイベントタイプを作成できます。
1. イベントクラスを作成します。
PlayerEventを拡張し、ビジネス固有のデータフィールドを追加します。/** * カスタム再生イベント */ public class MyCustomEvent extends PlayerEvent { /** * カスタムデータフィールド */ public final String customData; /** * コンストラクター * * @param playerId プレーヤー ID * @param customData カスタムデータ */ public MyCustomEvent(@NonNull String playerId, @NonNull String customData) { super(playerId); this.customData = customData; } }2. イベントをポストします。
// イベントを作成してポストします MyCustomEvent event = new MyCustomEvent(playerId, "custom_data"); PlayerEventBus.getInstance().post(event);3. カスタムイベントをサブスクライブします。
// カスタムイベントをサブスクライブします PlayerEventBus.getInstance().subscribe(MyCustomEvent.class, event -> { // カスタムイベントを処理します String data = event.customData; });
マルチプレイヤーでのイベント分離方法
各イベントには playerId が含まれます。playerId を比較することで、複数のプレーヤー間でイベントを分離できます。
// イベントをサブスクライブする際に playerId でフィルタリングします
mEventBus.subscribe(PlayerEvents.Info.class, event -> {
// 現在のプレーヤーのイベントのみを処理します
if (mPlayerId.equals(event.playerId)) {
updateProgress(event.currentPosition, event.duration);
}
});コマンドの一括サブスクライブ
PlayerCommand クラスには、一括サブスクリプション用の ALL_COMMANDS 配列が用意されています。
// すべての再生コマンドをサブスクライブします
for (Class<? extends PlayerCommand> commandClass : PlayerCommand.ALL_COMMANDS) {
mEventBus.subscribe(commandClass, mCommandListener);
}
// すべての再生コマンドのサブスクライブを解除します
for (Class<? extends PlayerCommand> commandClass : PlayerCommand.ALL_COMMANDS) {
mEventBus.unsubscribe(commandClass, mCommandListener);
}ベストプラクティス
サブスクリプションのライフサイクル管理
シナリオ | 推奨される方法 | 理由 |
アクティビティまたはフラグメント |
|
|
スロット内 |
| フレームワークがライフサイクルを自動的に処理します。 |
シングルトンコンポーネント | 弱い参照を使用するか、手動で管理します。 | シングルトンは長寿命のため、クリーンアップを明示的に行う必要があります。 |
スレッドセーフ
イベントは呼び出し元スレッドで同期的にディスパッチされます。次の点にご注意ください。
// メインスレッドでサブスクライブすると、コールバックもメインスレッドで実行されます
runOnUiThread(() -> {
mEventBus.subscribe(PlayerEvents.Info.class, event -> {
// このコールバックは、イベントをポストしたスレッドで実行されます。
// イベントがサブスレッドからポストされた場合は、メインスレッドに切り替えて UI を更新します。
runOnUiThread(() -> updateUI(event));
});
});コマンド送信のタイミング
コマンド | 推奨されるタイミング | 説明 |
| いつでも可能です。 | 副作用のない安全なコマンドです。 |
| プレーヤーが | 動画が読み込まれていることを保証します。 |
| 再生中または一時停止中。 | プレーヤーが初期化されていることを保証します。 |
| 最初のフレームがレンダリングされた後。 | キャプチャするフレームが存在することを保証します。 |
注意事項
コンストラクターのリスク: コンストラクター内でイベントをサブスクライブしたり送信したりしないでください。オブジェクトがまだ完全に初期化されていないため、NullPointerException が発生する可能性があります。
playerIdもまだ注入されていません。リスナーのマッチング: サブスクライブを解除する際は、元のリスナーインスタンスを使用してください。マッチングはオブジェクト参照の同一性に依存します。
長時間実行される操作を避ける: イベントコールバック内で長時間実行される操作を実行しないでください。イベントは同期的にディスパッチされ、スレッドをブロックします。
高頻度イベントのパフォーマンス:
シナリオ:
PlayerEvents.Info(再生進捗)、LoadingProgress、GestureEvents.Updateなどの高頻度のイベント (1 秒あたり数十回に達する場合があります)。オブジェクトを再利用することで、(
new String()のような)メモリ割り当てを回避します。負荷の高いレイアウト更新を防ぎ、再描画を削減するために、スロットリングポリシーと同値フィルタリングを使用します。
Examples
The project includes a complete example at playerkit-examples/example-event-system.
Example features
Feature | Description |
Subscribe to playback progress | Receive |
Send playback commands | Control play/pause using |
Display event logs | Show the last 20 event messages. |
Run the example
In the demo app, you can select the Event System example to view it.
API リファレンス
コアインターフェイス
インターフェイスまたはクラス | 説明 |
| すべてのイベントの基底クラスです。 |
| すべてのコマンドの基底クラスです。 |
| サブスクリプションと発行を管理するイベントバスです。 |
| イベントリスナーインターフェイスです。 |
PlayerEventBus メソッド
メソッド | 説明 |
| シングルトンのイベントバスを取得します。 |
| 指定したイベントタイプをサブスクライブします。 |
| 指定したイベントタイプをアンサブスクライブします。 |
| すべてのイベントタイプからリスナーをアンサブスクライブします。 |
| イベントをポストします。 |
| 指定したイベントタイプのサブスクライバー数を取得します。 |
| 指定したイベントタイプのサブスクライバーが存在するかどうかを確認します。 |
| すべてのサブスクリプションを解除します (注意して使用してください)。 |
技術原理
弱い参照メカニズム
イベントバスは、WeakReference を使用してリスナーを保持します。これにより、次のメリットがあります:
自動クリーンアップ:ガベージコレクション後に、リスナーはサブスクリプションリストから削除されます。
リーク防止:サブスクライブを解除するのを忘れても、深刻なメモリリークは発生しません。
安全なアクセス:ディスパッチする前に、イベントバスは各弱い参照が有効かどうかを確認します。
// 弱い参照を使用してリスナーを保持します
List<WeakReference<EventListener<? extends PlayerEvent>>> listeners;
// ディスパッチ前に有効性を確認します
Iterator<WeakReference<EventListener<? extends PlayerEvent>>> iterator = listeners.iterator();
while (iterator.hasNext()) {
WeakReference<EventListener<? extends PlayerEvent>> ref = iterator.next();
EventListener listener = ref.get();
if (listener != null) {
listener.onEvent(event); // 有効な場合は呼び出します
} else {
iterator.remove(); // 無効な場合はクリーンアップします
}
}単方向データフローと状態同期ループ
イベントシステムが処理するのは、一時的な通知 のみです。 PlayerStateStore と組み合わせて状態を永続化することで、厳密な単方向データフロー (UDF) アーキテクチャが構成されます。
データフロー:
方向 | メカニズム | 説明 |
コマンドアップ | UI → Controller | スロットがコマンドイベントを送信し、プレーヤーの動作を制御します。 |
ステートダウン | Controller → UI | コントローラーが状態変更イベントを発行し、スロットに通知します。 |
ステートプル | UI → StateStore | スロットが現在の状態を能動的に取得します (フォールバックメカニズム)。 |
なぜステートプルが必要なのですか?
イベントは一時的です。遅延バインドされたスロットは、アタッチ後に履歴イベントを取りこぼす可能性があります。 getPlayerStateStore() を使用すると、イベントサブスクリプションのタイミングに依存せず、プレーヤーの現在の状態を即座に同期できます。
@Overridepublic void onAttach(@NonNull SlotHost host) {
super.onAttach(host);
// スロットのアタッチが遅れて Prepared、StateChanged などを取りこぼしましたか?
// 現在の状態を取得して UI を初期化します
IPlayerStateStore stateStore = host.getPlayerStateStore();
PlayerState currentState = stateStore.getPlayState(); // 現在の再生状態
long position = stateStore.getCurrentPosition(); // 現在の再生位置
long duration = stateStore.getDuration(); // 動画の総再生時間
// 現在の状態に基づいて UI を初期化します
updatePlayPauseIcon(currentState);
updateProgress(position, duration);
}
このアーキテクチャにより、UI とコントローラーは完全に疎結合になります。UI はコントローラー参照を保持しません。イベントをサブスクライブし、コマンドを送信するだけです。コントローラーは誰がリッスンしているかを意識せず、イベントを発行し、コマンドを処理するだけです。
イベント継承のサポート
親イベントタイプをサブスクライブすると、すべての子イベントタイプを受信します。
// PlayerEvent をサブスクライブして、すべてのイベントを受信します
mEventBus.subscribe(PlayerEvent.class, event -> {
// すべての PlayerEvent サブクラスを受信します
});
// PlayerCommand をサブスクライブして、すべてのコマンドを受信します
mEventBus.subscribe(PlayerCommand.class, event -> {
// すべての PlayerCommand サブクラスを受信します
});よくある質問
イベントを受信できないのはなぜですか?
次の手順で問題をトラブルシューティングできます。
サブスクリプションのステータス:
subscribe()を呼び出したことを確認してください。イベントのマッチング:サブスクライブしたタイプと発行されたタイプが一致している必要があります。
本人確認:イベントの
playerIdが一致する必要があります。ガベージコレクションの確認:リスナーが早期にガベージコレクションされていないことを確認してください。
イベントをデバッグするにはどうすればよいですか?
LogHub を使用してログを表示できます。タグは PlayerEventBus です。
// サブスクリプションログを表示します
// I/PlayerEventBus: Subscribed to Info
// 発行ログを表示します (詳細ログを有効にします)
// I/PlayerEventBus: Posted event: Info to 2 listenersよくあるクラッシュの間違い
以下は、開発者が直面しがちな最も一般的な問題です。これらは避けるようにしてください。
間違い 1: アンサブスクライブのし忘れによるメモリリーク
誤ったコード:
public class MyActivity extends AppCompatActivity {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// サブスクライブしたが、アンサブスクライブしていない
PlayerEventBus.getInstance().subscribe(PlayerEvents.Info.class, event -> {
updateUI(event.currentPosition); // Activity がリークします!
});
}
// onDestroy でアンサブスクライブされていません
}
正しいコード:
public class MyActivity extends AppCompatActivity {
private PlayerEventBus.EventListener<PlayerEvents.Info> mListener;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mListener = event -> updateUI(event.currentPosition);
PlayerEventBus.getInstance().subscribe(PlayerEvents.Info.class, mListener);
}
@Override protected void onDestroy() {
super.onDestroy();
// 必ずアンサブスクライブします
PlayerEventBus.getInstance().unsubscribe(PlayerEvents.Info.class, mListener);
}
}
クラッシュの理由:ラムダ式は Activity への参照を保持します。アンサブスクライブしないと、Activity を解放できません。
間違い 2: コンストラクターにおけるイベントのサブスクライブ
誤ったコード:
public class MyComponent {
public MyComponent() {
// コンストラクターでサブスクライブ — オブジェクトが完全に初期化されていません
PlayerEventBus.getInstance().subscribe(PlayerEvents.Info.class, event -> {
// この時点で 'this' は完全に初期化されていない可能性があります
});
}
}
正しいアプローチ:
public class MyComponent {
private PlayerEventBus.EventListener<PlayerEvents.Info> mListener;
public void init() {
// 初期化メソッドでサブスクライブします
mListener = event -> handleEvent(event);
PlayerEventBus.getInstance().subscribe(PlayerEvents.Info.class, mListener);
}
public void destroy() {
// アンサブスクライブします
PlayerEventBus.getInstance().unsubscribe(PlayerEvents.Info.class, mListener);
}
}
クラッシュの理由:コンストラクターの実行中、オブジェクトは完全に初期化されていないため、NullPointerException や不整合な状態が発生する可能性があります。
間違い 3: スロットのコンストラクターにおけるイベントの送信
誤ったコード:
public class MySlot extends BaseSlot {
public MySlot(Context context) {
super(context);
// コンストラクターでは playerId が設定されていません
postEvent(new PlayerCommand.Toggle(getPlayerId())); // getPlayerId() は null を返します!
}
}
正しいアプローチ:
public class MySlot extends BaseSlot {
@Override public void onAttach(@NonNull SlotHost host) {
super.onAttach(host);
// onAttach の後にイベントを送信します
String playerId = getPlayerId();
if (playerId != null) {
postEvent(new PlayerCommand.Toggle(playerId));
}
}
}
クラッシュ原因: getPlayerId() メソッドは、onAttach() メソッドが呼び出された後にのみ有効な値を返します。コンストラクター内では null を返します。
間違い 4: イベントコールバックにおける時間のかかる操作の実行
誤ったコード:
mEventBus.subscribe(PlayerEvents.Info.class, event -> {
// コールバック内のネットワークリクエストがイベントディスパッチをブロックします
String data = fetchFromNetwork(); // ブロックします!
updateUI(data);
});
正しいアプローチ:
mEventBus.subscribe(PlayerEvents.Info.class, event -> {
// 時間のかかる操作を非同期で実行します
executorService.execute(() -> {
String data = fetchFromNetwork();
runOnUiThread(() -> updateUI(data));
});
});
クラッシュの理由:イベントディスパッチは同期的です。時間のかかる操作は、後続のすべてのイベントディスパッチをブロックし、UI のカクつきを引き起こします。
間違い 5: スロットにおける observedEvents() の不使用
誤ったコード:
public class MySlot extends BaseSlot {
@Override public void onAttach(@NonNull SlotHost host) {
super.onAttach(host);
// 手動でのサブスクリプションには手動でのクリーンアップが必要です
PlayerEventBus.getInstance().subscribe(PlayerEvents.Info.class, mListener);
}
@Override public void onDetach() {
// アンサブスクライブを忘れるとリークします!
super.onDetach();
}
}
正しいアプローチ:
public class MySlot extends BaseSlot {
// 自動管理のために observedEvents() を使用します
@Override protected List<Class<? extends PlayerEvent>> observedEvents() {
return Arrays.asList(PlayerEvents.Info.class);
}
@Override protected void onEvent(@NonNull PlayerEvent event) {
if (event instanceof PlayerEvents.Info) {
// イベントを処理します
}
}
}クラッシュの理由:手動でのサブスクリプションには手動でのクリーンアップが必要です。アンサブスクライブを忘れると、リスナーのリークが発生します。
手動での購読を使用する場合は、必ず onDetach() メソッドで購読を解除する必要があります:
@Override public void onDetach() {
PlayerEventBus.getInstance().unsubscribe(PlayerEvents.Info.class, mListener);
super.onDetach();
}