なぜこの記事を書いたか
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のインストール
- IDE・エディターの設定
- プロジェクトの生成・アプリの起動・ホットリロードの確認
エミュレータ上での動作検証
まずは、スマートウォッチのエミュレータ上でFlutterアプリが動くのか検証します。
スマートウォッチのAVD準備
AVD Managerを開き、「Create Virtual Device...」を押下します。
Category「Wear」から「Wear OS Round」を選択し、「Next」を押下します。
OSイメージとして、「Android API 28(Wear OS)」を選択し、「Next」を押下します。
Downloadと表示されている場合、イメージをダウンロードしてください。
Flutterアプリの実行
ここからはVisual Studio Code(以下、VSCodeと記述)上で実行します。私のVSCodeには「Dart」と「Flutter」の拡張機能をインストールしています。そのため、コマンドパレットを開き(ショートカットキー:Ctrl+Shift+p)、Flutterを入力すると、簡単にFlutterコマンドを選択・実行できます。
その中から「Flutter:New Project」を選択し、任意のディレクトリでFlutterプロジェクトを作成します。今回、私はプロジェクト名を「flutter_smartwatch」としました。
再度コマンドパレットを開き、「Flutter:Launch Emulator」で先ほど作成したAVDを選択し、デバッグモードを実行する(ショートカットキー:F5)と、スマートウォッチをエミュレートした環境でFlutterが実行されます。
ちなみに、四角型のAVDを作成し、Flutterアプリを実行すると、以下の結果が得られます。
どうやら、Flutterアプリはスマートウォッチ上でも動かせそうです。
画面の形に応じたレイアウトの実現
スマートウォッチの画面は、大きく「丸型」と「四角型」があります。アプリを実行してみわかりましたが、スマートウォッチの画面は小さいため、それぞれの形に適したUIを表示しなければ、すぐユーザビリティが落ちます。
したがって、スマートウォッチの形を判定し、画面を出し分ける処理を追加したいと思います。しかし、スマートウォッチの形を検出するFlutterプラグインを見つけられなかったので、独自に実装します。
今回はAndroid側にJavaで定義したスマートウォッチ画面の形の判定メソッドを、MethodChannelという仕組みを用いてFlutter側で呼び出す処理を実装します。
MethodChannelとは
FlutterでAndroidやiOSといったプラットフォーム側のコードを呼び出すために、MethodChannelというクラスが用意されています。以下の図のように、MethodChannelとは、Flutter⇔Android・iOSのやり取りを非同期で行う通り道であり、お互いにバイナリ形式でデータを送受信できます。
【引用】https://flutter.io/docs/development/platform-integration/platform-channels
ちなみに、やりとりできるデータの型は決まっているので注意してください。図の引用元に詳細があります。
実装方法としては、以下の通りです。
- Flutter側でMethodChannelを定義し、そのMethodChannel経由でAndroid側で定義したメソッドを呼び出す。
- Android側でMethodChannelを定義し、そのMethodChannelにFlutter側で呼び出したいメソッドを登録する。
さっそく実装してみましょう。
Flutter側の実装
MethodChannelにユニークな名前を付与し、インスタンスを生成する。MethodChannelの持つinvokeMethodメソッドで、Android側で定義した処理を呼び出すことができます。
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にスマートウォッチの形を判定するメソッドを登録します。
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メソッドを実装します。
丸型画面のスマートウォッチなら黄色、四角画面のスマートウォッチなら青色の背景を表示します。
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アプリを起動します。
ちゃんとできていましたね。
実機検証
気になりますよね。タイトルが『Flutterをスマートウォッチで動かす』なのにエミュレータでしか動かしてないと各方面から色々と言われそうです。
ということで、
買いましたよ...!Tickwatch Eです。OSはWear OS by Google 2.1です。
スペック | |
---|---|
OS | Wear OS by Google 2.1 |
メモリ | 512MB |
ストレージ | 4G |
さっそく検証しましょう。
PCからWi-Fi経由でデバッグする
スマートウォッチの設定
- 自分のスマートフォンとスマートウォッチがBluetoothでペアリングされていることを確認する。
- 設定⇒システム⇒端末情報⇒ビルド情報を7回タップ⇒設定画面に開発者向けオプションの項目が表示される。
- 設定⇒接続⇒Wi-Fi⇒接続するWi-Fiを選択する。
- 設定⇒開発者向けオプション⇒Wi-Fi経由でデバッグをオン。
- Wi-Fiがちゃんと接続されていれば、IPアドレスが表示される。このIPアドレスをメモしておく。
PCの設定
- スマートウォッチと同一ネットワークにPCが接続しているか確認する。
- 任意のディレクトリで以下のコマンドを実行する。
- adb connect 【上記でメモしたスマートウォッチのIPアドレス】
- 接続できたら以下が表示される。
- connected to :5555
- 今度はVSCodeでスマートウォッチが接続されているか確認する。
- デバッグ(ショートカットキー:F5)を開始する
- スマートウォッチにapkがインストールされるので確認する。
【参考】https://developer.android.com/training/wearables/apps/debugging
動作検証
動く、動くぞ...!
先ほどの画面と少し違うのは、文字列だけ出すのは、あまり面白くないと思ったので、加速度センサーの値を表示しているからです。動けば動くほど値が変わります。
加速度センサーの値を取得する実装
Flutterのsensorsプラグインを利用しています。
参考までにコードをあげます。
// ~省略~
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でないかな。