JavaScript
reactnative

ReactNativeでAndroidのバックボタンを制御する

More than 1 year has passed since last update.

ReactNativeでAndroidのバックボタンを制御する方法

基本編

とりあえずサンプル

index.android.js
import React, { Component } from 'react';
import { AppRegistry, BackHandler, Button, View } from 'react-native';

const onPressAndroidBack = () => {
  alert('Press Android Back!');
  return true;
};

const handleAndroidBack = () => (
  BackHandler.addEventListener('hardwareBackPress', onPressAndroidBack)
);

const unhandleAndroidBack = () => (
  handleAndroidBack().remove()
);

export default class AndroidBackSample extends Component {
  render() {
    return (
      <View>
        <View style={{ margin: 10 }}>
          <Button onPress={handleAndroidBack} title="ON" />
        </View>
        <View style={{ margin: 10 }}>
          <Button onPress={unhandleAndroidBack} title="OFF" />
        </View>
      </View>
    );
  }
}

AppRegistry.registerComponent('AndroidBackSample', () => AndroidBackSample);

サンプルの解説

バックボタンの制御の追加

  • 以下のコードでバックボタン押下時に実行する処理を登録できる
BackHandler.addEventListener('hardwareBackPress', onPressAndroidBack)
  • 第一引数はイベント名
    • 適当な値でも動く。よく分からない。
  • 第二引数は実行する処理
    • trueを返すとここで渡した処理を実行するだけで終わる
    • falseを返す関数だとバックボタン押下時のデフォルトの処理が続いて実行される。

バックボタンの制御の解除

  • 以下のコードで登録した処理を解除できる
handleAndroidBack().remove()
  • 登録時の関数に対して.remove()すると解除される
  • removeEventListenerを使っても動きは同じ(remove()の中でこの処理が呼ばれてるので)
BackHandler.removeEventListener('hardwareBackPress', onPressAndroidBack)

まとめ

  • Androidのバックボタン制御の追加/削除は簡単にできる
  • サンプルではボタン押下で制御を切り替えていたが、componentWillMountで追加、componentDidUnmountで削除とかするのが実用的かも

応用編

  • BackHandlerの実装を見ながらいろいろ試してみる

BackHandlerのコード

BackHandler.android.js
'use strict';

var DeviceEventManager = require('NativeModules').DeviceEventManager;
var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');

var DEVICE_BACK_EVENT = 'hardwareBackPress';

type BackPressEventName = $Enum<{
  backPress: string,
}>;

var _backPressSubscriptions = new Set();

RCTDeviceEventEmitter.addListener(DEVICE_BACK_EVENT, function() {
  var backPressSubscriptions = new Set(_backPressSubscriptions);
  var invokeDefault = true;
  var subscriptions = [...backPressSubscriptions].reverse();
  for (var i = 0; i < subscriptions.length; ++i) {
    if (subscriptions[i]()) {
      invokeDefault = false;
      break;
    }
  }

  if (invokeDefault) {
    BackHandler.exitApp();
  }
});

var BackHandler = {
  // ①
  exitApp: function() {
    DeviceEventManager.invokeDefaultBackPressHandler();
  },
  // ②
  addEventListener: function (eventName: BackPressEventName, handler: Function): {remove: () => void} {
    _backPressSubscriptions.add(handler);
    return {
      remove: () => BackHandler.removeEventListener(eventName, handler),
    };
  },
  // ③
  removeEventListener: function(eventName: BackPressEventName, handler: Function): void {
    _backPressSubscriptions.delete(handler);
  },

};

module.exports = BackHandler;

BackHandleの3つのメソッドを見てみる

① exitApp

  exitApp: function() {
    DeviceEventManager.invokeDefaultBackPressHandler();
  },

② addEventListener

  addEventListener: function (eventName: BackPressEventName, handler: Function): {remove: () => void} {
    _backPressSubscriptions.add(handler);
    return {
      remove: () => BackHandler.removeEventListener(eventName, handler),
    };
  },
  • 第一引数はイベント名
    • 基本編でも書いたが何でもいいみたい
  • 第二引数は実行する関数
  • _backPressSubscriptions.add(handler);でバックボタン押下時に実行される処理を追加している
    • _backPressSubscriptionsは上の方で定義されている
    • var _backPressSubscriptions = new Set();
    • Setは重複した値をもたないコレクションと思っておけば大丈夫
  • returnでリスナーを削除する処理を返してくれている
    • addEventListener('xxx', yyy).remove()で呼び出せる

③ removeEventListener

  removeEventListener: function(eventName: BackPressEventName, handler: Function): void {
    _backPressSubscriptions.delete(handler);
  },
  • 第一引数はイベント名
    • 見ての通り使われていない
  • 第二引数はremoveしたい処理
  • 第二引数に渡した処理を_backPressSubscriptionsから削除している

