Flutter基礎 – 画面の状態が変わるアプリを作ってみよう

今回作るアプリ

今回作るアプリはこちら🙌

  • FABを押すたびにカウントが1ずつアップする
  • リセットボタンを押すと、カウントがリセットされる

前回は画面に変化のないアプリでしたが、

今回は、ユーザーの操作に合わせて画面の状態が変化するアプリを作っていきます😆

ではいくぅ〜〜✨

アプリ全体に共通する部分を記述

この部分は前回と同じですね。

  • フォルダを分ける(lib/screenを作成)
  • インポートする(material.dart)
  • main関数を記述する
  • アプリ全体を表す、StatelessWidgetを継承したクラスを作成
  • buildメソッドをオーバーライドして中身を記述

コードはこのような感じになります👌

// (2) インポート
import 'package:flutter/material.dart';

// (3) main関数
void main() => runApp(MyApp());

// (4) アプリ全体を表す、ステートレスウィジェット作成
class MyApp extends StatelessWidget {
  // (5) buildメソッドをオーバーライド
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '画面の状態が変わるアプリ',
      home: HomeScreen(),
    );
  }
}

前回同様、「HomeScreen()」の箇所で赤波線が出ると思いますが、

とりあえずは無視でOKです。

画面ウィジェットを作る

では、HomeScreenを実装していきましょう👌

今回のポイントは、

  • ステートフルな画面の作り方
  • 基本的なウィジェットたち
  • 各ウィジェットの配置方法

です🙌

では、やっていきましょう!

StatefulWidget

さてご覧の通り今回は、

ユーザーの操作によって画面の状態が変わる”アプリです💡

そのため前回とは違い、継承するウィジェットは

StatefulWidget”になります😉

まずは前回同様、material.dartをインポートしてから始めましょう!

// (1) "stf" と打つと候補に出てくる "stful" を選択すると、以下2クラスが自動生成される
// (2) クラス名を "HomeScreen" としておく

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

え?2つのクラスが生成されたぞ?

そうなんです。ステートフルな画面を作る際は、

StatefulWidgetを継承したクラス”の「HomeScreen」と

Stateクラスを継承したプライベートクラス”の「_HomeScreenState」が

同時に生成されます🙌

これらをザックリと説明すると、

  • 「HomeScreen」は例えると”変数”のようなもので、「画面」を表します。
  • 「_HomeScreenState」は例えると”インスタンス”のようなもので、「画面の状態」を表します。

というと、イメージが付きやすいかと思います😆

ステートフルな画面は、状態を変えながら存在しますよね。

この”状態を変える”という作業が、Flutterでは少し特殊なんです。

例えばAndroid開発の場合、入力値をTextViewへ反映させるとき、

TextViewの値だけ変えればOKでした(こう聞くと当たり前のようですが笑)。

それに対しFlutterでは、画面の変更を反映させる(状態が変わる)とき、

”その都度”画面全体をビルドし直さないといけません😳(厳密には例外もあるようですが)

”ビルドし直す”ために呼び出されるのが、”buildメソッド”ってことか!

そのとおりです😆✨

以上のことから「ステートフル画面がクラスを分ける理由」は、

  • HomeScreenクラスが”産みの親”として画面を生成
  • _HomeScreenStateクラスが”育ての親”として、画面の状態の変更を反映させていくから

というイメージでOKだと思います👌✨

細かい所については、僕もまだちょっと曖昧なところがあるので割愛😓

また、別の記事として書こうと思います😆

では_HomeScreenStateクラスのbuildメソッドに、ウィジェットを配置していきましょう!

画面の土台作成(Scaffold)

まずはマテリアルデザインの画面の土台となる、

Scaffoldを配置します。

class _HomeScreenState extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      
    );
  }
}

AppBar

次はAppBarです😌

class _HomeScreenState extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(

      appBar: AppBar(
        // title: バーに表示されるテキスト
        title: Text('ホーム画面'),
        // centerTitle: テキストを中央揃えにするかどうか
        centerTitle: true,
      ),

    );
  }
}

FloatingActionButton

次にフローティングアクションボタンです😆

メチャクチャ簡単ですよ〜笑

