リバース・エンジニアリング

Flultterとテックブログと時々iOS

Flutter for Web でWebプロジェクトを新規作成する

前提

Flutter コマンドを叩ける状態にしておく

コマンド

Flutterパッケージのあるディレクトリで次のコマンドを叩く

$ flutter channel master

flutter をアップグレードします。

$ flutter upgrade

Web サポートを有効にします。

$ flutter config --enable-web

chrome などのブラウザが使えるかを念の為確認します。

$ flutter devices

モバイルプロジェクトと同様にプロジェクトファイルを作成します。

$ flutter create the_basics

Chrome で開く場合

Chrome でローカルで開発する場合に使う感じです。

$ flutter run -d chrome

## 成功時

Downloading Web SDK...                                              8.9s
Launching lib/main.dart on Chrome in debug mode...
Syncing files to device Chrome...                                       
17,359ms (!)                                       
Debug service listening on ws://127.0.0.1:62972/QwJpY-485sk=

Warning: Flutter's support for web development is not stable yet and hasn't
been thoroughly tested in production environments.
For more information see https://flutter.dev/web

🔥  To hot restart changes while running, press "r" or "R".
For a more detailed help message, press "h". To quit, press "q".

Chrome が開いて開発できます。

任意のサーバーで立ち上げる場合

laravel artisan serve みたいな感じ localhost で立ち上げたい場合に使う。

$ flutter run -d web-server

## 成功時
Launching lib/main.dart on Web Server in debug mode...
Syncing files to device Web Server...                                   
11,031ms (!)                                       
lib/main.dart is being served at http://localhost:63225

最後に書いている http://localhost:63225 を開くと表示されます。

Flutter コマンドを叩けるようにする

Flutter パッケージのダウンロードページ

Flutter パッケージのダウンロードページ

flutter.dev

FlutterパッケージをDLしてパスを通す。

Flutter パッケージのあるディレクトリーで次のコマンドを叩く

export PATH="$PATH:`pwd`/flutter/bin"

これで flutter のパスを叩けるようになる。

64. FlutterのProviderパターンを3分で理解する

FlutterのProviderパターンを3分で理解する

Flutter 初心者にとって Provider の扱いはとても難しく思うはず。
僕も例外なく Flutter 学習初めの頃は Provider を見てもあまり魅力を感じなかった。

今回はその Provider について理解できるようにすることが主目的である。

【目次】

事前知識

Provider の書き方を理解する上で、前提知識といいますか経験値が必要な気がします。

  • Container, Column, Row, ListView を使ってレイアウトを組み立てられる
  • StatefulWidget の setState の使い方を理解している
  • Widget の分割のメリットを理解している

多分、これらまで理解していたら Provider で今よりもいい感じの設計ができる(はず)。

Providerとは

Provider の概念・理念の理解は itome さんのブログが一番分かりやすいと思う。

itome.team

この記事を読めば Provider については理解できるはずだった。はずだったというのは僕が例外だったから。
多分、本当に初心者の方にとっては Provider の理解は難しいと思う。

Provider の特徴

完全に Providerについて理解していないので、勿論間違った解釈をしているかもしれない。
そのレベルですが、 Provider を使うことで得られるメリットは

  • StatefulWidget を使う必要がなくなる ( setState の更新は不要)
  • iOS でいうところの RxSwift の使い方に似ている
  • 末端の widget まで値を送る必要がなくなる

これらだと思っている。
それではこれを前提に Provider のついて学習していきたい。

サンプルコード

Provider のサンプルコードについては上記のページと同じコードを載せることになります。
最初は Flutter の初期コードをそのままカスタマイズするためです。

それでは Flutter の初期コードについてコメント文を削除した状態を載せます。

main.dart

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

こちらが初期コードになります。

特徴は

  • setState で値を更新している (StatefulWidget のため)
  • Widget の生成 と Model が同じクラス内で行われている

です。

これをProviderパターンに書き換えます。

Provider の手順について

それでは Provider を使う手順について紹介します。

  1. Provider をインストールする
  2. ChangeNotifier を継承した Model クラスを定義する
  3. 発火したいイベントを持っている widget にConsumer で包む
  4. Provider で値を更新させたい widget に ChangeNotifierProvider で包む
  5. 値を更新したい箇所 に Provider で包む

