中安拓也のブログ

プログラミングについて書くブログ。 Twitterやってます @l08084

【RxJS】Subjectを使って好きなタイミングでデータを流す

はじめに

今回は、前回の記事で作成した強制バージョンアップとメンテナンスのアラートを表示する機能に、下記の機能を追加することでリアクティブ・プログラミング用のライブラリであるRxJSのSubjectの使用方法について説明します。

  • 今回追加する機能
    • 強制バージョンアップのアラートの表示を、メンテナンスのアラートの表示よりも優先する機能
      • メンテナンスのアラートを表示しているときに、強制バージョンアップのアラートを表示する場合は、メンテナンスのアラートを閉じる
      • 強制バージョンアップのアラートを表示している時はメンテナンスのメッセージを表示しない

ちなみに、上記の例だとSubjectを使わなくてもすっきりしたコードが書けます。まあ、RxJSの勉強ということで。。。

環境

TypeScriptベースのフレームワークであるAngularと、iOS/AndroidのハイブリッドモバイルアプリケーションのフレームワークであるIonicを使用しています。

  • "rxjs": "~6.6.0",
  • "@angular/fire": "^6.1.4"
  • "firebase": "^8.3.1"
  • "semver": "^7.3.5"

ionic infoコマンドの実行結果

$ ionic info

Ionic:

   Ionic CLI                     : 6.11.8 (/usr/local/lib/node_modules/@ionic/cli)
   Ionic Framework               : @ionic/angular 5.6.0
   @angular-devkit/build-angular : 0.1101.4
   @angular-devkit/schematics    : 11.1.4
   @angular/cli                  : 11.1.4
   @ionic/angular-toolkit        : 3.1.0

Cordova:

   Cordova CLI       : 8.0.0
   Cordova Platforms : none
   Cordova Plugins   : no whitelisted plugins (1 plugins total)

Utility:

   cordova-res : not installed
   native-run  : not installed

System:

   ios-deploy : 1.9.2
   ios-sim    : 6.1.2
   NodeJS     : v12.13.1 (/usr/local/bin/node)
   npm        : 6.14.12
   OS         : macOS Catalina
   Xcode      : Xcode 12.0.1 Build version 12A7300

アラート表示の実装

前回の記事で作成した強制バージョンアップとメンテナンスのアラートを表示する実装です。

この実装にSubjectを使用して、強制バージョンアップのアラートの表示を、メンテナンスのアラートの表示よりも優先する機能を付けていきます。

src/app/services/version-check.service.ts

import { Injectable } from '@angular/core';
import { AngularFireDatabase } from '@angular/fire/database';
import { Observable } from 'rxjs';
import { Maintenance } from '../model/maintenance.model';
import { Version } from '../model/version.model';
import * as semver from 'semver';
import { AlertController } from '@ionic/angular';
@Injectable({
  providedIn: 'root',
})
export class VersionCheckService {
  // このアプリのバージョン
  private readonly appVersion = '1.0.0';

  private maintenance$: Observable<Maintenance>;
  private version$: Observable<Version>;
  private maintenanceAlert: HTMLIonAlertElement;
  private versionUpAlert: HTMLIonAlertElement;

  constructor(
    private db: AngularFireDatabase,
    private alertController: AlertController
  ) {}

  /**
   * 初期設定
   *
   * @memberof VersionCheckService
   */
  public initSetting(): void {
    // Realtime Databaseからデータを取得
    this.maintenance$ = this.db
      .object<Maintenance>('maintenance')
      .valueChanges();
    this.version$ = this.db.object<Version>('version').valueChanges();

    this.maintenance$.subscribe(
      async (maintenance: Maintenance) =>
        await this.checkMaintenance(maintenance)
    );
    this.version$.subscribe(
      async (version: Version) =>
        await this.checkVersion(this.appVersion, version)
    );
  }

  /**
   * メンテナンスポップアップを表示する。
   *
   * @private
   * @param {Maintenance} maintenance
   * @returns {Promise<void>}
   * @memberof VersionCheckService
   */
  private async checkMaintenance(maintenance: Maintenance): Promise<void> {
    if (!maintenance) {
      return;
    }

    if (!maintenance.maintenanceFlg) {
      // メンテナンスフラグがOFFだったら処理を中断する
      if (this.maintenanceAlert) {
        // メンテナンスメッセージが開かれている場合は閉じる
        await this.maintenanceAlert.dismiss();
        this.maintenanceAlert = undefined;
      }
      return;
    }

    // メンテナンスメッセージを表示する
    this.maintenanceAlert = await this.alertController.create({
      header: maintenance.title,
      message: maintenance.message,
      backdropDismiss: false, // 背景をクリックしても閉じない
    });
    await this.maintenanceAlert.present();
  }

