flutterで端末の気圧センサーを用いたpluginを自作して公開してみた

# 

# 下準備

まずこれを呼び出します。


`flutter create --template=plugin --platforms=android,ios -a java flutter_barometer`

テンプレート作成時に追加されている、 `example` ディレクトリで

`flutter build apk`

`flutter build ios --no-codesign`

を実行し、事前にアプリをビルドします。

# 作成

### Android
Android Studioで `[プラグイン名]/example/android/build.gradle` を選択してプロジェクトを開きます。

サイドバーにある`[プラグイン名]/java/[パッケージ名]/[プラグイン名]Plugin` というファイルに実装していきます。

### iOS

Xcodeで `[プラグイン名]/example/ios/Runner.xcworkspace` を選択してプロジェクトを開きます。

サイドバーにある

`Pods/Development Pods/hello/../../example/ios/.symlinks/plugins/hello/ios/Classes/Swift[プラグイン名]Plugin.swift`

を選択して実装していきます。



## Android側の実装

javaを用いて実装しました。

``` java:FlutterBarometerPlugin.java
public class FlutterBarometerPlugin implements FlutterPlugin, MethodCallHandler, SensorEventListener {
    private MethodChannel channel;
    private EventChannel stream;
    private SensorManager sensorManager;
    private EventChannel.EventSink sinkOnChanged;
    public Sensor pressureSensor;
    private Context applicationContext = null;

    @Override
    public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
        applicationContext = null;
        sensorManager.unregisterListener(this);
        channel.setMethodCallHandler(null);
    }

    // センサー値が変わった時
    @Override
    public void onSensorChanged(SensorEvent sensorEvent) {
        double pressure = sensorEvent.values[0];
        if (sinkOnChanged != null) {
            sinkOnChanged.success(pressure);
        }
    }

    // センサー値の精度が変わった時
    @Override
    public void onAccuracyChanged(Sensor sensor, int i) {

    }

    // センサーの初期化処理. 公式document参照.
    private boolean initializePressureSensor() {
        sensorManager = (SensorManager) applicationContext.getSystemService(Context.SENSOR_SERVICE);
        if (sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE) != null) {
            pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
            sensorManager.registerListener(this, pressureSensor, SensorManager.SENSOR_DELAY_NORMAL);
            return true;
        } else {
            pressureSensor = null;
            return false;
        }
    }

    private EventChannel.StreamHandler handler() {
        return new EventChannel.StreamHandler() {
            @Override
            public void onListen(Object listener, EventChannel.EventSink events) {
                if (initializePressureSensor()) {
                    sinkOnChanged = events;
                }
            }

            @Override
            public void onCancel(Object args) {
                sinkOnChanged = null;
            }
        };
    }

    // 最初に呼ばれる初期化処理
    @Override
    public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
        applicationContext = flutterPluginBinding.getApplicationContext();
        channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "flutter_barometer");
        channel.setMethodCallHandler(this);
        stream = new EventChannel(flutterPluginBinding.getBinaryMessenger(), "pressureStream");
        stream.setStreamHandler(handler());
    }

    // Flutter側から呼ばれた時発動
    @Override
    public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
        // getPlatFormVersionが呼ばれた時
        if (call.method.equals("getPlatformVersion")) {
            // この値が返る
            result.success("Android " + android.os.Build.VERSION.RELEASE);
            return;
        }
        result.notImplemented();
    }
}

```

まず最初にonAttachedToEngine()が呼ばれ、その中でMethodChannelとEvanChannelが初期化されます。その後FlutterからMethodChannel.invokeMethod()が呼ばれた時にonMethodCallが呼び出され、result.success()で引数に値を入れる事でFlutter側に値を返すことができます(Future型)。また、sinkOnChanged.success()の引数に値を入れることでFlutter側に値を返すことができます(Stream型)。Stream型でリアルタイムに値を返すことができるので端末のセンサーを用いたアプリを開発したい場合こちらになるかと思います。
## iOS側の実装
``` swift:SwiftFlutterBarometerPlugin.swift
import Flutter
import UIKit
import CoreMotion

public class SwiftFlutterBarometerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
    var altimeter:CMAltimeter?
    var pressure:Double = 0.0
    var sinkOnChanged: FlutterEventSink?
    var listeningOnChanged = false

    // ここで初期化していいのかはわかりません.取りあえず動きます.
    override init() {
        super.init()
        altimeter = CMAltimeter()
        getCurrentPressure()
    }

    // センサー値を取得
    private func getCurrentPressure() {
        if(CMAltimeter.isRelativeAltitudeAvailable()) {
            altimeter!.startRelativeAltitudeUpdates(
                to: OperationQueue.main,
                withHandler:
                    {data, error in
                        if error == nil {
                            let pressure:Double = data!.pressure.doubleValue
                            self.pressure =  pressure * 10
                            self.onChanged(value: self.pressure);
                        }
                    }
            )
        }
    }
    
    //まずこれが呼ばれる
    public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(name: "flutter_barometer", binaryMessenger: registrar.messenger())
        let stream = FlutterEventChannel(name: "pressureStream", binaryMessenger: registrar.messenger())
        let instance = SwiftFlutterBarometerPlugin()
        registrar.addMethodCallDelegate(instance, channel: channel)
        stream.setStreamHandler(instance)
    }
    
    // EventChannel 初期化
    public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        self.sinkOnChanged  = events
        listeningOnChanged = true
        return nil
    }
    
    // EventChannel 終了処理
    public func onCancel(withArguments arguments: Any?) -> FlutterError? {
        listeningOnChanged = false
        return nil
    }
    
    // EventChannel 値を流す
    public func onChanged(value: Double) {
        if listeningOnChanged {
            if let sink = self.sinkOnChanged {
                sink(value)
            }
        }
    }
    
    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        // これが呼ばれた時に
        if call.method == "getPlatformVersion" {
            // これを返す
            result("iOS " + UIDevice.current.systemVersion)
        }
        
        if call.method == "getCurrentPressure" {
            result(self.pressure)
        }
    }
}
```
swiftもAndroid側のjavaと同様、まずregister()が走り、その後MethodChannel.invokeMethodが呼ばれた時はhandle()が呼ばれてFlutter側にFuture型で値を返し、sinkOnChanged()ではStream型で値を返します。
## Dart側の実装

``` dart:lib/flutter_barometer.dart
import 'dart:async';

import 'package:flutter/services.dart';

class FlutterBarometer {
  static const MethodChannel _channel =
      const MethodChannel('flutter_barometer');
  static const EventChannel _barometerEventChannel =
      EventChannel('pressureStream');

  static var _onPressureChanged;

  static Future<String?> get platformVersion async {
    final String? version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }

  static Future<double> get currentPressue async {
    final pressure = await _channel.invokeMethod('getCurrentPressure');
    return pressure;
  }

  static Stream<double> get currentPressueEvent {
    if (_onPressureChanged == null) {
      _onPressureChanged = _barometerEventChannel.receiveBroadcastStream().map(
            (element) => element as double,
          );
    }
    return _onPressureChanged;
  }
}

```

currentPressueEventではEventChanelでsinkされた値をreceiveBroadcastStream()でリアルタイムで受け取るようにしています。これでセンサー値をリアルタイムで受け取れます。

``` dart:example/lib/main.dart
import 'package:flutter/material.dart';
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:flutter_barometer/flutter_barometer.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String _platformVersion = 'Unknown';
  double _currentPressure = 0.0;

  @override
  void initState() {
    super.initState();
    initPlatformState();
    FlutterBarometer.currentPressueEvent.listen((event) {
      setState(() {
        _currentPressure = event;
      });
    });
  }

  Future<void> initPlatformState() async {
    String platformVersion;
    try {
      platformVersion =
          await FlutterBarometer.platformVersion ?? 'Unknown platform version';
    } on PlatformException {
      platformVersion = 'Failed to get platform version.';
    }
    if (!mounted) return;
    setState(() {
      _platformVersion = platformVersion;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('flutter_barometer'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                'Running on: $_platformVersion\n',
                style: TextStyle(fontSize: 30),
              ),
              Text(
                '${(_currentPressure * 1000).round() / 1000} hPa',
                style: TextStyle(
                  fontSize: 70,
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}
```
こんな感じで使います。

# いざ公開

外部へ公開する場合、以下の4点が必要です。

- README.md （そのプラグインの解説を記載したファイル）
- CHANGELOG.md （各バージョンでの変更を記載したファイル）
- LICENSE （そのプラグインのライセンスを記載したファイル）
- パブリックにしているAPIに対してのコメントの記述(今回`lib/flutter_barometer.dart`に何も記述していないのでpub pointが減点されている。)

公開するプラグインが問題ないかを事前に --dry-run をおこなってチェックし、

`flutter pub publish --dry-run`

以下のコマンドで公開します。

`flutter pub publish `

一度公開してしまうと削除できないので気をつけて下さい。

# 成果物

https://pub.dev/packages/flutter_barometer_plugin

# 参考文献

https://qiita.com/napo/items/caee087c6b8dbe510e87

https://qiita.com/bellx2/items/7ed392d9a6b769566620