- はじめに
- 環境
- アラート表示の実装
- ObservableではなくSubjectが必要になるタイミング
- メンテナンスよりも強制バージョンアップのアラートを優先して表示する
- おまけ: どうしてSubjectにasObservableが必要なのか
- 参考
はじめに
今回は、前回の記事で作成した強制バージョンアップとメンテナンスのアラートを表示する機能に、下記の機能を追加することでリアクティブ・プログラミング用のライブラリである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(); } }
動作確認
上記のコードを実際に動かしてみます。
まず、メンテナンスのアラートを表示します。
続いて、強制バージョンアップのアラートを表示すると、メンテナンスのアラートが閉じて、強制バージョンアップのアラートだけが表示されることがわかります。期待結果通りの動きです。
おまけ: どうして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