63
Help us understand the problem. What are the problem?

posted at

updated at

三連休に本気出したらFlutterでアプリを作成できるか検証してみた

変更履歴

2021年9月13日 Githubのリンクを変更(中身は変わっていない)

初稿
2020年3月25日 Flutter 2.0未対応

はじめに

「俺はまだ本気を出してない」ってことで、この前の三連休に本気出してみました。
「三連休でも頑張ったら、アプリ作れるもんだぜ」というノリと勢いを伝えるのが本記事の目的です。:laughing:

作ったもの

「早口言葉を音声認識で入力して敵を倒すゲームアプリ」
実際のスマホ画面.gif
こんな感じ。

知れること・知れないこと

知れること

  • 「作ってやるぞ」という気合い。
  • Flutter初心者がぶつかるであろう壁についての知見。
  • 「自分でもできるかも」という自信が湧いてくる(かも)。

知れないこと

  • DB接続を利用した処理の実装(追加機能でやりたい)
  • 認証機能の実装(追加機能でやりたい)

記事作成者のステータス

  • もうすぐエンジニア3年目。
  • 普段はJavaでWeb系の開発やっている。
  • 一度だけFlutterのハンズオンに参加した経験あり。
  • ハンズオンに参加しただけで、どう足掻いてもFlutterもアプリ開発も初心者。

まずFlutterって?

Wikiより

Googleによって開発されたフリーかつオープンソースのモバイルアプリケーションフレームワークである。FlutterはAndroidやiOS向けのアプリケーションの開発に利用されている。

よく聞く特徴としてはAndroidでもiOSでも動く「クロスプラットフォームフレームワーク」の部分ではないかと思います。

また今回注目したいのは公式のここの部分

Fast Development
Paint your app to life in milliseconds with Stateful Hot Reload. Use a rich set of fully-customizable widgets to build native interfaces in minutes.

ざっくり「早く開発できる」と書いてあるものと判断。(Hot Reloadの説明とも取れますが)
「本当に?」ってことで今回作ってみます。

フライングしたこと

本記事については冒頭まで書いた状態にしておきました。
ハンズオンに参加した関係で開発環境の構築も済です。
環境構築については、公式ドキュメントFlutter 開発環境構築手順 (2019年 保存版)などなどご参照ください。

以降の記事の書き方について

3日間、実際にやったことを書いていきます。
(ちなみに私にとっての「1日」とは「夜寝るまで」を意味しています)
また「壁」と称したところがFlutterに関する内容を特に書いた場所なので、
Flutterの開発の参考にしよう!!」という方は「壁」を中心に読んでいただければ幸いです。

1日目

開発環境の確認

まずは開発環境を確認。
flutter doctorの結果は以下の通りです。

$ flutter doctor
[✓] Flutter (Channel stable, v1.12.13+hotfix.8, on Mac OS X 10.15.3 19D76, locale ja-JP)