1. Provider をインストールする

Provider パッケージのページはこちらになります。

pub.dev

yaml ファイルに宣言してインストールします。

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  provider: ^4.1.2

使うファイルで import します。

main.dart

import 'package:provider/provider.dart';

これで準備が整いました。

2. ChangeNotifier を継承した Model クラスを定義する

次に ChangeNotifier を継承した Model クラスを定義します。

main.dart

import 'package:provider/provider.dart';

class CountModel extends ChangeNotifier {
  /// 初期値
  int count = 0;
  
  /// count の更新メソッド
  void increment() {
    count ++;
    notifyListeners();
  }
}

こんな感じでOKです。

3. 発火したいイベントを持っている widget に Consumer で包む

次に値を更新するイベントを持っているwidget を ChangeNotifierProvider で包んであげます。
ですが、最初のコードは StatefulWidget で書かれているので StatelessWidget に書き直して Widget build(BuildContext context) の中身を移動させます。

main.dart

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

この時点では counter や incrementCounter が存在しないのでエラーになります。
まずは Scaffold の部分を次のように変更します。

      child: Consumer<CountModel>(
          builder: (context, model, child) => Scaffold(
              appBar: AppBar(
                title: Text('Flutter Demo Home Page'),
              ),
             /*
             * 以下省略
              */
          )
      )

Consumer() で Scaffold を包みました。

4. Provider で値を通知したい widget に ChangeNotifierProvider で包む

次に値を更新させたい widget を ChangeNotifierProvider で包みます。
今回は MyHomePage クラスの Scaffold に対して値を通知したいです。

main.dart

    return ChangeNotifierProvider<CountModel>(
      create: (context) => CountModel(),
      child: Consumer<CountModel>(
          builder: (context, model, child) => Scaffold(
              appBar: AppBar(
                title: Text('Flutter Demo Home Page'),
              ),
             /*
             * 以下省略
              */
          )
      ),
    );

5. 値を更新したい箇所 に Provider で包む

最後に更新された値を受け取ってそれを widget に反映させます。
値を提供するということで Provider と名付けられているかもしれません。

main.dart

Text(
    /// Provider を使う
    '${Provider.of<CountModel>(context).count}',
    style: Theme.of(context).textTheme.headline4,
    )

これで Consumer で包んだ FloatingActionButton をタップした時にProviderで受け取った Text の数値が変更されます。
またこれはwidget で切り離して使うこともできます。

class CountText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      /// context からModelの値が使える
      '${Provider.of<CountModel>(context).count}',
      style: Theme.of(context).textTheme.headline4,
    );
  }
}

なんと、model の count が Provider.of からシングルトンのように取得できるようになりました。
わざわざ count のプロパティを受け取る必要がないことがわかります。

これでビルドすると Flutter プロジェクトが作成された初期画面と同じ挙動になります。

全体のソースコード

最後に全体のソースコードを載せておきます。

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CountModel extends ChangeNotifier {
  /// 初期値
  int count = 0;

  /// count の更新メソッド
  void increment() {
    count++;
    notifyListeners();
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(),
    );
  }
}


class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<CountModel>(
      create: (context) => CountModel(),
      child: Consumer<CountModel>(
          builder: (context, model, child) => Scaffold(
              appBar: AppBar(
                title: Text('Flutter Demo Home Page'),
              ),
              body: Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Text(
                      'You have pushed the button this many times:',
                    ),
                    CountText(),
                  ],
                ),
              ),
              floatingActionButton: FloatingActionButton(
                onPressed: model.increment,
                tooltip: 'Increment',
                child: Icon(Icons.add),
              )
          )
      ),
    );
  }
}

class CountText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      /// context からModelの値が使える
      '${Provider.of<CountModel>(context).count}',
      style: Theme.of(context).textTheme.headline4,
    );
  }
}

ということで長かったですが、最後にこのパターンを暗記します。
一回テンプレート的に書き方を覚えたら後は応用になるからです。

ちなみに、この Provider パターンは、

RxSwift に例えるとするなら、
Consumer は BehavorRelay の accept で値を渡すところ
Provider は Observable で値を通知するところ

