Stay Home 中に Flutter で アプリ作ってみた

Stay Home 中に Flutter で アプリ作ってみた

はじめに

はじめまして。鯖江拠点の大友です。
昨今の事情で、うちの子供(小3)が通う小学校が休校、私も在宅勤務で、ずっとStay Homeしています。
そのおかげか、以前より子供とコミュニケーションを取る機会が増えました。
話題の9割は、某無人島移住ゲームの話題なんですが…
※島民代表は子供の方で、私は居候の身です。
小学校が休校でも、しっかり宿題はでるようで、平日は何かしら宿題をしています。
そのうちの1つに、”計算カード”というものがあります。
単語の暗記カードのようなものに式と答えが書いてあり、それを答えを読み上げながら1枚ずつめくっていき、どれだけの時間がかかるか記録します。
よくスマホのストップウォッチで時間を計測しています。
1年生や2年生は、足し算、引き算でしたが、3年生は掛け算です。
高速でカードをめくって、答えを読み上げてますが、正誤のチェックまでしていないので、そのへんは性善説で…

計算カード

2020年の年明けから、業務でFlutterを使ったAndroidアプリ開発をしています。
そのノウハウを活かしつつ、かんたんなアプリを作ってみたいと思ってたので、これを機に、”計算カード”をネタにアプリを作ってみました。

Flutterって?

Flutterについて、かんたんに。

  • Googleが作ったフレームワーク
  • Android,iOSのアプリを1つのコードで開発できる
    • もちろん、Android, iOSで固有のコードを書かないといけないところもある 
    • どうやらFlutter for Webがあるらしい(未踏の地)
  • コードは主にDart
    • ちょっとしたDartの文法や動作を確認したかったら、こちらがおすすめ
    • DartもGoogleが開発した言語
    • 個人的にはJavaとC#を足して割った感じ、どちらかというとJava寄り?
    • Java歴の長い私には特に抵抗はなかった
  • HotReload超早い
    • コードを書きながら修正した箇所をエミュレータや実機にすぐ反映できる(全てではない)

Flutterの開発環境を準備

早速Flutterを開発する環境を構築していきましょう。
ちなみに、開発はmac(macOS Catalina 10.15.4)で行っていきます。

まずは、Flutterのインストール。
ここ から zip をダウンロードするか、git clone し path を通しましょう。
※Tools に bash と書いてありますが、zshに移行済みのmacでも問題なく動作していました。

次に、Android Studio を インストールします。
Android StudioはエミュレーターやAndroid SDKを設定するために使います。
Configure->AVD Manager から エミュレーター を作ってください。

Android Studio


Android Studioでも開発できますが、私はVSCode派なので、コーディング、デバッグはそちらでやることにします。

VSCodeはFlutter拡張があるので、それをインストールしてください。

Flutter拡張

ここまでできたら、Flutter の診断ツールを実行してみましょう。

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, v1.17.1, on Mac OS X 10.15.4 19E287, locale ja-JP)

