43
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

UL Systems (ウルシステムズ)Advent Calendar 2018

Day 4

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

Last updated at Posted at 2018-12-03

なぜこの記事を書いたか

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アプリが動くのか検証します。

スマートウォッチのAVD準備

AVD Managerを開き、「Create Virtual Device...」を押下します。
image.png

Category「Wear」から「Wear OS Round」を選択し、「Next」を押下します。
image.png

OSイメージとして、「Android API 28(Wear OS)」を選択し、「Next」を押下します。
Downloadと表示されている場合、イメージをダウンロードしてください。
image.png

任意のAVD Nameを入力し、Finishを押下します。
image.png

Flutterアプリの実行

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

image.png

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

再度コマンドパレットを開き、「Flutter:Launch Emulator」で先ほど作成したAVDを選択し、デバッグモードを実行する(ショートカットキー:F5)と、スマートウォッチをエミュレートした環境でFlutterが実行されます。

image.png
おお、動いた...

ちなみに、四角型のAVDを作成し、Flutterアプリを実行すると、以下の結果が得られます。
watch_square.png

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

画面の形に応じたレイアウトの実現

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

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

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

MethodChannelとは

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

image.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にスマートウォッチの形を判定するメソッドを登録します。

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メソッドを実装します。

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

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

image.png

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

実機検証

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

ということで、
IMG_20181202_215519.jpg
買いましたよ...!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 :5555
  3. 今度はVSCodeでスマートウォッチが接続されているか確認する。
  4. デバッグ(ショートカットキー:F5)を開始する
  5. スマートウォッチにapkがインストールされるので確認する。

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

動作検証

20181203_231112.gif

動く、動くぞ...!

IMG_20181202_225758.jpg

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

加速度センサーの値を取得する実装

Flutterのsensorsプラグインを利用しています。

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

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でないかな。

43
14
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?