バックボタン押下時の動き

RCTDeviceEventEmitter.addListener(DEVICE_BACK_EVENT, function() {
  // A
  var backPressSubscriptions = new Set(_backPressSubscriptions);
  // B
  var invokeDefault = true;
  // C
  var subscriptions = [...backPressSubscriptions].reverse();
  // D
  for (var i = 0; i < subscriptions.length; ++i) {
    if (subscriptions[i]()) {
      invokeDefault = false;
      break;
    }
  }
  // E
  if (invokeDefault) {
    BackHandler.exitApp();
  }
});
  • バックボタン押下のイベントを監視しているので押下されるとここの処理が実行される
  • A: backPressSubscriptionsはバックボタン押下時に実行したい処理のコレクション
    • BackHandleのメソッドで追加したり削除してたやつが入ってくる
  • B: invokeDefaultはデフォルトの処理を実行するかどうかのフラグ
  • C: backPressSubscriptionsを配列化しつつ順序をリバース(後に登録されたものから実行していくため)
  • D: 配列を順番に実行していく
    • 戻り値がfalseなら次の処理へ
    • 戻り値がtrueならそこで終了
  • E: invokeDefaultがtrue(=以下の2パターン)の場合デフォルトの処理を実行
    • 処理が何も登録されていない
    • 全ての処理がfalseを返している

ここまでの整理

BackHandlerのメソッドは以下の3つ

  • exitApp: デフォルトの処理を呼び出す
  • addEventListener: 処理を追加
  • removeEventListener: 処理を削除

バックボタン押下時の動き

  • 処理が何も登録されていない場合
    • デフォルトの処理が実行される
  • 処理が何か登録されている場合
    1. 後に登録した処理から順に実行される
    2. 実行される処理がtrueを返したらそこで終了
    3. 全部falseを返したら最後にデフォルトの処理が実行される

整理したことを試してみる

何も登録されていない場合デフォルトの処理が実行される

index.android.js
import React, { Component } from 'react';
import { AppRegistry, BackHandler, Text, View } from 'react-native';

export default class AndroidBackSample extends Component {
  render() {
    return (
      <View>
        <Text>テスト</Text>
      </View>
    );
  }
}

AppRegistry.registerComponent('AndroidBackSample', () => AndroidBackSample);
  • バックボタンを押すとアプリが閉じる
  • 当たり前の挙動

処理を2つ登録する(1つ目はtrue、2つ目もtrueを返す)

index.android.js
import React, { Component } from 'react';
import { AppRegistry, BackHandler, Text, View } from 'react-native';

const onPressAndroidBack1 = () => {
  console.log('1番目に登録');
  return true;
};

const onPressAndroidBack2 = () => {
  console.log('2番目に登録');
  return true;
};

const handleAndroidBack1 = () => (
  BackHandler.addEventListener('hardwareBackPress', onPressAndroidBack1)
);

const handleAndroidBack2 = () => (
  BackHandler.addEventListener('hardwareBackPress', onPressAndroidBack2)
);

export default class AndroidBackSample extends Component {
  componentWillMount() {
    handleAndroidBack1();
    handleAndroidBack2();
  }

  render() {
    return (
      <View>
        <Text>テスト</Text>
      </View>
    );
  }
}

AppRegistry.registerComponent('AndroidBackSample', () => AndroidBackSample);
  • バックボタン押すと「2番目に登録」だけ表示される

処理を2つ登録する(1つ目はfalse、2つ目はtrueを返す)

index.android.js
const onPressAndroidBack1 = () => {
  console.log('1番目に登録');
  return false;
};

const onPressAndroidBack2 = () => {
  console.log('2番目に登録');
  return true;
};
  • バックボタン押すと「2番目に登録」だけ表示される
  • 2つ目の方だけ実行されて終わるので両方trueの時と同じ結果

処理を2つ登録する(1つ目はtrue、2つ目はfalseを返す)

index.android.js
const onPressAndroidBack1 = () => {
  console.log('1番目に登録');
  return true;
};

const onPressAndroidBack2 = () => {
  console.log('2番目に登録');
  return false;
};
  • バックボタン押すと「2番目に登録」「1番目に登録」の順に表示される

処理を2つ登録する(1つ目はfalse、2つ目もfalesを返す)

index.android.js
const onPressAndroidBack1 = () => {
  console.log('1番目に登録');
  return false;
};

const onPressAndroidBack2 = () => {
  console.log('2番目に登録');
  return false;
};
  • バックボタン押すと「2番目に登録」「1番目に登録」の順に表示されてアプリが閉じる

まとめ

  • ライブラリのコードを読むのは面白い
  • Androidのバックボタンなんてニッチな処理をここまで書いている人はきっといないだろう