[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
[✓] Xcode - develop for iOS and macOS (Xcode 11.3.1)
[✓] Android Studio (version 3.6)
~~以下省略~~

開発はAndroid Studioで実施しました。

設計

この三連休のために温めておいた頭の中の設計図(そう大層なもんではないですが)を、外出しします。
iPadに(買ったのに滅多に使わない)Apple Pencilでお絵描き↓
(「途中から字が手書きやん」という苦情は受け付けないです)

ファイル_001.png

アプリ概要
一言でまとめると「早口言葉を音声認識で入力して敵を倒すゲームアプリ」

左の画面

  • ホーム画面
  • レベル別のプレイ画面を選択(出てくる早口言葉が異なる)

右の画面

  • プレイ画面
  • ランダムでレベルに応じた早口言葉を表示
  • 敵の表示
  • 音声入力でテキストを入力(逆にいうと音声入力しかできない)
  • やり直しボタン
  • 攻撃ボタン

共通

  • Drawerにホームに戻るためのメニュー用意

壁1 : 「何が作れるのかなぁ」

Flutterでどういったアプリが実現できるのか。
……正直そこまで突き詰めてはないのですが、私は設計のとき、以下のWidget一覧眺めてました。(参考までに)

Flutter:Widget一覧
(画像がついていて、イメージがしやすい)
Widget catalog
(公式情報)

実装(1)

「Start a new Flutter project」でポチポチしてプロジェクト作ったら、実装の開始です!!

壁2 : 「デザインどんな感じにしようかなぁ」

後からいくらでも変えられるのでしょうが、最初はアプリの色とか全体的なデザインとか気になるものです。
一般的にはアプリ全体のデザイン(色)を指定するためにMaterialAppThemeDataをいじることが多いようです。

(今回は以下のような感じにしました)

main.dart
      ~~
      theme: ThemeData(
        primaryColor: Colors.blueGrey[600],
        accentColor: Colors.blueGrey[600],
      ),
      ~~

参考
Flutterでアプリ全体を統一感のあるデザインにする方法
FlutterのThemeを理解する
materialpalette.com (色の参考)

Flutterでアプリ全体を統一感のあるデザインにする方法の記事をよく読むと、本来はテキストのスタイルとかもThemeDataにまとめておいた方がいいみたいですね。(今回は所々個別で指定しています)

1日目の残りは、ホーム画面とDrawerを作成して終了!!
1日目は手を動かすよりは、サイトやドキュメントを読んでいる時間の方が多かったです。

1日目進捗

1日目.gif

Drawerの中のアイコン(家マークや×マーク)はFlutterで用意されているアイコンクラスを利用してます。多分Flutterでアプリ開発するときはほぼ確実に使うと思います。↓
Icon-classの公式情報

2日目

実装(2)

壁3 : 「画面遷移どうやってやるんだろう」

実装していると、ほぼ確実に画面遷移したくなるはず。
~私が参考にした記事~
FlutterのNavigatorで画面遷移
Flutterの画面遷移

今回は、MaterialAppインスタンス生成時にroutesを指定するやり方を取っています。

main.dart
      ~~
      routes: <String,WidgetBuilder>{
        '/home': (BuildContext context) => MyHomePage(title: title),
        '/play/basic': (BuildContext context) => PlayPage(title: title, level: 'basic'),
        '/play/advanced': (BuildContext context) => PlayPage(title: title, level: 'advanced'),
      },
      ~~

ホームに遷移させるときの書き方↓

  Navigator.of(context).pushReplacementNamed("/home");

壁4 : 「【StatelessWidget】と【StatefulWidget】って?」

多分ここが一番、めちゃくちゃ大事なところです!!!!
他のところはぼんやりでも実装書き進められるのですが、ここは手が止まってでも理解すべきところかと思います。

☆要点がまとまっていて、とても助けられた記事。
Flutterの基礎

StatelessWidget

Stateの概念を持たないWidget。「StatelessWidget」を継承して実装します。
動的に変化しない画面要素を作るときに利用します。
本アプリではホーム画面やDrawerメニューで使いました。

  • (参考)Drawerメニューの実装
drawer_menu.dart
import 'package:flutter/material.dart';

class DrawerMenu extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: ListView(
        padding: EdgeInsets.zero,
        children: <Widget>[
          DrawerHeader(
            decoration: BoxDecoration(
              color: Colors.blueGrey,
            ),
            child: Text(
              'メニュー',
              style: TextStyle(
                color: Colors.white,
                fontSize: 24,
              ),
            ),
          ),
          ListTile(
            // home画面
            leading: Icon(Icons.home),
            title: Text('ホーム'),
            onTap: () {
              Navigator.of(context).pushReplacementNamed("/home");
            },
          ),
          ListTile(
            leading: Icon(Icons.close),
            title: Text('閉じる'),
            onTap: () {
              Navigator.pop(context);
            },
          ),
        ],
      ),
    );
  }
}

Flutterの基礎でも説明がありますが、StatelessWidgetを継承したら、「buildメソッド書きなさい」ってなるので、buildメソッドでWidgetもしくはテキストを返すように実装します。
(上記コードはDrawerのWidgetを返した場合)

returnの後につらつらとコードを書くのは違和感あるかもしれませんが、
やってみた印象としては、最初は下手に切り分けたりせず、そのままつらつら書いた方が良さそうです。