class _HomeScreenState extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ホーム画面'),
        centerTitle: true,
      ),

      // FloatingActionButtonのコンストラクタを渡す
      floatingActionButton: FloatingActionButton(
        // child: 丸に表示されるアイコンを渡す
        child: Icon(Icons.add),
        // onPressed: ボタンが押されたときのコールバックを実装する
        onPressed: () => _countUp(),
      ),

    );
  }
}

アイコンはIconsクラスのものを使用します。

onPressedに渡すのはVoidCallBack型(なんじゃそれ)、

要は「引数、戻り値のない関数」です🙌

例えばボタンが押されたらコンソールに表示させたい場合は、

onPressed: () => print('ボタンが押された'),

とできます。今回は処理を外だししたいのでメソッドを作ります。

まだメソッドの実装はせずに、赤い波線を無視して次へ行きましょう!

ウィジェットを並べる

次は他の画面部品を並べていきます😌

ここもネイティブとは違う概念ですので、ちょっと難しいかもしれません😢

Columnウィジェット

前回のアプリでは、Textウィジェットは1つだけでした。

しかし今回のアプリの完成形を見ると、

上からテキスト、カウント文字と上下に並んでいますね。

この場合body引数へ渡すのは”列”自体です。

Androidでいうところの、”LinearLayout(orientation: vertical)”みたいな感じですね👌


class _HomeScreenState extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ホーム画面'),
        centerTitle: true,
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () => _countUp(),
      ),

      body: Column(
        children: <Widget>[
          // 引数はリスト形式。ここに複数の画面部品を並べる
        ],
      ),

    );
  }
}

Textウィジェット

ます最初の画面部品、Textウィジェットです✨

これは今まで何回か登場していますが、

今回は引数にフォントサイズを指定してやります👌

まずはテキストのみ。

class _HomeScreenState extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ホーム画面'),
        centerTitle: true,
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () => _countUp(),
      ),
      body: Column(
        children: <Widget>[

          // 1行目
          Text('ボタンが押された回数'),
          // 2行目(変数にしておく)
          Text('$_count'),

        ],
      ),
    );
  }
}

TextStyleウィジェット

スタイルの指定にはTextStyleウィジェットを使います。

Flutterではこれもウィジェットなんです😳

びっくりですね〜

//  省略
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () => _countUp(),
      ),
      body: Column(
        children: <Widget>[

          Text(
            'ボタンが押された回数',
            style: TextStyle(fontSize: 25.0),
          ),

          Text(
            '$_count',
            style: TextStyle(fontSize: 30.0), 
          ),

        ],
      ),
    );
  }
}

RaisedButtonウィジェット

次はリセットボタンを配置します。

今回はRaisedButtonを使用します😉

THE、ボタン!って感じのボタンですね(??)

// 省略 
     floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () => _countUp(),
      ),
      body: Column(
        children: <Widget>[
          Text(
            'ボタンが押された回数',
            style: TextStyle(fontSize: 25.0),
          ),
          Text(
            '$_count',
            style: TextStyle(fontSize: 30.0),
          ),

          RaisedButton(
            // child: ボタンに表示する文字を渡す
            child: Text('リセット'),
            // onPressed: ボタンが押されたときのコールバック
            onPressed: () => _countReset(),
          ),

        ],
      ),
    );
  }
}

onPressedは先程同様VoidCallBackを渡します👌

まだメソッドは作らないので波線は無視👍

この段階で確認してみる

とりあえずここまでできたところで、

放置していたcountUp()メソッドとcountReset()メソッド、countフィールドをとりあえず空で実装し、

ビルドしてみるとこのようになります

全体的に左上に寄っていますね。これではだめだ!!

てことで、次は位置調整をしていきます👌✨

ウィジェットの位置を調整する

やることはこちら!

  • 全体を中央に寄せる
  • テキストの間に余白を入れる

ではやっていきましょう!

Centerウィジェット

これは前回やりましたね✨

//  省略

    floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () => _countUp(),
      ),

      body: Center(

        child: Column(
          children: <Widget>[
            Text(
              'ボタンが押された回数',
              style: TextStyle(fontSize: 25.0),
            ),
            Text(
              '$_count',
              style: TextStyle(fontSize: 30.0),
            ),
            RaisedButton(
              child: Text('リセット'),
              onPressed: () => _countReset(),
            ),
          ],
        ),

      ),

    );
  }

 Columnを選択して「Alt+Enter(Macはoption+return)」で候補を表示し、

「wrap with Center」で一気にくるみます✨

では確認してみましょう。