iOS でいうならば、
Consumer は NotificationCenter をpostするところ
Provider は NotificationCenter でadd して値を通知するところ

みたいなイメージです。

そんな印象でした。今日はここまで。

それでは、バイバイ!

7年前は初心者だった僕へ伝えたい6つのこと

【目次】

7年前は初心者だった僕へ伝えたい6つのこと

この記事は、7年前はまだプログラミング初心者だった自分へ向けた手紙です。
7年前は社会人としても未熟でしたが、そもそもプログラミングスキルも不安で心配事が絶えない時期だった。

今はそのだいたいの心配事は解消されている。
けど、やっぱり2014年から2015年頃は不安だらけだった。

そんな昔の自分に7年経った今ならどうアドバイスができるんだろう、というのが今回のテーマです。
ちょうどいいので6つ考えてみた。

プログラミングの上達法は継続すること

まず2013年頃は仕事が終わってからずっと自宅でプログラミングの本を読み漁っていた。
当時はObjective-Cアプリ開発していたからObjcの話しになるけど、本当に当時はObjective-Cは化け物級に難しいと感じていた。
勉強の傍ら何度「プログラミング 上達法 コツ」で検索しただろうか。
どうやったら「最短で」プログラミングが上達するかについて、 検索しまくっていたぐらいにプログラミングスキルに自信を持てない時期だった。
そもそも、アプリが作れないと生活が維持できなかったからそりゃあ頑張るわけだけど・・・。

Objective-Cに慣れてきた2014年に、いきなりSwiftという新しいプログラミング言語が登場した時は本当に苦しかったのを今でも覚えている。
プログラミングがめちゃくちゃできる同僚が「めっちゃ簡単なプログラミング言語が出たね!」と言ってはしゃいでいたのは悔しかった。
僕にとっては新しいプログラミング言語が出てきて「また学習時間が増えるのか」くらいにしか思わなかったよ。

でもね、今になって思えばプログラミングを最短で身につける方法っていうのは結局「継続する」方法以外にない気がする。
先人たちやプログラミングがうまい同僚がなぜ上手いのかは「今まで継続してきたから」に他ならない。
だから君がすべきことはそんなプログラミングがうまい同僚や友人と出会ったら、損得抜きで仲良くなって色々教わればいいと思う。
そんなプログラミングが上手い人達の習慣をコピーすること(真似ること)。
習慣の継続は時を経てば最高の資産になると今は思う。

フロントエンドの技術の変遷はやっぱり激しい

当時(~2014年)はモバイルアプリ界隈はiOS一強だった。
モバイルアプリの業界へ参入する上で選択肢は

のどちらかを選ぶかしか選択肢はなかった記憶がある。

Objective-Cを選択すると自動的にMac / iPhone アプリしか作れないというリスクとご臨終する。おまけに難易度高い。
Java を選択するとJava の仕事で食っていけるけどEclipse での開発になる。でもJava の難易度低い。

というプログラミング初心者にとっては結構極端な選択肢しかなかった。
当時の自分はiOSアプリが作りたくてObjective-Cを選択したけど、この選択肢がベストチョイスだったのか、 今でも迷う(笑)。

しかもちょうどObjective-Cの開発に慣れてきた頃に、Swift が登場するとかいう追い打ちをかけてきたのが懐かしいです。
当時はSwiftイケてる勢とObjective-Cずっと残るよ勢が2:8くらいの割合でいた。

それが一年を経過して3:7 へ、また一年経過して4:6と移り変わっていきましたね。
ちょうど肌感では2016年くらいから Swift : Objective-C = 5:5 の比率に変わって、
Objective-C からSwift へのコンバートタスクの求人案件が増えていった。
Objective-C歴3年目くらいで、とうとう僕もSwift を書き始めた。ちょうどSwift 2 くらいの時。
そんなタイミングだったので、このコンバートタスクも一部携わるようになったのは経験的に良かったと思う。

その後と並列してリアクティブプログラミングがCocoa で流行して、上位層のRxによるMVVM開発案件が爆発的に増加したのを覚えている。2016 ~ 2017年頃では開発アーキテクチャーでMVC vs MVVM 論争がめちゃくちゃ流行ってRxSwift がスター的な存在まで輝いてた。

