すべてのプロダクト
Search
ドキュメントセンター

ApsaraVideo VOD:イベントシステム

最終更新日:Sep 11, 2026

本トピックでは、AliPlayerKit イベントシステムのコアコンセプト、イベントカテゴリ、使用方法、ベストプラクティスについて説明します。

コンセプト

イベントとは

イベントとは、プレーヤーの実行中に発生する状態変化または動作の通知です。各イベントには、その発生元を識別し、複数のプレーヤーインスタンス間でイベントを隔離するために、プレーヤー ID (playerId) が含まれます。

AliPlayerKit のイベントは、主に次の 2 つのカテゴリに分類されます:

タイプ

説明

例

ステータスイベント

プレーヤー内部で生成される、プレーヤーの状態変化の通知です。

StateChanged、Prepared、Error

コマンドイベント

UI または外部コンポーネントによって生成される、プレーヤーの動作を制御するための命令です。

Play、Pause、Seek、SetSpeed

すべてのイベントは、イベント構造の一貫性を確保するために、基底クラス PlayerEvent を継承します。

イベントシステムとは

イベントシステムとは、イベントのサブスクリプションとパブリッシュを管理するための通信アーキテクチャです。コアコンポーネントは PlayerEventBus です。

次の機能を備えています:

  • 型安全なサブスクリプション: イベントタイプでサブスクライブできます。型安全性はコンパイル時に保証されます。

  • スレッドセーフ: マルチスレッド環境でのイベントのパブリッシュとサブスクリプションをサポートします。

  • 弱い参照の管理: 弱い参照を使用してリスナーを保持し、ガベージコレクションされたリスナーを自動的にクリーンアップします。

  • イベントの隔離: playerId を使用して、複数のプレーヤーインスタンス間でイベントを隔離します。

イベントシステムは、UI コンポーネント (Slots) とコントローラーを完全にデカップリングします。UI はコマンドを送信し、状態変化をサブスクライブするだけで、コマンドがどのように実行されるかを意識する必要はありません。コントローラーはコマンドを処理し、状態変化をパブリッシュするだけで、誰がリッスンしているかを意識する必要はありません。

機能

課題の解決

  1. コンポーネントの疎結合:コンポーネント間の直接的な依存関係を排除し、カップリングを削減することで、交換可能性とテスト容易性を向上させます。

  2. イベントトレーサビリティ:マルチプレイヤーシナリオにおけるイベントソースの混乱を解消します。

  3. 責務の分離:UI コンポーネントとビジネスロジックを分離し、責務の境界を明確にします。

コアバリュー

イベントシステムは、コンポーネント間の通信を標準化します。開発者は次の方法で使用できます:

使用方法

説明

利点

イベントをサブスクライブする

プレーヤーの状態変化をリッスンします。

UI をビジネスロジックから疎結合化し、リアクティブに UI を更新します。

コマンドを送信する

プレーヤーの動作をコントロールします。

UI はコントローラー参照を保持する必要がなく、コマンドベースで対話します。

イベントをカスタマイズする

イベントタイプを拡張します。

アーキテクチャの一貫性を保ちながら、特定のビジネスシナリオに対応します。

アーキテクチャ上の利点:

  • 疎結合:コンポーネントはイベントを通じて通信し、直接的な依存関係を持ちません。

  • テスト容易性:イベントでやり取りをシミュレートすることで、コンポーネントを独立してテストできます。

  • 拡張性:フレームワーク コードを変更することなく、カスタムイベントタイプを追加できます。

  • スレッドセーフ:追加の同期なしで、マルチスレッド環境で動作します。

コア機能

機能

説明

型安全なサブスクリプション

イベントタイプごとにサブスクライブします。型チェックはコンパイル時に行われます。

弱い参照の管理

ガベージコレクションされたリスナーを自動的にクリーンアップし、メモリリークを防ぎます。

イベントの継承のサポート

親イベントタイプをサブスクライブすると、すべての子イベントタイプを受信します。

マルチプレイヤーの隔離

playerId を使用して、異なるプレーヤーからのイベントを区別します。

イベントカテゴリ

プレーヤーイベント (PlayerEvents)

これらは、プレーヤーの状態を外部コンポーネントに通知するためにプレーヤー内部で生成される状態変化イベントです。

イベント

説明

付随データ

Prepared

プレーヤーの準備が完了しました。

duration :動画の総再生時間

FirstFrameRendered

最初のフレームがレンダリングされました。

-

StateChanged

再生状態が変更されました。

oldState、newState

VideoSizeChanged

動画サイズが変更されました。

width、height

Info

再生進捗が更新されました。

currentPosition、duration、bufferedPosition

Error

再生エラーが発生しました。

errorCode、errorMsg

