火曜日, 6月 24, 2025
火曜日, 6月 24, 2025
- Advertisment -
ホームニューステックニュースとりあえず WearOS で動くアプリを作ってみる(ついでにスマートウォッチをWebサーバにしてみる) #Flutter - Qiita

とりあえず WearOS で動くアプリを作ってみる(ついでにスマートウォッチをWebサーバにしてみる) #Flutter – Qiita



とりあえず WearOS で動くアプリを作ってみる(ついでにスマートウォッチをWebサーバにしてみる) #Flutter - Qiita

こんにちは! menu事業部 フロントエンドエンジニアの坂井田です。

皆さんは普段スマートウォッチを使っていますか?

気づけば市場に出始めてから今年で11年1になるそうですが、「自分でアプリを作ってみたい」と思ったことはありませんか?

最初は「スマートウォッチアプリなんて難しそう…」と思っていたのですが、今回チャレンジする機会があったのでその結果をご紹介します。

watch

ちなみに今回実機でデバッグするにあたって、Ticwatch Atlas というスマートウォッチを使用しました。

開発環境の選択

SwiftやKotlinといったネイティブで開発することもできるのですが、今回は普段から使っていて慣れているFlutterで作ってみました。

スマートウォッチアプリを開発する際に便利なパッケージが pub.dev でいくつか公開されていたので、後ほどご紹介します。

PCとスマートウォッチを接続する

実機でデバッグするための設定を行います。

付属してきたケーブルはデータ通信ができず、スマートウォッチアプリを開発する際はWi-Fi経由のワイヤレスデバッグで行うようです。

スマートウォッチの設定画面からワイヤレスデバッグを有効にし、PCで以下コマンドを実行すると接続することができます。

ペアリング(初回のみ)

adb pair 192.168.XX.XX:XXXX

接続

adb connect 192.168.XX.XX:XXXX

設定方法の詳細については以下のページをご覧ください。

とりあえず、Flutterのサンプルアプリを動かしてみる

以下コマンドを実行して生成されるアプリを動かしてみます。

先程の手順で adb connect コマンドを実行すると、デバッグする端末を選択する際にスマートウォッチが出てくるので選択すると起動できます!

image

画面が小さいのと角が丸いため一部表示されていない部分はありますが、特にコードに変更を加えなくてもあっさり起動しました🚀

カウンターアプリを作ってみる

これだけでは物足りないので、勉強を兼ねてスマートウォッチにあると便利そうなカウンターアプリを作ってみます。

ここでは、リューズ(時計の横についている回転する部品)の回転に応じてカウントアップ / カウントダウンするシンプルなものにしてみます。

  • 時計回りに回転:カウントアップ
  • 反時計回りに回転:カウントダウン

pub.dev で調べたところリューズの回転を取得できる以下のパッケージを見つけたので、これを使って作ってみました。

また、リューズの回転の反応が良すぎて一気にカウントしてしまっていたため、デバウンス処理を追加できる以下のパッケージを使って間引くようにしました。

このパッケージには2種類の間引き方がありますが、今回は回転したらすぐに反映させたい処理だったので、デバウンスではなくスロットルを採用しています。

この間引き方の違いについては以下の記事が分かりやすいです。

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_debouncer/flutter_debouncer.dart';
import 'package:wearable_rotary/wearable_rotary.dart';
import 'dart:async';

void main() {
  runApp(const WearCounterApp());
}

class WearCounterApp extends StatelessWidget {
  const WearCounterApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        brightness: Brightness.dark,
        textTheme: const TextTheme(
          displayLarge: TextStyle(
            fontSize: 48,
            fontWeight: FontWeight.bold,
            color: Colors.white,
          ),
        ),
      ),
      home: const CounterScreen(),
    );
  }
}

class CounterScreen extends StatefulWidget {
  const CounterScreen({super.key});

  @override
  StateCounterScreen> createState() => _CounterScreenState();
}

class _CounterScreenState extends StateCounterScreen> {
  int _counter = 0;
  StreamSubscriptionRotaryEvent>? _rotarySubscription;
  final _throttler = Throttler();

  Color _currentColor = Colors.white;
  Color _backgroundColor = Colors.black;
  Timer? _colorResetTimer;

  @override
  void initState() {
    super.initState();
    _setupRotaryInput();
  }

  @override
  void dispose() {
    _rotarySubscription?.cancel();
    _colorResetTimer?.cancel();
    super.dispose();
  }

  void _setupRotaryInput() {
    _rotarySubscription = rotaryEvents.listen((RotaryEvent event) {
      _throttler.throttle(
        duration: Duration(milliseconds: 200),
        onThrottle: () {
          _handleRotaryEvent(event);
        },
      );
    });
  }

  void _handleRotaryEvent(RotaryEvent event) {
    if (event.direction == RotaryDirection.clockwise) {
      _incrementCounter();
      return;
    }
    if (event.direction == RotaryDirection.counterClockwise) {
      _decrementCounter();
      return;
    }
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
      _currentColor = Colors.white;
      _backgroundColor = Colors.green;
      _triggerHapticFeedback();
      _resetColorAfterDelay();
    });
  }

  void _decrementCounter() {
    setState(() {
      if (_counter > 0) {
        _counter--;
        _currentColor = Colors.white;
        _backgroundColor = Colors.red;
        _triggerHapticFeedback();
        _resetColorAfterDelay();
      }
    });
  }

  void _resetColorAfterDelay() {
    _colorResetTimer?.cancel();
    _colorResetTimer = Timer(const Duration(milliseconds: 200), () {
      if (mounted) {
        setState(() {
          _currentColor = Colors.white;
          _backgroundColor = Colors.black;
        });
      }
    });
  }

  void _triggerHapticFeedback() {
    HapticFeedback.lightImpact();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: AnimatedContainer(
        duration: const Duration(milliseconds: 200),
        curve: Curves.easeInOut,
        color: _backgroundColor,
        child: GestureDetector(
          behavior: HitTestBehavior.opaque,
          child: Center(
            child: AnimatedDefaultTextStyle(
              duration: const Duration(milliseconds: 200),
              style: TextStyle(
                color: _currentColor,
                fontSize: MediaQuery.of(context).size.width * 0.4,
                fontWeight: FontWeight.bold,
              ),
              child: Text('$_counter', textAlign: TextAlign.center),
            ),
          ),
        ),
      ),
    );
  }
}