[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
[!] Xcode - develop for iOS and macOS (Xcode 11.4.1)
    ✗ CocoaPods not installed.
        CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that responds to your plugin usage on the Dart side.
        Without CocoaPods, plugins will not work on iOS or macOS.
        For more info, see https://flutter.dev/platform-plugins
      To install:
        sudo gem install cocoapods
[✓] Android Studio (version 3.6)
[✓] VS Code (version 1.45.1)
[✓] Connected device (2 available)

今回は、iOSの開発はしないので、Xcodeの設定はしていません。(そのうちやってみよう)
場合によっては、Android toolchainに[!]がついてる場合がありますが、その場合は以下のコマンドを実行してみましょう。

$ flutter doctor --android-licenses

デバッグを実機でやりたい場合は、実機側の開発者向けオプションの設定をしておきましょう。

診断ツールの必要な項目がクリアになったら、プロジェクトを作っていきましょう。
以下は、Android側のコードをKotlin、iOS側のコードをSwiftで書き出すというコマンドです。

$ flutter create -i swift -a kotlin keisan_card_app

VSCodeで作成したプロジェクトを開くと、pubspec.yamlに記載されている、関連するパッケージのインストールが自動で始まります。
コンパイルエラーがない状態で、おもむろにF5(実行)をしてみると、エミュレーターが起動し、サンプルアプリが起動します。

Flutter大地に立つ

右下のボタンを押すと、真ん中の数字が増えていく簡単なものです。
コードを見てもらうとわかりますが、少ないコードでMaterialっぽいUIが作れたりします。
FlutterではそれぞれのコンポーネントのことをWidgetと呼びます。
プロジェクトを作るとMaterialデザインがデフォルトになってしまいますが、Cupertino(iOS風)デザインも簡単に作れたりします。

ざっと駆け足ですが、開発する環境はこれで整いました。

つくってみる

さっそくつくってみましょう。
今回はAndroid版のみとして、GooglePlayには公開しません。
Emulator、実機で動作するところをゴールにしています。
iOS版やストアに公開などは、また機会があれば…

ざっと簡単に今回作るアプリの仕様はこんな感じです。

  • 式の出現順は毎回ランダムにシャッフル
  • スタートでストップウォッチを開始し、タイムを表示する
  • 式が表示されるが、答えは伏せた状態
  • 次の式を表示できる
  • 残りの枚数が表示される
  • 最後までいったらストップウォッチを止める

ポイントとしては、状態管理でしょうか。

Flutterでの状態管理はいくつか手法があります。
サンプルアプリであるStatefulWidgetを使って、setStateで状態を更新する方法や、ScopedModelを使う方法、BLoCパターンを使う方法やReduxなどなど…
公式で公開されている内容はこんな感じです。
全部試したことはないですが、setState、ScopedModel、BLoCを使った感覚では、setStateが一番シンプルですが、複雑、それなりの規模のものを作るには少し使いづらいです。
ScopedModelは、状態というより、ロジックどこからでも値をセットしたり、ゲットしたりすることに使ってます。(それを状態だということも…)
BLoC(Business Logic Component)については、純粋なView操作と、それ以外少々複雑化しそうなロジックを分離することができるので、ロジックがスッキリします。ただ、Streamで状態を監視してるので、それを受け取ったりするところが煩雑なロジックになりがちなところが少々難点かと感じてます。(私のコーディング力がないとも言える…)

今回はsetStateでも十分に簡単に実装できたんですが、あえてBLoCパターンで実装しました。

Flutterは特にファイル構成に縛りはないので、自由にファイルを分割することができます。それ故にしっかりルール化しておかないと、大規模になったときにえらい目に合います…
今回は画面部分とロジック部分と計算カードクラスの3つに分けてみました。

ファイル構成

本来なら、しっかりディレクトリ掘って、それぞれ管理すべきですが、今回はご容赦を…

  • main.dart
    • 起動時に実行される
  • keisan_card.dart
    • 計算カード1枚のクラス
  • keisan_card_page.dart
    • 計算カード画面構成
  • keisan_card_manager.dart
    • 計算カードロジック部分
    • 計算カード生成、並べ替え
    • タイマー管理
    • 状態管理

画面部分とロジック部分のやり取りは、画面側から、Providerでロジック部分を呼び出します。
ロジック側にはStreamControllerが定義されており、Streamを通じて状態変化を通知し、画面側はそれを受け取り、Widgetをリビルドします。
Streamを受け取り、WidgetをビルドするのはStreamBuilderを使用します。

class KeisanCardPage extends StatefulWidget {
  KeisanCardPage({Key key}) : super(key: key);
  @override
  _KeisanCardPageState createState() => _KeisanCardPageState();
}

class _KeisanCardPageState extends State<KeisanCardPage> {

  final _keisanCardManager = KeisanCardManager();

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('計算カード'),
      ),
      body: Column(
        children: <Widget>[
 (snip...)
        ],
      ),
      floatingActionButton: _buildNextButton(),
    );
  }

  @override
  void dispose() {
    _keisanCardManager.dispose();
    super.dispose();
  }

  Widget _buildNextButton() {
    return StreamBuilder<bool>(
      stream: _keisanCardManager.hasNext,
      builder: (_, snapshot) {
        var _hasNext = snapshot.data ?? false;
        if (_hasNext) {
          return FloatingActionButton(
            onPressed: _keisanCardManager.next,
            child: Icon(Icons.play_arrow),
          );
        } else {
          return FloatingActionButton(
            onPressed: null,
            backgroundColor: Colors.grey,
            child: Icon(Icons.stop),
          );
        }
      },
    );
  }
}
class KeisanCardManager {

(snip...)