LoadingBegin

バッファリングが開始されました。

-

LoadingProgress

バッファリングの進捗。

percent、netSpeed

LoadingEnd

バッファリングが終了しました。

-

SetSpeedCompleted

再生速度の設定が完了しました。

speed

SnapshotCompleted

スクリーンショットが完了しました。

result、snapshotPath、width、height

SetLoopCompleted

ループ設定が完了しました。

loop

SetMuteCompleted

ミュート設定が完了しました。

mute

SetScaleTypeCompleted

フィルモードの設定が完了しました。

scaleType

SetMirrorTypeCompleted

ミラー設定が完了しました。

mirrorType

SetRotationCompleted

回転設定が完了しました。

rotation

TrackQualityListUpdated

画質リストが更新されました。

trackQualityList

TrackSelected

画質の選択が完了しました。

trackIndex

プレーヤーコマンド (PlayerCommand)

これらは、プレーヤーの動作を制御するために使用されるコマンドイベントです。UI コンポーネントまたは外部コードが送信します。

コマンド

説明

パラメーター

Play

再生を開始します。

-

Pause

再生を一時停止します。

-

Toggle

再生と一時停止を切り替えます。

-

Stop

再生を停止します。

-

Replay

もう一度再生します。

-

Seek

指定した位置にシークします。

position :ターゲット位置 (ミリ秒)

SetSpeed

再生速度を設定します。

speed :再生速度 (0.3~3.0)

Snapshot

スクリーンショットを撮ります。

-

SetLoop

ループ再生を設定します。

loop :ループするかどうか

SetMute

ミュートを設定します。

mute :ミュートするかどうか

SetScaleType

フィルモードを設定します。

scaleType :フィルモード

SetMirrorType

ミラーを設定します。

mirrorType :ミラーモード

SetRotation

回転を設定します。

rotationMode :回転角度

SelectTrack

画質を切り替えます。

trackQuality :ターゲットの画質

ジェスチャーイベント (GestureEvents)

ユーザーのジェスチャー操作によって生成されるイベントです。これらのイベントはジェスチャー自体を記述し、ビジネスロジックは含みません。

イベント

説明

付随データ

SingleTapEvent

シングルタップが発生しました。

x、y :タップ座標

DoubleTapEvent

ダブルタップが発生しました。

x、y :タップ座標

LongPressEvent

長押しが開始されました。

x、y :長押し座標

LongPressEndEvent

長押しが終了しました。

-

HorizontalDragStartEvent

水平ドラッグが開始されました。

startX、startY

HorizontalDragUpdateEvent

水平ドラッグが更新されました。

deltaPercent :パーセンテージの増分

HorizontalDragEndEvent

水平ドラッグが終了しました。

-

LeftVerticalDragStartEvent

左側の垂直ドラッグが開始されました。

startX、startY

LeftVerticalDragUpdateEvent

左側の垂直ドラッグが更新されました。

deltaPercent、currentPercent

LeftVerticalDragEndEvent

左側の垂直ドラッグが終了しました。

-

RightVerticalDragStartEvent

右側の垂直ドラッグが開始されました。

startX、startY

RightVerticalDragUpdateEvent

右側の垂直ドラッグが更新されました。

deltaPercent、currentPercent

RightVerticalDragEndEvent

右側の垂直ドラッグが終了しました。

-

コントロールバーイベント (ControlBarEvents)

コントロールバーの表示状態を同期させるイベントです。

イベント

説明

Show

コントロールバーを表示します。

Hide

コントロールバーを非表示にします。

ResetTimer

自動非表示タイマーをリセットします。

ShowSettings

設定インターフェイスを表示します。

フルスクリーンイベント (FullscreenEvents)

これらは、フルスクリーンモードへの切り替え、またはフルスクリーンモードからの復帰のためのイベントです。

イベント

説明

Toggle

フルスクリーンモードを切り替えます。

ライフサイクルイベント (PlayerLifecycleEvents)

これらは、プレーヤーのライフサイクルポリシーに関連するイベントです。これらを使用して、プレーヤーインスタンスの作成、再利用、破棄を監視できます。

イベント

説明

PlayerCreated

新しいプレーヤーインスタンスが作成されました。

PlayerDestroyed

プレーヤーインスタンスが破棄されました。

PlayerReused

アイドル状態のプレーヤーインスタンスが再利用されました。

PlayerHit

アクティブなプレーヤーインスタンスがヒットしました。

PlayerEvicted

プレーヤーインスタンスがエビクトされました。

スロットイベント (SlotEvents)

これらはスロット内部で生成されるイベントです。

イベント

説明

TopBarBackClicked

トップバーの戻るボタンがクリックされました。

基本的な使用法

イベントシステムは、次の 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() を使用して、イベントをサブスクライブし、処理できます。ストラテジーには次の特徴があります。

特徴

説明

主にモニタリング向け

ストラテジーは通常、分析のためにイベントをサブスクライブするのみで、コマンドは送信しません。

イベントのフィルタリング

マルチプレイヤーのシナリオでは、isCurrentPlayer(event) を使用してイベントをフィルタリングする必要があります。

コンテキストへのアクセス

StrategyContext で、プレーヤーの状態とデータにアクセスします。

コード例:

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. 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. 2. イベントをポストします。

    // イベントを作成してポストします
    MyCustomEvent event = new MyCustomEvent(playerId, "custom_data");
    PlayerEventBus.getInstance().post(event);
  3. 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);
}

ベストプラクティス

サブスクリプションのライフサイクル管理

シナリオ

推奨される方法

理由

アクティビティまたはフラグメント

onDestroy() でサブスクライブを解除します。

Activity のリークを防止します。

スロット内

observedEvents() を使用して自動管理します。

フレームワークがライフサイクルを自動的に処理します。

シングルトンコンポーネント

弱い参照を使用するか、手動で管理します。

シングルトンは長寿命のため、クリーンアップを明示的に行う必要があります。

スレッドセーフ

イベントは呼び出し元スレッドで同期的にディスパッチされます。次の点にご注意ください。

// メインスレッドでサブスクライブすると、コールバックもメインスレッドで実行されます
runOnUiThread(() -> {
    mEventBus.subscribe(PlayerEvents.Info.class, event -> {
        // このコールバックは、イベントをポストしたスレッドで実行されます。
        // イベントがサブスレッドからポストされた場合は、メインスレッドに切り替えて UI を更新します。
        runOnUiThread(() -> updateUI(event));
    });
});

コマンド送信のタイミング

コマンド

推奨されるタイミング

説明

Play/Pause/Toggle

いつでも可能です。

副作用のない安全なコマンドです。

Seek

プレーヤーが Prepared 状態になった後。

動画が読み込まれていることを保証します。

SetSpeed

再生中または一時停止中。

プレーヤーが初期化されていることを保証します。

Snapshot

最初のフレームがレンダリングされた後。

キャプチャするフレームが存在することを保証します。

注意事項

  1. コンストラクターのリスク: コンストラクター内でイベントをサブスクライブしたり送信したりしないでください。オブジェクトがまだ完全に初期化されていないため、NullPointerException が発生する可能性があります。playerId もまだ注入されていません。

  2. リスナーのマッチング: サブスクライブを解除する際は、元のリスナーインスタンスを使用してください。マッチングはオブジェクト参照の同一性に依存します。

  3. 長時間実行される操作を避ける: イベントコールバック内で長時間実行される操作を実行しないでください。イベントは同期的にディスパッチされ、スレッドをブロックします。

高頻度イベントのパフォーマンス:

  • シナリオ: 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 PlayerEvents.Info events in real time.

Send playback commands

Control play/pause using PlayerCommand.Toggle.

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 リファレンス

コアインターフェイス

インターフェイスまたはクラス

説明

PlayerEvent

すべてのイベントの基底クラスです。

PlayerCommand

すべてのコマンドの基底クラスです。

PlayerEventBus

サブスクリプションと発行を管理するイベントバスです。

EventListener<T>

イベントリスナーインターフェイスです。

PlayerEventBus メソッド

メソッド

説明

getInstance()

シングルトンのイベントバスを取得します。

subscribe(eventType, listener)

指定したイベントタイプをサブスクライブします。

unsubscribe(eventType, listener)

指定したイベントタイプをアンサブスクライブします。

unsubscribe(listener)

すべてのイベントタイプからリスナーをアンサブスクライブします。

post(event)

イベントをポストします。

getSubscriberCount(eventType)

指定したイベントタイプのサブスクライバー数を取得します。

hasSubscribers(eventType)

指定したイベントタイプのサブスクライバーが存在するかどうかを確認します。

unsubscribeAll()

すべてのサブスクリプションを解除します (注意して使用してください)。

技術原理

弱い参照メカニズム

イベントバスは、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 サブクラスを受信します
});

よくある質問

イベントを受信できないのはなぜですか?

次の手順で問題をトラブルシューティングできます。

  1. サブスクリプションのステータス:subscribe() を呼び出したことを確認してください。

  2. イベントのマッチング:サブスクライブしたタイプと発行されたタイプが一致している必要があります。

  3. 本人確認:イベントの playerId が一致する必要があります。

  4. ガベージコレクションの確認:リスナーが早期にガベージコレクションされていないことを確認してください。

イベントをデバッグするにはどうすればよいですか?

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();
}