RxSwift 勢は今息してますか?

もう2018年からはObjective-Cで開発しているところは少なくなったのが僕の所感ですが、
求人を見るとSwift : Objective-C = 7: 3 くらいの感覚だったと記憶してる。

2019年には SwiftUI とCombine が登場してRxSwift の勢力が一気に減った感を勝手に感じました。
逆に2016 ~ 2017 年にRxSwift で開発を進めて市民権を得られたアプリは、今でも続いていて保守案件として顕在しています。
この辺りの案件で割と中上級者のプレイヤーが悲鳴を上げてるイメージです。
ただまあ初心者でもRxが書けるみたいなんですが、
初心者が書いたRxSwift + MVVM で作られたアプリの保守はさすがにしたくないよねって話し。

このようにモバイルアプリは、それでもiOSアプリ開発界隈だけの流れでこれなので、これにプラスAndroidアプリ開発の変遷を足すととんでもない歴史になる気がします。
それぐらいモバイルアプリ開発の技術の移り変わりが激しい業界です。

結果として、この業界は

Objective-C

Swift

RxSwift

SwiftUI

?

?

?

という流れを得ています。

モバイルアプリ開発をキャリアとして選ぶということは毎年こういった技術の移り変わりが起きる業界なので、もしもそれの学習コストに耐えられなくなったらサーバーサイドとかに移行したほうがいいかも。

なんせ、こういった技術の移り変わりに対応できたとしても、次の項目で結構モチベーションが下がってしまうからです。

どこまでいってもこの世界は10代・20代が最高に強い

主語がデカイので、もしかしたら界隈にマサカリが飛んでくるかもしれないけど、モバイルアプリは新技術にすぐに慣れる若い年代の方が有利です。

設計面では勿論ベテランの方が仕様変更に強い設計を作れて有利ですが、スピード面では若い人の方が強いですね。
僕も20代の頃はそのスピード面でベテラン勢に優位性を図っていましたが(当時はすみませんでした。)、30代にもなると設計とか色々な事を考慮してしまってスピードを出せなくなった感がある。
自分の中では丁寧に開発してると表現してます。

20代は早く開発できるので仕事をした分だけ評価されるのも面白かった。
当時はあまり例外処理とかライブラリの寿命とかを気にする必要がなかったから(その視点がなかっただけである)。
だけどそれで図に乗らずに開発に慣れてきたら、今度はアウトプットを意識した方がいいかも。

7年前の自分はどうやればプログラミングが上達するか分かんないだろうけど、分からないなりにアプリ開発を続けていけば自ずとプログラミングのコツが分かってくると思う。
0 から 50% にするのは割と続けるだけで到達できる。
80% から 90% とかはさらに死にものぐるいで学習する必要がでてくるかも。
まあ、諦めたらそこで終わりだから継続の習慣を身につければいいと思う。

最新技術が出てきたら迷わず時間を投資すること

これは昔、大失敗した自分への戒めでもある。
2014年にSwift がリリースされたとき、Objective-Cを頑張っていた自分はSwiftの登場でまた学習時間が増えることが嫌で、Swiftへの投資を控えていた。

多分、これは今から当時の自分に戻っても絶対同じ事しそうだけど。

最新技術が出たら迷わずそれに時間を投資するのがベストプラクティスだと思う。
これは言ってもなかなか出来ない選択なので一つコツを教えるとしたら、毎日1時間だけでも触ってみること。
触るのが難しいなら調査するだけでもいいと思う。

1年これを継続してれば、それでその新しい技術の経験値を365時間得たということになる。
簡単に言えば、

毎日
既存技術: 3時間
新技術: 1時間

1年
既存技術: 1095時間
新技術: 365時間

の経験年数を得たことになる。

そうはいっても、
今の自分もこれは全く出来ていない感が半端ないので、できなければ諦めるのも人生。

20代は年収を気にするよりも情報発信したほうが得だから