  StreamController _hasNextController;

  Stream<bool> get hasNext => _hasNextController.stream;

  KeisanCardManager() {
    _hasNextController = StreamController<bool>();
  }

  void dispose() {
    _hasNextController.close();
  }

(snip...)

}

Flutterにはストップウォッチクラスが標準で備わっているので、それを使っていきます。
上記のようにロジック部分からStreamを通じて値を返していくのですが、画面のアクショントリガーではなく、ロジック側から定期的に値を返したいので、Timerを使っていきます。

class KeisanCardManager {
  final _stopWatch = Stopwatch();
  Timer _timer;

  StreamController _timerController;

  Stream<Stopwatch> get timer => _timerController.stream;

(snip...)

  void _start() {

    _stopWatch.reset();
    _stopWatch.start();
    _timer = Timer.periodic(Duration(milliseconds: 500), (Timer t) {
      _timerController.sink.add(_stopWatch);
    });

  }

(snip...)

}

上記の場合だと、500ミリ秒間隔でStreamControllerへ値を設定しています。
※もうちょい短い間隔でも良いかも。
sink.addするだけで、streamの受け手側のStreamBuilderが実行され、Widgetがビルドされます。

これらを組み合わせて、作ってみました。

起動直後
上の走ってる人ボタンで開始
右下の三角ポタンで次へ
緑のランプボタンで答えを表示
完走!!

さいごに

のべ実装時間は3時間程度なので、随所に荒いところが残っていたり、UIの構成がダサかったり、テストコード書いていなかったり、色々反省点はありますが、実装したコードはこちらにおいています。

反省点としては、いきなりコードを書き出して、場当たり的にStreamControllerを作ってしまったので、もっと整理すればスマートになった気がしています。
子供向けなのにUIパーツがシンプル過ぎるので、もうちょい子供っぽくしても良かったかも…

あとは、全くAndroidアプリにする必要なかったこと…
この程度なら、単純にJavascriptでブラウザアプリとして作れたなーと思ってますwww
業務で作ったアプリはBluetoothでデバイス接続したりしたので、無理やりBluetoothでなにかにデータを送ってみたり…
ハイスコアを記録できたり、九九以外でも対応できたり、カメラでVR、ARっぽくしてみたりなどなど、まだまだ蛇足いろんな機能を盛り込んでみようかと思います。

答えを見ずに次に行けるようにしてしまったので、ただただ81回連射した時間を計測するアプリになってしまったこと…
それはそれで面白いのでそのままにしておきます!
魂の16連射には程遠いですが、私は81連射は8秒台でした!!!

では、よい、Flutter Lifeを!

採用情報

メンバーズエッジで最高のチームで最高のプロダクトを作りませんか?

最高のプロダクトをつくる 最高のチームで働く

在宅でも、地方でも、首都圏でも。多様な働き方で最高のチームをつくり、お客様のプロダクトパートナーを目指します。アジャイル開発を通じ、開発現場の第一線で活躍し続けたいエンジニアを募集しています。