ReactNativeにはFeliCaのID(IDm)を読む機能は無いので作ってみます。
iOS13からIDm読めるようになるみたいですが、まだリリースされていないので、とりあえずAndroidのみ。
完成イメージ
機能は可能な限りシンプルに(FeliCa関連機能にフォーカスするためエラー処理とか無視)。
- UIとしてTextとButtonを2個配置。
- START POLLINGでポーリング開始。
- FeliCaを認識したらTextに表示
- STOP POLLINGでポーリング停止。
という感じ。FeliCaの認識はIntentは使わずReaderModeで処理します。
前提
- Macで作業します
- Android Studioインストール、エミュレータ作成済み
- react-native-cliインストール済み
- FeliCa raeder/write機能付、かつreader/write機能On状態の端末
プロジェクトの作成
プロジェクトの作成にはreact-nativeコマンドを利用します。
なお、expoである程度作ってejectするという手もあります。
expo ejectも試したのですが、run-androidコマンドがうまく動きませんでした(別にFeliCa関係ない)。別途調査。
react-native init FeliCaIDm
この時点でプロジェクトが正常に動くかどうか、一度試して置いたほうがいいでしょう。
Android Studioからエミュレータを起動した状態で、
react-native run-android
とするとアプリが起動します。
なお、実際のFeliCaの動作確認には「おサイフケータイ」対応のAndroid端末が必要です。adbがディバイスを認識している状態なら、上記コマンドで実機にも展開されるようです。
プロジェクトの設定
ract-native initコマンドでいろいろ生成される中にandroidというフォルダがあるので、それをAndroid Studioで開きます
Gradle Update
Updateするか?と聞かれるのでUpdateしておきましょう。
AndroidManifest.xmlの編集
NFC機能を利用するので許可します。
<uses-permission android:name="android.permission.NFC" />
build.gradele(Project:xxxx)の編集
デフォルトではminSdkVersion=16となっていますが、後で使うReaderModeは19からなので19に変更します。
minSdkVersion = 19
NativeModule開発・利用の流れ
NativeModuleの作成はおおよそ下記のような流れ。
その他、値の連携方法等は、こちらの記事が大変参考になりました。ありがとうございます。
- Module Classの作成(FeliCaModule.java):実際に利用する機能を実装する
- Package Classの作成(FeliCaPackage.java):ReactNative側から呼び出せる準備をする
- Package Classの登録(MainApplication.java):ReactNative側から呼び出せるよう登録する
- App.jsからの利用(ReactNative側)
という感じです。上3つはAndroid Studioで作業し、App.jsはお好みのエディタで作業する感じ。
実際のAndroid Stuidoのソース構造は下記のような感じになります。
なお、ソース全体はGitHubにあげてますので参考まで。
実装
では、実装していきます。
FeliCaModule.java
プロジェクト内に新規にFeliCaModuleクラスを作成し、実装します。
- getName()は実装必須
- polling startでポーリングを開始してFeliCaカードの認識を待ちます
- 認識したらcallback classのonTagDiscoveredが呼ばれるので、その中でReactとしてのEventを発火。React側にFeliCaを認識したEventと読み取ったIDを通知
- polling stopでポーリング停止。
FeliCa側の処理がなれてないと???って感じかもしれませんが、やってることは単純。
package com.felicaidm;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.util.Log;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import java.util.Formatter;
import java.util.Locale;
import javax.annotation.Nullable;
public class FeliCaModule extends ReactContextBaseJavaModule {
NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this.getReactApplicationContext());
public FeliCaModule(ReactApplicationContext reactContext){
super(reactContext);
}
@Override
//RN側でFeliCaで呼び出せる
public String getName(){
return "FeliCa";
}
@ReactMethod
//Polling開始(ReaderMode利用)
public void startPolling(Callback callback){
nfcAdapter.enableReaderMode(this.getCurrentActivity(),new MyReaderCallback(this.getReactApplicationContext()),NfcAdapter.FLAG_READER_NFC_F,null);
callback.invoke("start polling...");
}
@ReactMethod
//Polling停止
public void stopPolling(Callback callback){
nfcAdapter.disableReaderMode(this.getCurrentActivity());
callback.invoke("stop polling...");
}
//ReaderModeの引数にわたすCallbackをinner classで実装
//Callback内でEventを発火させる
private class MyReaderCallback implements NfcAdapter.ReaderCallback{
ReactApplicationContext context;
MyReaderCallback(ReactApplicationContext reactContext){
context = reactContext;
}
@Override
public void onTagDiscovered(Tag tag){
//IDm取得(tag.getId()でsystem0のIDmが取れる。複数systemがある場合は注意)
String idmString = bytesToHexString(tag.getId());
Log.d("Hoge","IDm=" + idmString);
//必要に応じて取得したtag使ってNfcF生成してtransceive()でいろいろすればよい
//渡すパラメータ定義(emitの際 WritableMapである必要があるため)
WritableMap params = Arguments.createMap();
params.putString("idm", idmString);
//sendEvent
sendEvent(context,"onTagDiscovered",params);
}
}
//SendEvent(ここでは1つしかEventが無いが、普通は複数あるので関数化しておく)
private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params){
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName,params);
}
//bytes列を16進数文字列に変換
public static String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
Formatter formatter = new Formatter(sb);
for (byte b : bytes) {
formatter.format("%02x", b);
}
return sb.toString().toUpperCase();
}
}
onTagDiscoveredの中でFeliCaコマンドを発行することでIDmの読取りだけでなくFeliCaに対するRead/Write処理も行えます。こちらなども参考にしてみてください。
FeliCaPackage.java
次にPackage。createViewManagers()とcreateNativeModules()を実装する必要があるみたい。
createViewManagers()の方は事実上何もしない。
createNativeModules()の方で、上記で作成したFeliCaModuleをモジュールリストに追加している。
package com.felicaidm;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class FeliCaPakage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext){
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext){
List<NativeModule> modules = new ArrayList<>();
//FeliCaModuleを追加
modules.add(new FeliCaModule(reactContext));
return modules;
}
}
MainApplication.java
MainApplication.javaにおいて追加したモージュールリストをApp.jsから呼べるように登録する。
作業で追加するのは1行だけ。複数のModuleやPackageを作成した場合は複数追加することになります。
package com.felicaidm;
import android.app.Application;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
import java.util.Arrays;
import java.util.List;
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
//FeliCaPackageを追加
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
+ new FeliCaPakage()
);
}
@Override
protected String getJSMainModuleName() {
return "index";
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
App.js
あとはReactNative側(App.js)で利用するだけです。
FeliCa module機能自体の呼び出しに加え、今回はデータ連携にエベントを利用しているのでその定義をしています。
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* @flow
*/
import React, {Component} from 'react';
//必要なComponentを追加
import {Platform, StyleSheet, Text, View, Button, NativeModules, NativeEventEmitter} from 'react-native';
//module取得
const { FeliCa } = NativeModules;
//event取得
const felicaEvents = new NativeEventEmitter(FeliCa);
const instructions = Platform.select({
ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu',
android:
'Double tap R on your keyboard to reload,\n' +
'Shake or press menu button for dev menu',
});
type Props = {};
export default class App extends Component<Props> {
//stateにidm定義
state = {
idm: "xxxxxxxx"
}
componentWillMount(){
//event追加
felicaEvents.addListener("onTagDiscovered",({idm})=>{
this.setState({idm:idm});
});
}
render() {
return (
<View style={{flex:1,justifyContent:'center',alignItems:'center'}}>
<Text>IDm:{this.state.idm}</Text>
<View style={{marginTop:20}}></View>
<Button
title="start polling"
onPress={()=>this.startPolling()}
/>
<View style={{marginTop:20}}></View>
<Button
title="stop polling"
onPress={()=>this.stopPolling()}
/>
</View>
);
}
//polling開始(callback定義するとかならず処理が必要みたい)
startPolling = () => {
this.setState({idm:"Reading..."});
FeliCa.startPolling(x => console.log(x));
}
//polling停止(callback定義するとかならず処理が必要みたい)
stopPolling = () => {
FeliCa.stopPolling(x => console.log(x));
}
}
簡単ではありますが以上です。
残課題など
RNはexpoから入ったので「RNいいじゃん!」と思っていたけど、expo以外の世界は闇が深い・・・。
npmパッケージを作る
ちゃんとエラー処理入れたバージョンでそのうち作ります。
expo ejectで同じことをやる
実際の開発では、ある程度expoで開発して、最後にejectして・・・とう流れかなと。
ちらっとやった感じではエラーがでた。
→最新のexpo(2.20.1)でエラーは無くなりました。
iOSでのエラーに対応
iOSで利用した場合、「このOSでは利用できません」とかやりたい。
まあ、それはPlatform判定してエラー処理すればいいのですが、そもそも
react-native run-ios
でうまく起動しません。Native module cannot be null.というエラー。
まあ、iOS側は何も実装してないので、そのための対応が必要な感じなのはわかるのですが、具体的に何をどうしたらよいのか。。。さて。分かる人教えて!!(とりあえず、iOS側でもダミーの機能実装したらいいのかな?)。
→iOS側に同じパラメータやメソッドを実装することで直りました。