あれ?中央揃えになっているのは横方向だけだ・・・

なぜでしょうか?

これは、揃えているのが”Column”だからです。

列は縦方向に要素を並べていくので、Centerウィジェットではvertiacal方向への中央揃えはできません

試しにColumnウィジェットをRowウィジェットに変更して、見てみましょう。

この通り、Rowは横方向に要素を並べていくため、横方向への中央揃えはできてないですね😢

じゃ”あ”どう”ずれ”ばい”い”ん”だよ”ぉ”ぉ”お”

おや?カ○ジくんがでてきましたね?

では、その方法を解説します。

Columnの主軸方向に中央揃えする

”主軸”とは”並べる方向”と言い換えても構いません😌

Centerでは主軸方向への位置調整はできませんでした。

それなら、Columnの中で指定してやればいいんです😉👍

ColumnのmainAxisAlignment引数に、以下を渡します。

// 省略
      body: Center(
        child: Column(

      // MainAxisAlignment.centerを渡す
          mainAxisAlignment: MainAxisAlignment.center,

          children: <Widget>[
            Text(
              '押された回数',
              style: TextStyle(fontSize: 25.0),
            ),
            Text(
              '$_count',
              style: TextStyle(fontSize: 30.0),
            ),
            RaisedButton(
              child: Text('リセット'),
              onPressed: () => _countReset(),
            ),
          ],
        ),
      ),
// 省略

これで中央に寄せることができました😆✨

SizedBoxウィジェット

いまの状態でもいいとは思うのですが、

今回はカウンターの数字とリセットボタンの間に余白を入れてみましょう!

余白を入れる方法のひとつ、SizedBoxを使う方法をご紹介します😉

このウィジェット、本来は名前の通り何かのサイズを決める際に使用しますが、

何もウィジェットをラップ(包む)せずに単体で使うことで、余白として使うことができるんです😆✨

使い方は簡単です!

// 省略
            Text(
              '$_count',
              style: TextStyle(fontSize: 30.0),
            ),

            SizedBox(
              // height: 縦方向の幅を指定
              height: 30.0,
            ),

            RaisedButton(
              child: Text('リセット'),
              onPressed: () => _countReset(),
            ),
// 省略

これで求めていたレイアウトになりました😆✨

処理を実装する

最後に、処理を実装していきます😆

処理自体は簡単ですが、気をつけいないと行けないポイントがひとつだけあります😳😳

ではやっていきまそう!

必要なメソッド、フィールドを定義

先程の実装で波線になっていた、

  • _countUp()メソッド…ボタンを押すとカウントが増える
  • _countReset()メソッド…ボタンを押すとカウントがリセットされる
  • _countフィールド…カウントを格納する変数

こいつらをひとまず作っておきましょう!

_countフィールドはint型、初期値は0を代入しておきます👌

class _HomeScreenState extends State<HomeScreen> {
  int _count = 0;

// 省略

カウントアップボタンの処理実装

まずはカウントアップの方をやっていきます👌

  _countUp() {
    _count++;
  }

めちゃくちゃ簡単やないかいwwwww

でも、ちょっと待ってください🤭🤭

最初の解説で、

Flutterでは、画面を変更するたびにビルドし直す必要がある

と言いました。じつはこのままでは、画面の数字は増えていかないんです😣

反映させるには、こうします。

  _countUp() {
    setState(() {
      _count++;
    });
  }

setState()メソッドは、

変数の状態を保持したまま、buildメソッドをもう一度呼び出し、画面を再構成するのに使用します。

Flutterではこの「SetState()メソッド

かなり重要です!!何度も使います!

「あれ?なんで動かないの?」→「SetState()してなかったわw」

ってのが多いですw覚えておきましょう😆✨✨

リセットボタンの処理実装

次はリセットですね、これも同じようにSetState()を使って更新させます🙌

  _countUp() {
    setState(() {
      _count++;
    });
  }

完成!

完成です✨

今回は状態変化のあるアプリを作ってみました😆🙌

簡単な処理ではありますが、

ネイティブ開発と違う概念のところも多くあるので、

まだまだ慣れないところもありますねw

ただFABの実装なんかにしてもめちゃくちゃ楽で、

ボタンの連続タップ防止も考えなくていいなんて今の所神ですね〜😌

次回からもまだまだFlutterの魅力についてお伝えしていきます✨