あんまりこういうことはブログでは書きたくないんだけど、7年前の自分なら必要性を理解していなかったので敢えてこれを書く。
githubにコミットしろという話しではない。
どちらかといえば、ブログなりQiitaなりに自分のしてきたことをアウトプットして情報資産を増やしたほうがいいということ。
年収ももちろん気にする必要があるんだけど、年収は上がっていくとその分だけ税金も累進課税で増えていく。

一応、自社サービスの一環で個人アプリ開発にもチャレンジしてリリースもしたんだけど、 ソースコードのメンテナンスだったり広告費にお金がかかったりで結局トータルでペイできなくて中止に終わったんだ。
個人でプッシュ通知を打つには当時ならサブスクのサードパーティを使って、毎月4~5000円のかかっていたように思う。
だけどやっぱりコストがしんどい。

そこからさらにAppStoreのランキングに乗せるために広告宣伝費として1ダウンロード400 ~ 500 円というコストがかかった。
広告を打つのにも100DLからとかいうユニット単位だったから一発広告を打つだけで20~30万円かかる世界。

今でこそランキングブーストなんて言葉は流行らないけど3年ぐらいまで流行っていた気がする。
大手が予算の限りに広告費でランキングブーストするもんだから、それで個人開発者が絶命されていった。

個人アプリ開発をリリースして資産を増やしたい気持ちがあるのはわかるけど、多分当時に戻るならブログを立ち上げて情報発信でアウトプットしまくっていたと思う。
そのブログに色々な情報を残してブランディングしたほうが情報資産を増やす戦略してはありだったと思う。

2020年は東京オリンピック開催年だったけど感染症で延期されちゃった

今年2020年はこれだけで1冊の本が書けるってぐらいのビッグニュースなんだけど、あれだけ話題になっていた2020年の東京五輪の開催が2021年に延期されました。

そして、再びリーマンショック級の不況が今全世界を襲っている。
東京だけでなく日本、だけでなく全世界がコロナウイルスという感染症で焼かれてるから、これはもう個人で取れる対応策はない。

リーマンショックの時は不運に不運が重なって悲惨な目に逢いまくったけど、7年後の僕はとりあえずは無事だから安心してほしい。

この時のためにこの10年間めちゃくちゃ準備してきたから大丈夫って思ってたけど、やっぱり心配だった。
とりあえず2020年の年越しは乗り越えられそうだから、今は安心してiOSアプリ開発の勉強を頑張ってほしい。

63. Flutterでボタンにアニメーションを追加する

ボタンにアニメーションを追加する

本当はボタンに1回転のアニメーションを入れたかったのですが、Animation の回転が思わぬ位置に止まったので諦めました。
なので前回紹介したフリップのアニメーションを使ってボタンにアニメーションをかけます。

前回の続きはこちらになります。

tamappe.hatenadiary.com

使うアニメーションのパッケージはこちらです。

pub.dev

NumberButtonのウィジェットクラスを改造します。

改造前

class NumberButton extends StatelessWidget {
  final int number;
  final Function onPressed;

  NumberButton(this.number, this.onPressed);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 60,
      height: 60,
      decoration: BoxDecoration(
        border: Border.all(color: Colors.white, width: 2.0),
        borderRadius: BorderRadius.circular(10),
        color: Constants.orangeColor,
      ),
      child: FlatButton(
          child: Text(
            '$number',
            style: TextStyle(
              fontSize: 20.0,
              fontWeight: FontWeight.bold,
            ),
          ),
          onPressed: onPressed),
    );
  }
}

改造後

class NumberButton extends StatelessWidget {
  /// ボタンの番号
  final int _number;
  /// タップした時の処理
  final Function _onPressed;
  final bool _isOnTouch;

  NumberButton(this._number, this._onPressed, this._isOnTouch);

  @override
  Widget build(BuildContext context) {
    return FlipCard(
      direction: FlipDirection.HORIZONTAL,
      speed: 500,
      // タップイベント
      onFlip: _onPressed,
      flipOnTouch: _isOnTouch,
      front: _frontNumberButton(),
      back: _backNumberButton(),
    );
  }