  /**
   * 強制バージョンアップメッセージを表示する。
   *
   * @private
   * @param {string} appVersion
   * @param {Version} version
   * @returns
   * @memberof VersionCheckService
   */
  private async checkVersion(appVersion: string, version: Version) {
    if (!version || !version.minimumVersion) {
      return;
    }

    if (semver.gte(appVersion, version.minimumVersion)) {
      // 最低バージョンよりもアプリのバージョンが高かったら処理を中断する
      if (this.versionUpAlert) {
        // 強制バージョンアップメッセージが開かれている場合は閉じる
        await this.versionUpAlert.dismiss();
        this.versionUpAlert = undefined;
      }
      return;
    }

    // 強制バージョンアップメッセージを表示する
    this.versionUpAlert = await this.alertController.create({
      header: version.title,
      message: version.message,
      backdropDismiss: false, // 背景をクリックしても閉じない
    });
    await this.versionUpAlert.present();
  }
}

ObservableではなくSubjectが必要になるタイミング

AngularのRxJSを使ってデータの受け渡しをする - Qiita

AngularでObservableを使うとき、もう一つ抑えておきたいクラスがあります。 上記のコードではObservableクラスのインスタンスを作成したタイミングでしかデータを流すことができず、クリックなどのイベントをトリガーとしてデータを処理したいような場合には向いていません。 そんな時に使用するのがSubjectクラスです。 SubjectクラスのインスタンスはObservableとobserverの2つの役割を同時に担うことができ、任意のタイミングでデータを流すことができます。

上記の記事で記載されている通り、SubjectはObservableと違って、next()メソッドを呼び出すことができるので、任意のタイミングでデータを流すことができます。

メンテナンスよりも強制バージョンアップのアラートを優先して表示する

さて、Subjectを使用して、メンテナンスのアラートを表示しているときに、強制バージョンアップのアラートを表示する場合は、メンテナンスのアラートを閉じる機能を実装するには、下記のようにする必要があります。

  • 強制バージョンアップのアラートが表示されたときには、Subject(isShowVersionUpAlert)のnext(true)を呼び出してストリームにtrueの値を流す
  • 強制バージョンアップのアラートが閉じたときには、Subject(isShowVersionUpAlert)のnext(false)を呼び出してストリームにfalseの値を流す
  • 下記の処理でストリームに流された値を受け取る
    this.isShowVersionUpAlert
      .pipe(
        filter(
          (isShowVersionUp: boolean) =>
            isShowVersionUp && !!this.maintenanceAlert
        )
      )
      .subscribe(async () => {
        await this.maintenanceAlert.dismiss();
        this.maintenanceAlert = undefined;
      });

上記の処理では、Subject(isShowVersionUpAlert)ストリームにデータが流れたときに、強制バージョンアップが表示されている(isShowVersionUp=true) かつ、メンテナンスアラートが表示されている(!!this.maintenanceAlert=true)場合は、メンテナンスのアラートを閉じるという処理をしています。

上記の処理を追加した、コードの全体像は下記のようになります。

src/app/services/version-check.service.ts

