Angular4 で強制的に DOM 要素の変更を検知させて画面描画を更新させたいとき

Cordova アプリで Bluetooth 通信を行う cordova-plugin-bluetoothle を使っていて遭遇し、発見したこと。

cordova-plugin-bluetoothle では、Bluetooth 接続の状況が変わるたびにネイティブコードが反応して、bluetoothle.initializePeripheral() メソッドのコールバック関数が自動的に実行されるという特殊な動きをする。

これを Angular4 の中で使ったときに、ある問題が起きた。

bluetoothle.initializePeripheral((result) => {
  // 本来はペリフェラル端末としての初期化処理が成功した時にこのコールバック関数が実行される
  
  // Angular のデータバインディングを使って、画面上にメッセージを表示する
  this.msg = 'ペリフェラル端末の初期化を行いました。';
  
  if(result.status === 'subscribed') {
    // しかし、ペリフェラル端末に対しセントラル端末から subscribe() 要求があったときなども
    // このコールバック関数が実行される
    
    // ココでデータバインディングしようとしても画面描画が更新されない
    this.msg = 'セントラル端末が subscribe し始めました';
  }
  else if(result.status === 'readRequested') {
    // セントラル端末から read() 要求があった場合も発火する
    
    // ここも画面描画が更新されず…
    this.msg = 'セントラル端末から read() 要求がありました';
  }
}, (error) => {
  // エラー時のコールバック関数 (省略)
}, {
  // パラメータ (省略)
});

初期化処理のために実行した時は、JavaScript から bluetoothle.initializePeripheral() を叩いているので、コールバック関数内でコンポーネントのプロパティを変更すれば、Angular がそれを自動検知して画面描画が自動的に更新される。つまり、「ペリフェラル端末の初期化を行いました。」のメッセージは画面に表示されるワケである。

しかし、セントラル端末から subscribe() されたり read() されたりしたときにネイティブコードが反応してコールバック関数が実行された場合、画面上で発生するイベントとは別のタイミングで動作するために Angular がプロパティの変更を検知して画面描画を更新できないようだ。

そこで色々調べたところ、ChangeDetectorRef#detectChanges() というメソッドを叩くと、強制的に画面描画を更新できるようだ。

import { ChangeDetectorRef } from '@angular/core';

// コンポーネントのコンストラクタで ChangeDetectorRef を DI しておく
constructor(private changeDetectorRef: ChangeDetectorRef) { }

// コンポーネントのメソッド
peripheral() {
  bluetoothle.initializePeripheral((result) => {
    // 本来はペリフェラル端末としての初期化処理が成功した時にこのコールバック関数が実行される
    
    // Angular のデータバインディングを使って、画面上にメッセージを表示する
    this.msg = 'ペリフェラル端末の初期化を行いました。';
    
    if(result.status === 'subscribed') {
      // しかし、ペリフェラル端末に対しセントラル端末から subscribe() 要求があったときなども
      // このコールバック関数が実行される
      
      // ココでデータバインディングしようとしても画面描画が更新されない
      this.msg = 'セントラル端末が subscribe し始めました';
      // ★そこで強制的に画面描画を更新させる
      this.changeDetectorRef.detectChanges();
    }
    else if(result.status === 'readRequested') {
      // セントラル端末から read() 要求があった場合も発火する
      
      // ここも画面描画が更新されず…
      this.msg = 'セントラル端末から read() 要求がありました';
      // ★そこで強制的に画面描画を更新させる
      this.changeDetectorRef.detectChanges();
    }
  }, (error) => {
    // エラー時のコールバック関数 (省略)
  }, {
    // パラメータ (省略)
  });
}

こんな作りにして、ChangeDetectorRef#detectChanges() を叩いてやれば良い。

これで「セントラル端末が subscribe し始めました」とか「セントラル端末から read() 要求がありました」といった、特殊なタイミングで発火したときも画面表示を更新できる。