  Widget _frontNumberButton() {
    return Container(
      width: 60,
      height: 60,
      decoration: BoxDecoration(
        border: Border.all(color: Colors.white, width: 2.0),
        borderRadius: BorderRadius.circular(10),
        color: Constants.orangeColor,
      ),
      child: Center(
        child: Text(
          '$_number',
          style: TextStyle(
            fontSize: 30.0,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    );
  }

  Widget _backNumberButton() {
    return Stack(
      children: <Widget>[
        Container(
          width: 60,
          height: 60,
          decoration: BoxDecoration(
            border: Border.all(color: Colors.white, width: 2.0),
            borderRadius: BorderRadius.circular(10),
            color: Constants.orangeColor,
          ),
          child: Center(
            child: Text(
              '$_number',
              style: TextStyle(
                fontSize: 30.0,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ),
        Opacity(
            opacity: .8,
            child: Container(
                width: 60,
                height: 60,
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(10),
                  color: Colors.black,
                )))
      ],
    );
  }
}

注目するところはこちらになります。

FlipCard(
      direction: FlipDirection.HORIZONTAL,
      speed: 500,
      // タップイベント
      onFlip: _onPressed,
      flipOnTouch: _isOnTouch,
      front: _frontNumberButton(),
      back: _backNumberButton(),
    );

FlipCard はフリップ速度をコントロールできるので500 に指定します。
フリップの報告は縦は違和感しかないので横(HORIZONTAL) に指定します。

そして、前面(front) と背面(back) にそれぞれ乗せたいウィジェットを乗せるといった感じになります。

_frontNumberButton() のウィジェットはこちらになります。

  Widget _frontNumberButton() {
    return Container(
      width: 60,
      height: 60,
      decoration: BoxDecoration(
        border: Border.all(color: Colors.white, width: 2.0),
        borderRadius: BorderRadius.circular(10),
        color: Constants.orangeColor,
      ),
      child: Center(
        child: Text(
          '$_number',
          style: TextStyle(
            fontSize: 30.0,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    );
  }

_backNumberButton() のデザインはタップ後にボタンを押せなくして、 さらに少し非アクティブに見せたいので黒色のカバーを乗せます。

  Widget _backNumberButton() {
    return Stack(
      children: <Widget>[
        Container(
          width: 60,
          height: 60,
          decoration: BoxDecoration(
            border: Border.all(color: Colors.white, width: 2.0),
            borderRadius: BorderRadius.circular(10),
            color: Constants.orangeColor,
          ),
          child: Center(
            child: Text(
              '$_number',
              style: TextStyle(
                fontSize: 30.0,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ),
        Opacity(
            opacity: .8,
            child: Container(
                width: 60,
                height: 60,
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(10),
                  color: Colors.black,
                )))
      ],
    );
  }

こちらをビルドして確認すると下のようなアニメーションが出来ているのが分かります。

f:id:qed805:20200530182924g:plain:w300
フリップアニメーションを追加

だんだんゲームぽく見えてきましたね。
これでゲーム画面での最後の機能は右上のタイマーのみになります。

ただ Dartの Time 系の API 操作が結構難しそうでこれを作れるかちょっと不安です。

62. Flutterライブラリflip_cardでフリップアニメーションを作ってみる

フリップアニメーションを簡単に作れる flip_card を使ってみる

今作っているナンバーズアプリでカードにアニメーションを入れたいのでアニメーションの入れ方を調査しました。 調査しているうちにシンプルなパッケージがありましたので簡単に紹介してみます。

フリップカードアニメーションを作れるflip_cardパッケージ

flip_card はフリップカードアニメーションを簡単に再現できる component です。

pub.dev

Horizontal Vertical
f:id:qed805:20200530112345g:plain:w300 f:id:qed805:20200530112417g:plain:w300

簡単に使い方を説明すると

FlipCard(
  direction: FlipDirection.HORIZONTAL, // default
  front: Container(
        child: Text('Front'),
    ),
    back: Container(
        child: Text('Back'),
    ),
);

このように デザインは Container で作った widget を front(全面) と back (背面) に乗せるだけです。 Card と FlatButton が合体した widget みたいな印象を受けました。 ボタンタップ時の処理は onFlip のプロパティがありますのでタップイベントもハンドリングできます。

サンプルソースコード

flip_card を入れる

yaml ファイルを編集して flip_card が使える状態にします。

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^0.1.2
  flip_card: ^0.4.4

yaml ファイルでインストールに失敗するときはインデントを見直します。結構あるあるです。

パッケージをインポートする

ファイルにインポートします。

import 'package:flip_card/flip_card.dart';

今回は簡単に StatelessWidget に FlipCard を乗せました。

class FlipCardButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: FlipCard(
          direction: FlipDirection.HORIZONTAL,
          speed: 500,
          // タップイベント
          front: Container(
            width: 100,
            height: 100,
            color: Colors.redAccent,
            child: Center(child: Text('前面')),
          ),
          back: Container(
            width: 100,
            height: 100,
            color: Colors.blueAccent,
            child: Center(child: Text('背面')),
          ),
        ),
      ),
    );
  }
}

main.dart にはただ FlipCardButton ウィジェットを乗せました。

main.dart

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Numbers',
      theme: new ThemeData(
        fontFamily: 'Arial',
      ),
      home: FlipCardButton(),
    );
  }
}

これをビルドすると次のような画面が表示されます。

f:id:qed805:20200530115145g:plain:w300
FlipButtonCard

(はてなブログではめちゃくちゃ容量小さい gif でないとアップロードできないので動作が鈍いですが、実際は滑らかな動きです。)

これを使ってナンバーズアプリのボタンにアニメーションを入れていきます。

今日はここまでです。

それではバイバイ!

61. FlutterのsetStateでゲームロジックを組み立てる

ゲームロジックを組み立てる

今日はナンバーズアプリのゲームロジックを組み立てます。

【目次】

前回の記事はこちらになります。興味があったら読んでね。

tamappe.hatenadiary.com

このゲームで作らないといけない機能は大まかに3つほどです。

  1. 上のラベルに表示されている数のボタンをタップすると数字が繰り上がるという正解のロジック
  2. タイマーカウント
  3. 盤面のボタンの配置をランダムにする

正解のロジック

今回は一番上の「正解のロジック」の作っていきます。
そのために必要なのか「次の数字」が表示されているUIのデザインを整えることです。

f:id:qed805:20200507232203p:plain:w300
ゲーム画面のデザイン

ここの左上の部分を作っていこうと思います。

といっても特段難しいことではなく、
レイアウトは前回で Columnを使って縦に widget を並べました。


正解の数字  タイマー

    盤面


という並びになったらOKです。
細かい余白は Paddingなり margin を使って調整します。

縦の並びは Column で行いましたので
正解の数字とタイマーは Row で並べることにします。

Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                crossAxisAlignment: CrossAxisAlignment.end,
                children: <Widget>[
                  正解の数字,
                  タイマー
                ],
              ),

みたいな感じです。

実際はこのように並べました。

Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                crossAxisAlignment: CrossAxisAlignment.end,
                children: <Widget>[
                  Padding(
                    padding: EdgeInsets.only(left: 20.0),
                    child: ClipRRect(
                      borderRadius: BorderRadius.circular(5),
                      child: Container(
                          width: 140.0,
                          height: 50.0,
                          color: Colors.white,
                          child: Center(
                            child: Text(
                              '$currentNumber',
                              textAlign: TextAlign.center,
                              style: TextStyle(
                                  fontWeight: FontWeight.bold, color: Colors.black, fontSize: 30),
                            ),
                          )),
                    ),
                  ),
                  Container(
                    padding: EdgeInsets.only(right: 20),
                    child: Center(
                        child: Text(
                      'Timer: 3.57',
                      style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                    )),
                  )
                ],
              ),

正解の数字を currentNumber というプロパティで定義します。

game_play_page.dart

class GamePlayPage extends StatefulWidget {
  @override
  _GamePlayPageState createState() => _GamePlayPageState();
}

class _GamePlayPageState extends State<GamePlayPage> {
  int currentNumber = 1;