StatefulWidget

Stateの概念を持つWidget。「StatefulWidget」を継承して実装します。
動的に変化させたい画面要素を作るときに利用します。
今回はプレイ画面で使いました。

「完全に理解した」レベルの説明。「実装を進める」ための理解であり、正確さには欠けると思うので参考程度でお願いします。

  • (参考)playページの実装一部
play_page.dart
class PlayPage extends StatefulWidget {
  PlayPage({Key key, this.title, this.level}) : super(key: key);

  final String title;
  final String level;

  // アロー関数を用いて、Stateを呼ぶ
  @override
  State<StatefulWidget> createState() => _PlayPage();
}

StatefulWidget自体はとても単純です。createState()メソッドが必須で、Stateクラスを返すように書けばOK。上記のcreateState()の部分はほぼ呪文レベルです。

play_page.dart
const List<String> baseThemeList = [
  '生麦生米生卵',
  '隣の客はよく柿食う客だ',
  'バスガス爆発',
  '裏庭には二羽ニワトリがいる',
  '赤パジャマ黄パジャマ青パジャマ',
  '赤巻紙青巻紙黄巻紙',
  '老若男女',
  '旅客機の旅客'
];

~~中略~~

class _PlayPage extends State<PlayPage> with SingleTickerProviderStateMixin {

  SpeechRecognition _speech;
  bool _speechRecognitionAvailable = false;
  bool _isListening = false;
  int _hp = 5;

  String _themeText = '';
  String transcription = '';

  String _image = '';

  Language selectedLang = languages.first;