import { Injectable } from '@angular/core';
import { AngularFireDatabase } from '@angular/fire/database';
import { Observable, Subject } from 'rxjs';
import { Maintenance } from '../model/maintenance.model';
import { Version } from '../model/version.model';
import * as semver from 'semver';
import { AlertController } from '@ionic/angular';
import { filter } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class VersionCheckService {
  private readonly appVersion = '1.0.0';

  private maintenance$: Observable<Maintenance>;
  private version$: Observable<Version>;
  private maintenanceAlert: HTMLIonAlertElement;
  private versionUpAlert: HTMLIonAlertElement;

  // add this!
  private isShowVersionUpAlert = new Subject<boolean>();

  constructor(
    private db: AngularFireDatabase,
    private alertController: AlertController
  ) {}

  /**
   * 初期設定
   *
   * @memberof VersionCheckService
   */
  public initSetting(): void {
    this.maintenance$ = this.db
      .object<Maintenance>('maintenance')
      .valueChanges();
    this.version$ = this.db.object<Version>('version').valueChanges();

    this.maintenance$.subscribe(
      async (maintenance: Maintenance) =>
        await this.checkMaintenance(maintenance)
    );
    this.version$.subscribe(
      async (version: Version) =>
        await this.checkVersion(this.appVersion, version)
    );

    // add this!
    this.isShowVersionUpAlert
      .pipe(
        filter(
          (isShowVersionUp: boolean) =>
            isShowVersionUp && !!this.maintenanceAlert
        )
      )
      .subscribe(async () => {
        await this.maintenanceAlert.dismiss();
        this.maintenanceAlert = undefined;
      });
  }

  /**
   * メンテナンスポップアップを表示する。
   *
   * @private
   * @param {Maintenance} maintenance
   * @returns {Promise<void>}
   * @memberof VersionCheckService
   */
  private async checkMaintenance(maintenance: Maintenance): Promise<void> {
    if (!maintenance) {
      return;
    }

    // add conditions '|| !!this.versionUpAlert'
    if (!maintenance.maintenanceFlg || !!this.versionUpAlert) {
      if (this.maintenanceAlert) {
        await this.maintenanceAlert.dismiss();
        this.maintenanceAlert = undefined;
      }
      return;
    }

    this.maintenanceAlert = await this.alertController.create({
      header: maintenance.title,
      message: maintenance.message,
      backdropDismiss: false,
    });
    await this.maintenanceAlert.present();
  }

  /**
   * 強制バージョンアップメッセージを表示する。
   *
   * @private
   * @param {string} appVersion
   * @param {Version} version
   * @returns
   * @memberof VersionCheckService
   */
  private async checkVersion(appVersion: string, version: Version) {
    if (!version || !version.minimumVersion) {
      return;
    }

    if (semver.gte(appVersion, version.minimumVersion)) {
      if (this.versionUpAlert) {
        await this.versionUpAlert.dismiss();
        this.versionUpAlert = undefined;

        // add this!
        this.isShowVersionUpAlert.next(false);
      }
      return;
    }

    this.versionUpAlert = await this.alertController.create({
      header: version.title,
      message: version.message,
      backdropDismiss: false,
    });

    // add this!
    this.isShowVersionUpAlert.next(true);

    await this.versionUpAlert.present();
  }
}

動作確認

上記のコードを実際に動かしてみます。

まず、メンテナンスのアラートを表示します。

f:id:l08084:20210404155530p:plain
メンテナンスのアラートが表示されている

続いて、強制バージョンアップのアラートを表示すると、メンテナンスのアラートが閉じて、強制バージョンアップのアラートだけが表示されることがわかります。期待結果通りの動きです。

f:id:l08084:20210404155604p:plain
強制バージョンアップのアラートだけが表示される

おまけ: どうしてSubjectにasObservableが必要なのか

上記の実装をしている時に、SubjcetにasObservable()メソッドが用意されているのはなぜなのか?という疑問を持ちました。

asObservable()はSubjectをObservableに変換するメソッドですが、Subjectは元々Observableの機能を持っているため、SubjectからObservableに変換する意味がわからなかったためです。

Why asObservable with Subjects?. When I started learning angular I read… | by Mamta Bisht | Medium

この疑問を解消するのに、上記の記事の解説が参考になりました。

SubjectからasObservable()を呼び出してObservableに変換するのは、機能を制限するため、とのことです。

Subjectを他クラスに渡すときなどに、そのまま渡すとnext()メソッドが使えるので、他のクラスからも値を流すことが可能になってしまい、意図していない値の流され方をされてしまうかもしれません。

そのような事態を防ぐために、asObservable()でSubjectをObservableに変換してnext()メソッドを使えない状態にしてから渡したほうが良い、とのことでした。

参考

Why asObservable with Subjects?. When I started learning angular I read… | by Mamta Bisht | Medium

[Angular] サービスを使用してデータをコンポーネント間で共有する - Qiita

AngularのRxJSを使ってデータの受け渡しをする - Qiita

RxJS を学ぼう #5 – Subject について学ぶ / Observable × Observer – PSYENCE:MEDIA