Flutterをスマートウォッチで動かす

# なぜこの記事を書いたか
Flutterに関する日本語記事がここ数ヶ月でかなり増えてきました。これもFlutter人気の兆しなのかとわくわくしています。このビッグウェーブに乗ろうと思い、今回Flutterをネタに記事を投稿しようと思いましたが、すでにWebに溢れているテーマでは面白くありません。

ということで、記事がまったくみつからなかったネタ、Flutter×スマートウォッチをテーマにします。**スマートウォッチ上で、Flutterは動くか**、これを検証します。

今後、同じことを考えた物珍しい方への一助となればうれしいです。

※ちなみに今回は、Apple Watchは対象ではありません。もし試した方がいらっしゃれば、ぜひどういう結果になったか教えていただきたいです。

# バージョン
- Flutter：v0.11.10
- Java：1.8.0_102
- エミュレータのイメージ：android-wear Wear OS (API level 28)

# 開発環境の構築方法
本記事では、Flutterの開発環境の構築方法は扱いません。環境構築から行いたい方は、公式ドキュメントを参照してください。

- Flutterのインストール 
    - https://flutter.io/get-started/install/ 
- IDE・エディターの設定 
    - https://flutter.io/get-started/editor/#androidstudio 
- プロジェクトの生成・アプリの起動・ホットリロードの確認
    - https://flutter.io/get-started/test-drive/#androidstudio

# エミュレータ上での動作検証
まずは、スマートウォッチのエミュレータ上でFlutterアプリが動くのか検証します。

## スマートウォッチのAVD準備
AVD Managerを開き、「Create Virtual Device...」を押下します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155589/97db0b2f-21ca-5bdd-4529-6833a4b83b2c.png)

Category「Wear」から「Wear OS Round」を選択し、「Next」を押下します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155589/8b5b9e02-0c33-47ac-fb40-639bb5e7fffd.png)

OSイメージとして、「Android API 28(Wear OS)」を選択し、「Next」を押下します。
Downloadと表示されている場合、イメージをダウンロードしてください。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155589/ab00f4d5-ca32-3491-8730-35b023687c09.png)

任意のAVD Nameを入力し、Finishを押下します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155589/eaa05928-334f-3b88-6b6e-d5ddc077bbce.png)

## Flutterアプリの実行
ここからはVisual Studio Code（以下、VSCodeと記述）上で実行します。私のVSCodeには「Dart」と「Flutter」の拡張機能をインストールしています。そのため、コマンドパレットを開き（ショートカットキー：Ctrl+Shift+p）、Flutterを入力すると、簡単にFlutterコマンドを選択・実行できます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/155589/092448da-84b2-abe8-f557-3c6eaa39e781.png)

その中から「Flutter:New Project」を選択し、任意のディレクトリでFlutterプロジェクトを作成します。今回、私はプロジェクト名を「flutter_smartwatch」としました。

再度コマンドパレットを開き、「Flutter:Launch Emulator」で先ほど作成したAVDを選択し、デバッグモードを実行する（ショートカットキー：F5）と、スマートウォッチをエミュレートした環境でFlutterが実行されます。
 
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155589/e4755a3c-7be3-9107-2806-5515bfa558a9.png)
おお、動いた...