  void _onPressedNumberButton() {}

  @override
  Widget build(BuildContext context) {
    return Scaffold(
    );
  }
}

と定義しました。

ゲームロジック

あとは currentNumber を使って正解ロジックを組み立ててればいいだけです。
正解したらcurrentNumber の数字を更新すればOKです。
正解しているかどうかはボタンをタップした時にそのボタンが持っている数字と一致しているかどうかを確認します。

if (index == currentNumber)

こんな感じです。

前回、ボタンをタップしたときの関数プロパティ onPressed を定義しました。
これを使って GridView.count の中でロジックを書きます。

GridView.count(
                    mainAxisSpacing: 8,
                    crossAxisSpacing: 8,
                    physics: const NeverScrollableScrollPhysics(),
                    crossAxisCount: 5,
                    children: List.generate(25, (index) {
                      return NumberButton(index + 1, () {
                        if (index + 1 == currentNumber) {
                          _updateCurrentNumber();
                        }
                      });
                    })

index + 1 == currentNumber の時に updateCurrentNumber メソッドを叩きます。
updateCurrentNumber メソッドでは

  void _updateCurrentNumber() {
    if (currentNumber >= 25) {
      Navigator.push(
        context,
        new MaterialPageRoute<Null>(
          settings: RouteSettings(name: Constants.clearRoute),
          builder: (BuildContext context) => ClearPage(),
        ),
      );
    }
    setState(() {
      currentNumber += 1;
    });
  }

というふうに処理しました。
今はまだ Provider とか使っていませんので StatefulWidget の setState を使ってUIを更新しています。
本当は Provider で StatelessWidget にして comsumer で currentNumber を更新するのが今どきみたいです。
Provider については余力があったときに勉強してみようと思います。

これで盤面のボタンをタップして正解の数字をヒットしたら currentNumber が更新されて次の数字に更新されます。

この今回は部分的にコードの処理を説明しただけなので分かりにくいと思います。
そこで最後にこの部分の全体のソースコードを載せることにします。

game_play_page.dart

class GamePlayPage extends StatefulWidget {
  @override
  _GamePlayPageState createState() => _GamePlayPageState();
}

class _GamePlayPageState extends State<GamePlayPage> {
  int currentNumber = 1;

  void _onPressedNumberButton() {}

  void _updateCurrentNumber() {
    if (currentNumber >= 25) {
      Navigator.push(
        context,
        new MaterialPageRoute<Null>(
          settings: RouteSettings(name: Constants.clearRoute),
          builder: (BuildContext context) => ClearPage(),
        ),
      );
    }
    setState(() {
      currentNumber += 1;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                crossAxisAlignment: CrossAxisAlignment.end,
                children: <Widget>[
                  Padding(
                    padding: EdgeInsets.only(left: 20.0),
                    child: ClipRRect(
                      borderRadius: BorderRadius.circular(5),
                      child: Container(
                          width: 140.0,
                          height: 50.0,
                          color: Colors.white,
                          child: Center(
                            child: Text(
                              '$currentNumber',
                              textAlign: TextAlign.center,
                              style: TextStyle(
                                  fontWeight: FontWeight.bold, color: Colors.black, fontSize: 30),
                            ),
                          )),
                    ),
                  ),
                  Container(
                    padding: EdgeInsets.only(right: 20),
                    child: Center(
                        child: Text(
                      'Timer: 3.57',
                      style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                    )),
                  )
                ],
              ),
              SizedBox(
                height: 48.0,
              ),
              SizedBox(
                height: 400.0,
                child: GridView.count(
                    mainAxisSpacing: 8,
                    crossAxisSpacing: 8,
                    physics: const NeverScrollableScrollPhysics(),
                    crossAxisCount: 5,
                    children: List.generate(25, (index) {
                      return NumberButton(index + 1, () {
                        if (index + 1 == currentNumber) {
                          _updateCurrentNumber();
                        }
                      });
                    })
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

class NumberButton extends StatelessWidget {
  final int number;
  final Function onPressed;

  NumberButton(this.number, this.onPressed);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 60,
      height: 60,
      decoration: BoxDecoration(
        border: Border.all(color: Colors.white, width: 2.0),
        borderRadius: BorderRadius.circular(10),
        color: Constants.orangeColor,
      ),
      child: FlatButton(
          child: Text(
            '$number',
            style: TextStyle(
              fontSize: 20.0,
              fontWeight: FontWeight.bold,
            ),
          ),
          onPressed: onPressed),
    );
  }
}

このようになりました。

これで正解したときのロジックが完成になります。

今日はここまでにします。

それではバイバイ!