カウントに合わせて背景色も変化するようにしてみました。

スマートウォッチはこれくらいシンプルな機能に絞って開発した方が、使い勝手が良さそうです。

image

HTTPリクエストを試す

カウントした値をPOSTリクエストで送信できるかどうか試してみます。

今回はHTTPリクエストを手軽に試すことができる以下のサービスを使用して検証しました。

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

...省略

Futurevoid> _sendCountToAPI() async {
  try {
    final url =
        'https://webhook.site/61349ae9-022e-41be-9edf-f045636e09b8?count=$_counter';

    final response = await http.post(
      Uri.parse(url),
      headers: {'Content-Type': 'application/json'},
      body: json.encode({
        'count': _counter,
        'timestamp': DateTime.now().toIso8601String(),
      }),
    );

    if (response.statusCode != 200) {
      _showErrorSnackBar(response.statusCode.toString());
      return;
    }

    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text(
          '送信完了',
          style: TextStyle(color: Colors.white, fontSize: 14),
        ),
        backgroundColor: Colors.green,
        duration: Duration(seconds: 2),
        behavior: SnackBarBehavior.floating,
      ),
    );
  } catch (e) {
    _showErrorSnackBar(e.toString());
  }
}

void _showErrorSnackBar(String error) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(
        'エラーが発生しました: $error',
        style: const TextStyle(color: Colors.white, fontSize: 14),
      ),
      backgroundColor: Colors.red,
      duration: const Duration(seconds: 3),
      behavior: SnackBarBehavior.floating,
    ),
  );
}

実際に動かしてみたところ、問題なくPOSTリクエストすることができました!

スマホアプリと同じ感覚で作れてしまうのがいいですね✨️

image

(補足)HTTP通信について

通信するためにはWi-Fiをオンにしないといけなくて、電池持ちが悪くなるのでは?という懸念を抱く方がいらっしゃるかもしれません。

実は時計のWi-Fiをオンにしている必要はなく、Bluetoothで接続しているスマホがネットに繋がっている状態であればOKのようです。

実際に、スマホが近くにある状態の場合は、スマートウォッチのWi-Fiをオフにしていても通信することができ、離れた場所に行くと通信できなくなることを確認しました。

image

ウォッチによってはバッテリー容量がスマホの 1/10 程度しかなかったりするので、これは嬉しいですね🙌

懸念点

Flutterで作るとアプリサイズが大きい

今回サンプルとして作ったカウンターアプリは、ビルドしたところ 18.6 MB ありました。

なお --tree-shake-icons --obfuscate --split-debug-info=obfuscate/android オプションを付けることで、少し軽量化することができました。(今回は 0.9 MB 減少)

スマートウォッチによってはストレージが少ないものもあるので注意が必要です🚨2

また、--target-platform オプションを付けてSoCを制限することでも容量を削減することができます。

WearOS関連のパッケージの更新が止まりがち

pub.dev にいくつか関連パッケージがあったのですが、どれも最終更新が1年以上前になっていたのが気になりました。

スマートウォッチのアプリは機能を制限したりとスマホアプリをそのまま移植することはあまりないと思うので、クロスプラットフォームで作っている方が少ないのかもしれません。

応用編

スマホアプリを作る感覚でアプリができてしまったことからも分かるように、adb install コマンドを使うことでapkファイルをインストールできることが判明したので、ちょっと遊んでみようと思います💡

スマートウォッチにSSH接続してみる

まずはコマンドを実行できるようにするために、Linuxをエミュレートできる Termux アプリのapkをダウンロードしてウォッチにインストールします。

次に Termux アプリをスマートウォッチで起動して、以下のコマンドを入力してOpenSSHのサーバをインストールします。

キーボードが小さくて入力しづらいですが、頑張ります🧑‍💻

sudo apt update
sudo apt install openssh-server

ifconfig などでIPアドレスを確認してPCからコマンドを実行すると、スマートウォッチにSSH接続することができます!!🔥

image

Webサーバにしてみる

導入方法等詳細は端折りますが、Apacheを導入することでWebサーバとしても動いちゃいます🔥🔥

PHPを動作させることもできました😇

image

まとめ

「とりあえず作ってみる」という精神で始めたスマートウォッチアプリ開発でしたが、意外とスマホアプリを作るときと同じ感覚で開発することができ、やりづらさはありませんでした。

「作ったものを身に着けて使える」という、スマホアプリにはない魅力を感じることができました✨️

興味がある方は、ぜひこれを期にチャレンジしてみるのはいかがでしょうか。

▼採用情報

レアゾン・ホールディングスは、「世界一の企業へ」というビジョンを掲げ、「新しい”当たり前”を作り続ける」というミッションを推進しています。

現在、エンジニア採用を積極的に行っておりますので、ご興味をお持ちいただけましたら、ぜひ下記リンクからご応募ください。





Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -