FlutterからKotlin/Swiftのネイティブコードを呼んでみた

# FlutterからネイティブAPIを呼びたい
[MethodChannel](https://api.flutter.dev/flutter/services/MethodChannel-class.html)を使って、FlutterからKotlin（Android）とSwift（iOS）のネイティブコード呼び出しを試してみました。

[Writing custom platform-specific code](https://flutter.dev/docs/development/platform-integration/platform-channels)に記載のサンプルコードを動かしてみたので、実際の流れをメモしておきます。

Java/Objective-C版はこちら
[FlutterからJava/Objective-Cのネイティブコードを呼んでみた - Qiita](https://qiita.com/unsoluble_sugar/items/9282f1d3ce9ca38daf85)

サンプルソースはこちら
https://github.com/unsolublesugar/flutter-sample-custom-platform-specific-code

# サンプルプロジェクトの作成
まずはサンプルプロジェクトを作成します。サンプルアプリでは、バッテリー残量を取得するネイティブコードを実装します。
`$ flutter create batterylevel`

新規プロジェクト作成時にKotlinとSwiftとがサポートされていなかった場合は、以下フラグ付きコマンドで作成しなおしてください。

`$ flutter create -i swift -a kotlin batterylevel`

※ドキュメントによると、デフォルトでJava、Objective-Cがサポートされているとのことですが、v1.9.1ではオプションなしでKotlinとSwiftが使えました。

```
-i, --ios-language             [objc, swift (default)]
-a, --android-language         [java, kotlin (default)]
```

# Flutter側の呼び出しコード

サンプル画面のWidgetとバッテリー残量取得メソッドの実装を加えます。以下、`_MyHomePageState`のみ抜粋した`main.dart`ファイルです。

```main.dart
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
...
class _MyHomePageState extends State<MyHomePage> {
  static const platform = const MethodChannel('samples.flutter.dev/battery');

  String _batteryLevel = 'Unknown battery level.';

  Future<void> _getBatteryLevel() async {
    // Get battery level.
    String batteryLevel;
    try {
      final int result = await platform.invokeMethod('getBatteryLevel');
      batteryLevel = 'Battery level at $result % .';
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get battery level: '${e.message}'.";
    }

    setState(() {
      _batteryLevel = batteryLevel;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            RaisedButton(
              child: Text('Get Battery Level'),
              onPressed: _getBatteryLevel,
            ),
            Text(_batteryLevel),
          ],
        ),
      ),
    );
  }
}
```

`MethodChannel`で呼び出すバッテリー残量取得メソッド`getBatteryLevel`は、KotlinとSwift側でそれぞれ定義していきます。


なお、ひとつのアプリで使用されるチャンネル名は、一意である必要があります。チャンネル名の前にはドメインプレフィックスを付けます。
`MethodChannel('samples.flutter.dev/battery');`

# Kotlin側のネイティブコード（Android）
それではKotlin（Android）のコードを書いていきましょう。

## 必要なimportの追加

`MainActivity.kt`にimportを追加。

```MainActivity.kt
import android.os.Bundle
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES

import io.flutter.app.FlutterActivity
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.plugin.common.MethodChannel
```

## getBatteryLevelを実装

`MainActivity`の`onCreate`メソッド内で`MethodChannel`を定義し、`setMethodCallHandler`を呼び出します。


```MainActivity.kt
class MainActivity: FlutterActivity() {
  private val CHANNEL = "samples.flutter.dev/battery"

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    GeneratedPluginRegistrant.registerWith(this)

    MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
    }
  }
}

```
ここで定義したチャンネル名`samples.flutter.dev/battery`を、Flutter側の呼び出しで使用します。


Android端末のバッテリーレベルを取得するコードを追加します。

```MainActivity.kt

  private fun getBatteryLevel(): Int {
    val batteryLevel: Int
    if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
      val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
      batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
    } else {
      val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
      batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
    }
    return batteryLevel
  }

```

`getBatteryLevel()`を`MethodChannel `で呼び出すコードを追加。


```MainActivity.kt
  private val CHANNEL = "samples.flutter.dev/battery"

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    GeneratedPluginRegistrant.registerWith(this)

    MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
      if (call.method == "getBatteryLevel") {
        val batteryLevel = getBatteryLevel()
        if (batteryLevel != -1) {
          result.success(batteryLevel)
        } else {
          result.error("UNAVAILABLE", "Battery level not available.", null)
        }
      } else {
        result.notImplemented()
      }
    }
  }
```

これでAndroid用Kotlinネイティブコードの呼び出し準備は完了です。

# エミュレーター上で実行

上記コードをエミュレーター上でbuild runすると「Get Battery Level」ボタンと「Unknown battery level.」テキストが表示されます。

<img width="429" alt="スクリーンショット 2019-11-05 15.02.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6380/283a2b22-531b-55f7-1c2c-f0d950496848.png">

ボタンタップで、端末のバッテリー残量を取得。テキスト反映されます。
<img width="429" alt="スクリーンショット 2019-11-05 15.02.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6380/aeb8f205-a0e1-1a01-5eb4-9ddfb9f1f2ca.png">

サクッと呼べましたね。

# Swift側のネイティブコード（iOS）
続いてSwift（iOS）のコードを書いていきます。
## XcodeでAppDelegate.swiftを開く
Xcodeを起動し、対象プロジェクト内のiOSフォルダを開きます。`Runner`フォルダ配下にある`AppDelegate.swift`を開きます。
<img width="695" alt="スクリーンショット 2019-11-05 15.33.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6380/3602a28a-6451-8707-9714-e013fa6e7b89.png">

## getBatteryLevelを実装

Androidと同じく`MethodChannel`の実装を定義します。

```AppDelegate.swift
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let batteryChannel = FlutterMethodChannel(name: "samples.flutter.dev/battery",
                                              binaryMessenger: controller.binaryMessenger)
    batteryChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      // Note: this method is invoked on the UI thread.
      // Handle battery messages.
    })

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}
```

バッテリーレベルの取得メソッド`receiveBatteryLevel`を追加。

```AppDelegate.swift
private func receiveBatteryLevel(result: FlutterResult) {
  let device = UIDevice.current
  device.isBatteryMonitoringEnabled = true
  if device.batteryState == UIDevice.BatteryState.unknown {
    result(FlutterError(code: "UNAVAILABLE",
                        message: "Battery info unavailable",
                        details: nil))
  } else {
    result(Int(device.batteryLevel * 100))
  }
}
```

`setMethodCallHandler`で`receiveBatteryLevel`を呼び出します。

```AppDelegate.swift
batteryChannel.setMethodCallHandler({
  [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
  // Note: this method is invoked on the UI thread.
  guard call.method == "getBatteryLevel" else {
    result(FlutterMethodNotImplemented)
    return
  }
  self?.receiveBatteryLevel(result: result)
})
```

iOS Simulatorを起動すると、Android同様にボタンとテキストが表示されます。
<img width="439" alt="スクリーンショット 2019-11-05 15.44.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6380/042e6350-65f7-eae5-de30-55071e4e57cc.png">

ボタンをタップするとMethodChannel経由で`receiveBatteryLevel()`が呼び出されテキストが更新されます。
<img width="439" alt="スクリーンショット 2019-11-05 15.45.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6380/99bf72ac-21f4-e1ab-66c8-cf060bbf6241.png">
残念ながらiOS Simulator上ではバッテリーAPIがサポートされていないようですが「Battery info unavailable」というSwift側で書いたコードが動いていることがわかりますね。

# ネイテイブからFlutterへのコールバック通知について
ネイテイブからFlutter側へ状態変化等のコールバック通知を行うには、EventChennelという仕組みを利用するそうです。

[EventChannel class - services library - Dart API](https://api.flutter.dev/flutter/services/EventChannel-class.html)

こちらはまだ試していないので、別途触ってみたいと思います。