ちなみに、四角型のAVDを作成し、Flutterアプリを実行すると、以下の結果が得られます。
![watch_square.png](https://qiita-image-store.s3.amazonaws.com/0/155589/801cfddd-a001-a39e-38df-0eecec1d01fa.png)

どうやら、Flutterアプリはスマートウォッチ上でも動かせそうです。

# 画面の形に応じたレイアウトの実現
スマートウォッチの画面は、大きく「丸型」と「四角型」があります。アプリを実行してみわかりましたが、スマートウォッチの画面は小さいため、それぞれの形に適したUIを表示しなければ、すぐユーザビリティが落ちます。

したがって、スマートウォッチの形を判定し、画面を出し分ける処理を追加したいと思います。しかし、スマートウォッチの形を検出するFlutterプラグインを見つけられなかったので、独自に実装します。

今回はAndroid側にJavaで定義したスマートウォッチ画面の形の判定メソッドを、MethodChannelという仕組みを用いてFlutter側で呼び出す処理を実装します。

## MethodChannelとは
FlutterでAndroidやiOSといったプラットフォーム側のコードを呼び出すために、[MethodChannel](https://docs.flutter.io/flutter/services/MethodChannel-class.html)というクラスが用意されています。以下の図のように、MethodChannelとは、Flutter⇔Android・iOSのやり取りを非同期で行う通り道であり、お互いにバイナリ形式でデータを送受信できます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/155589/b95a2675-85ca-e7bc-5a84-544be7145b64.png)
【引用】https://flutter.io/docs/development/platform-integration/platform-channels

ちなみに、やりとりできるデータの型は決まっているので注意してください。図の引用元に詳細があります。

実装方法としては、以下の通りです。
1. Flutter側でMethodChannelを定義し、そのMethodChannel経由でAndroid側で定義したメソッドを呼び出す。
2. Android側でMethodChannelを定義し、そのMethodChannelにFlutter側で呼び出したいメソッドを登録する。

さっそく実装してみましょう。

## Flutter側の実装

MethodChannelにユニークな名前を付与し、インスタンスを生成する。MethodChannelの持つinvokeMethodメソッドで、Android側で定義した処理を呼び出すことができます。

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

// ～MethodChannelに関係ないコードは省略～

class _MyHomePageState extends State<MyHomePage> {
  // MethodChannleの定義
  // 文字列を指定し、一意に特定する。
  static const platform = MethodChannel("com.example/shape");

  Future<Shape> detectWatchShape() async {
    try {
      // invokeMethodでプラットフォーム側（Android・iOS）で定義したメソッドを呼び出す。
      final int result = await platform.invokeMethod("detectWatchShape");
      return result == 0 ? Shape.round : Shape.square;
    } on PlatformException catch (e) {
      print(e);
      // デフォルトを四角に設定
      return Shape.square;
    }
  }

  // ～buildメソッドは省略～
}

// 画面の形が丸型（Shape.round）か四角型（Shape.square）かを表すenum型の定義。
enum Shape { round, square }

```

## Android側の実装
Android側でもMethodChannelで定義します。このMethodChannelにスマートウォッチの形を判定するメソッドを登録します。

```java:MainActivity.java
package com.example.fluttersmartwatch;

import android.os.Bundle;
import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.*;
import io.flutter.plugins.GeneratedPluginRegistrant;

public class MainActivity extends FlutterActivity {
  // MethodChannel名の指定。
  // Flutter側で呼び出す際に指定する文字列と合わせる。
  private static final String CHANNEL = "com.example/shape";
  
  private static final int CIRCLE = 0;
  private static final int SQUARE = 1;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);

    // MethodChannelの定義
    new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(new MethodCallHandler() {
      // Flutter側でinvokeMethodが呼ばれたときに実行される。
      @Override
      public void onMethodCall(MethodCall call, MethodChannel.Result result) {
        // MethodCallのmethodプロパティ内に、Flutter側でinvokeMethodメソッドに
        // 引数として渡した文字列が格納されているので、文字列比較で実行する処理を判定します。
        if(call.method.equals("detectWatchShape")) {
          // ここでスマートウォッチの形を判定する。
          // 丸型なら0を返し、四角型なら1を返す。
          if(getFlutterView().getResources().getConfiguration().isScreenRound()) {
            // Flutter側に返したい値を引数に渡す。
            result.success(CIRCLE);
          } else {
            // Flutter側に返したい値を引数に渡す。
            result.success(SQUARE);
          }
        } else {
          result.notImplemented();
        }
      }
    });
  }
}
```

## 動作検証
上記コードで実際にスマートウォッチの形を判別できているか試します。それぞれ異なる画面を表示するよう、_MyHomePageStateクラスにbuildメソッドを実装します。

丸型画面のスマートウォッチなら黄色、四角画面のスマートウォッチなら青色の背景を表示します。

```dart:main.dart
class _MyHomePageState extends State<MyHomePage> {
  // detectWatchShapeメソッドは省略

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FutureBuilder(
        // 上記で定義した画面の形を判定するメソッドを設定する。
        future: detectWatchShape(),
        builder: (BuildContext context, AsyncSnapshot<Shape> snapshot) {
          double height = MediaQuery.of(context).size.height;
          double width = MediaQuery.of(context).size.width;

          switch(snapshot.data) {
            case Shape.round:
              return Container(
                height: height,
                width: width,
                child: Center(child: Text("ROUND")),
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  color: Colors.yellow,
                ),
              );
            case Shape.square:
              return Container(
                height: height,
                width: width,
                child: Center(child: Text("SQUARE")),
                decoration: BoxDecoration(
                  shape: BoxShape.rectangle,
                  color: Colors.blue,
                ),
              );
            default:
              return Container(color: Colors.red);
          }
        },
      )
    );
  }
}
```

丸型と四角型の2つのエミュレータでFlutterアプリを起動します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155589/cc5c713d-2394-3f78-d84e-18bd2f8cd925.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/155589/ea078342-8f4e-e5b1-cfb8-721bcd44a9a6.png)

ちゃんとできていましたね。

# 実機検証
気になりますよね。タイトルが『**Flutterをスマートウォッチで動かす**』なのにエミュレータでしか動かしてないと各方面から色々と言われそうです。

ということで、
![IMG_20181202_215519.jpg](https://qiita-image-store.s3.amazonaws.com/0/155589/b3602c71-6d14-a50a-0932-9167e0d625c1.jpeg)
買いましたよ...！Tickwatch Eです。OSはWear OS by Google 2.1です。

|スペック||
|---|---|
|OS|Wear OS by Google 2.1|
|メモリ|512MB|
|ストレージ|4G|

さっそく検証しましょう。

## PCからWi-Fi経由でデバッグする
### スマートウォッチの設定
1. 自分のスマートフォンとスマートウォッチがBluetoothでペアリングされていることを確認する。
2. 設定⇒システム⇒端末情報⇒ビルド情報を7回タップ⇒設定画面に開発者向けオプションの項目が表示される。
3. 設定⇒接続⇒Wi-Fi⇒接続するWi-Fiを選択する。
4. 設定⇒開発者向けオプション⇒Wi-Fi経由でデバッグをオン。
    - Wi-Fiがちゃんと接続されていれば、IPアドレスが表示される。このIPアドレスをメモしておく。
    
### PCの設定
1. スマートウォッチと同一ネットワークにPCが接続しているか確認する。
2. 任意のディレクトリで以下のコマンドを実行する。
    - adb connect 【上記でメモしたスマートウォッチのIPアドレス】
    - 接続できたら以下が表示される。
    - connected to <IPアドレス>:5555
3. 今度はVSCodeでスマートウォッチが接続されているか確認する。
4. デバッグ（ショートカットキー：F5）を開始する
5. スマートウォッチにapkがインストールされるので確認する。

【参考】https://developer.android.com/training/wearables/apps/debugging

## 動作検証
![20181203_231112.gif](https://qiita-image-store.s3.amazonaws.com/0/155589/391e9528-be1a-215c-dea3-d5cddfc732c1.gif)

動く、動くぞ...！

![IMG_20181202_225758.jpg](https://qiita-image-store.s3.amazonaws.com/0/155589/66b20fa9-62d5-71cc-92aa-0560e43f5581.jpeg)

先ほどの画面と少し違うのは、文字列だけ出すのは、あまり面白くないと思ったので、加速度センサーの値を表示しているからです。動けば動くほど値が変わります。

## 加速度センサーの値を取得する実装
Flutterの[sensorsプラグイン](https://pub.dartlang.org/packages/sensors#-readme-tab-)を利用しています。

参考までにコードをあげます。

```main.dart
// ～省略～
return StreamBuilder(
  // accelerometerEventsは、Sensorsプラグインの提供するメソッドです。
  // Stream<AccelerometerEvent>を返します。
  // StreamBuilderを使うことで、Streamから新たなAccelerometerEventを受け取るたびに、
  // 画面を再描画します。
  stream: accelerometerEvents,
  builder: (context, AsyncSnapshot<AccelerometerEvent> eventSnapshot) {
    // 加速度の値が取得できなかった場合、ローディング画面を表示する。
    if(!eventSnapshot.hasData) return Center(child: CircularProgressIndicator(),);

    // AccelerometerEventは、加速度センサーの値を保持するオブジェクトです。
    // このオブジェクト経由でx軸、y軸、z軸の加速度を取得できます。
    AccelerometerEvent event = eventSnapshot.data;

    // shapeは、Shapeというenum型のインスタンス。
    // 画面の形が丸型（Shape.round）か四角型（Shape.square）かを格納している。
    // 画面が四角だった場合のコードは省略する。
    switch(shape) {
      case Shape.round:
        return Container(
          height: height,
          width: width,
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text("ROUND"),
                Text("x: ${event.x}"),
                Text("y: ${event.y}"),
                Text("z: ${event.z}")
              ],
            ),
          ),
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: Colors.yellow,
          ),
        );
      // ～省略～
    }
  },
);
```

## アプリサイズの比較
基本的に、Flutterアプリは大きくなる傾向があります。他のスマートウォッチアプリと比較します。

|アプリ|アプリサイズ|
|---|---|
|Wear OS|26.33MB|
|Flutterアプリ|25.76MB|
|Google Playストア|17.65MB|
|Google Play Music|10MB|
|Google MAP|1.74MB|

画面表示+加速度センサーの値の取得表示だけのアプリなのですが、やはり大きいですね。限られたリソースしか持たないスマートウォッチよりも、やはりFlutterは「モバイル」のためのフレームワークなんですよ。

# まとめ
今回、興味本位でFlutterをスマートウォッチで動かしてみました。

Flutterは、Wear OSでも動きました。通常のモバイルアプリのように開発できます。しかし、Flutterはあくまでもモバイルアプリ用のフレームワークなので、スマートウォッチ用のプラグインがほとんどありません。つまり、こだわればこだわるほど、独自にAndroid・iOSのコードを書く必要があります。

さらに、Flutterアプリは、ちょっとしたものでもアプリサイズが大きくなる傾向があります。モバイルより限られたリソースのスマートウォッチのアプリとしては、適切ではなさそうです。

これはFlutterが悪いとかではなく、単純に向き不向きの問題です。なんていったって、Flutterはモバイルアプリ用のフレームワークなのですから、スマートウォッチに最適化されていなくて当然です。Flutter自体は最高です。はやくバージョン1でないかな。