  @override
  initState() {
    super.initState();
    activateSpeechRecognizer();
    setTongueTwister(widget.level);
    if (widget.level == basic) {
      _image = 'images/kaiju.png';
      _hp = 5;
    }
    if (widget.level == advanced) {
      _image = 'images/fantasy_mahoujin_syoukan.png';
      _hp = 7;
    }
  }
~~中略~~
 @override
  Widget build(BuildContext context) {
    return Scaffold(
  ほにゃらら
~~中略~~
  void setTongueTwister(String level) {
    if (level == basic) {
      int index = Random().nextInt(baseThemeList.length);
      setState(() => _themeText = baseThemeList[index]);
    }
    if (level == advanced) {
      int index = Random().nextInt(advancedThemeList.length);
      setState(() => _themeText = advancedThemeList[index]);
    }
    // どちらの条件でもなかった場合
    if(level != basic && level != advanced) {
      int index = Random().nextInt(baseThemeList.length);
      setState(() => _themeText = baseThemeList[index]);
    }
  }
~~~~

with SingleTickerProviderStateMixinは「オフスクリーン時に不要なアニメーションでリソースを消費しないようにするための設定」だそうです(参考)呪文として書いています。

(私のイメージで説明しますが)Stateのフィールドの値は動的に変化させることができる・動的に変化させたい値を定義します。(今回は敵のHPとかお題の早口言葉とか)
※ ただのフィールドなので変化しない値も当然定義できます。

Stateクラスには状態を管理するメソッドが複数存在しますが、今回は利用したinitState(),build(),setState()についてだけ説明します。

  • initState()

Stateの初期化を行います。(敵のHPとかお題の早口言葉は、今回levelに応じて、初期化しています)

  • build()

基本的にStatelessWidgetのbuild()と同じノリで書いてOKです。

  • setState()

setState()の中で値を変化させると、それが動的に画面に反映されます。
例えば、上記のコードのsetTongueTwisterメソッドの中にはsetState(() => _themeText = baseThemeList[index]);といったコードが存在するので、setTongueTwisterを呼び出すたびに_themeText(早口言葉のお題)が変更されることになります。

壁5 : 「画像貼りたい!」

参考記事は以下です。難しくはないと思います。
Flutterで画像を表示する方法【まとめ】

今回は、イラスト屋様様の絵を貼っています。

壁6 : 「パッケージを導入したい!」

本アプリで必要なのは、音声認識のパッケージですね。今回はflutter_speechを入れました。

参考記事
【Flutter】パッケージ導入手順

パッケージ探すサイト自体はhttps://pub.dev/flutterです。
導入方法も各パッケージのページに説明が書いてあります。
余談ですが、flutter_speechExampleコードは大変勉強になりました。flutter_speechの実装は8割型Exampleの内容になっています。

壁7 : 「実機で動かしたい!」

「動かしたい」というより、エミュレータが音声認識非対応だったので、デバッグ時点で自分のスマホにアプリをインストールする必要がありました。
参考記事
[flutter]実機でデバッグする方法

PCとスマホを繋げられる環境必須です。

2日目進捗
2day.gif

実はこの時点では、「攻撃」=(「音声認識の終了」+「お題との文字列比較とHPの処理」)が思うように動かなかったのですが、不貞寝。

3日目

3日目については、本アプリ特有の話が多いです。

実装(3)

「早口言葉レベル2」の実装と、設計でも出し切れていなかった細かい機能追加を実装します。

「攻撃」のちょっと工夫
まず2日目にうまくいかなかった「攻撃」の部分

play_page.dart
  void attack() async {
    if (_isListening) {
      stop();
      await Future.delayed(Duration(milliseconds: 500));
    }
    if (_themeText == transcription) {
      setState(() => _hp = _hp - 1);
      setTongueTwister(widget.level);
    }
    if (_hp <= 0) {
      setState(() => _image = 'images/victory.png');
      setState(() => _themeText = '終了!!');
    }
  }

_isListening …音声認識中か否か
_themeText……入力すべき早口言葉
transcription……音声入力したテキスト

stop()メソッドは音声入力を終了するメソッド(flutter_speechのExampleのコード参照)です。

stop()してからtranscriptionが更新するまでラグがあるようだったので、処理を0.5秒待つように実装しました。(これで即座に攻撃できるようになりました)
asyncつけて「0.5秒待つ」はawait Future.delayed(Duration(milliseconds: 500)で書いてます。

HPの表示+お題の早口言葉変更機能+プレイ画面からホーム画面に戻るボタン
細々とした追加機能.gif

これにて、完成!!
(最初のgif画像をもう一度)私のスマホの画面を撮影した画像です。
実際のスマホ画面.gif
Listening...中に音声入力しています。
「攻撃」するたびにHPが「1」減って、HP「0」になると「VICTORY」(手描き)が出てくるという寸法です!!

Githubのソースページ

ちなみにレベル2もちゃんと実装してあるので、気になる人はcloneして
「実機で動かしたい!」やってみてください。

課題(本アプリ固有の話)

  • 漢字を平仮名に変換してから文字列比較をする。

flutter_speechが自動で漢字変換してくれていて、現状漢字のままで文字列比較を行っています。実際は両方平仮名に変換して比較すべきだと思っています。今回は後回し状態です。

想定外(本アプリ固有の話)

  • flutter_speechが滑舌があやしい場合も、けっこう認識してくれる。

滑舌鍛えたい人とかにニーズあるかなぁーと思ってたんですが、パッケージがちょっと優秀すぎでした。ちょこっとシビアな音声認識パッケージとかあったら嬉しい……

  • 音声聴き終わってでないとテキスト表示されない。

実装前は、しゃべっている途中でもテキストが表示されるイメージをしていました。実際はしゃべり終わった後でした。「攻撃」ボタン押すまで、テキスト表示なしの状態となってしまいました。

終わりに~Flutterによる開発は早かったのか~

いかがだったでしょうか? 「すごい」と思う人もいれば「3日もあってこの程度か!」と思う人もいることでしょう。

実際、一日中PC画面を睨み付けて、、目を血走らせながらコード書いていたか、、と言われると…そういう訳ではないです。
めちゃくちゃお昼寝もしてました。

コードも数えてみると、500行も書いてなかったです。
環境構築はしてないですが、それでも3日あれば今回のレベルのものを作るには余力がある印象でした。
「開発が早くできる」というのは、割と的を得ている気がします。

みなさんも三連休に「ひとアプリ」作ってみてはいかがでしょうか?

この記事で、よりFlutterの開発が楽になること・「アプリ開発してみようかな」という人が増えることを願っております。
以上です!!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
63
Help us understand the problem. What are the problem?