Chapter 06

Day5: 実際にアプリを作ってみる

fastriver
fastriver
2021.02.13に更新

なにか作ってみよう

最後なのでWidgetを紹介しつつアプリを一連の流れで作っていく

占いをするページを作ってみる

今回新たに使うWidgetたち

  • Row
  • CheckBox
  • Radio
  • Slider
  • SingleChildScrollView

新しくプロジェクトを作る

[File] -> [New Flutter Project]

使う雛形(これでmain.dartの中身を入れ替える)

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Web Training',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        //fontFamily: "Noto Sans JP",
      ),
      home: DefaultTextStyle.merge(
        style: TextStyle(
          height: 1.5
        ),
          child: MyHomePage()
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  
  Widget build(BuildContext context) {
    return Scaffold(
        body: Text("今日の天候を確認する")
    );
  }
}

中華フォント問題を解決する

追記: シークレットブラウザ(勝手に開くやつ)で見ると中華フォントになるが、同じURLを普通のブラウザで開くと問題ない場合もある。

Flutter Webは日本語フォントでなく中華フォントが使われることがまれによくある。

上の雛形を実行して文字がおかしくなければこの項は飛ばしてよし

一番簡単な解決策としてGoogleの公開しているWebフォントを適用する。

Google Fonts+日本語

日本語フォントを適用すれば問題ないのでNoto Sans JPを使えるようにする

web/index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
  <meta name="description" content="A new Flutter application.">

  <!--この一行を追加する-->
  <link href="https://fonts.googleapis.com/css?family=Noto+Sans+JP" rel="stylesheet">

  <!-- iOS meta tags & icons -->
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="aicflutter5">
  <link rel="apple-touch-icon" href="icons/Icon-192.png">

  <!-- Favicon -->
  <link rel="shortcut icon" type="image/png" href="favicon.png"/>
<!--略...-->

次にMaterialAppのthemeにこのフォントを適用する

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Web Training',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        fontFamily: "Noto Sans JP", //この一行のコメントアウトを外す
      ),
      home: DefaultTextStyle.merge(
        style: TextStyle(
          height: 1.5
        ),
          child: MyHomePage()
      ),
    );
  }
}

やったね

余談:

Flutter WebのデフォルトフォントはRobotoだが、Robotoには日本語が入っておらず正しく表示してくれない。(Robotoを無視してくれるブラウザならちゃんと表示されるかも)

MaterialAppのfontFamilyを指定するとそれを優先順位1位に入れてくれる

レイアウトを組んで見出しをつける

CenterとColumnで適当なレイアウトを作る

ContainerとTextで適当な見出しを作る

  
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
          child: Column(
            children: [
              Container(
                width: 800.0,
                height: 500.0,
                margin: EdgeInsets.all(16.0),
                color: Colors.red,
                child: Center(
                  child: Text(
                    "運勢占い",
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 50.0
                    ),
                  ),
                ),
              )
            ],
          )
        )
    );
  }

<iframe style="width:800px;height:400px;" src="https://dartpad.dev/embed-flutter.html?id=a01114c6528e4fcf174111e224f1835a"></iframe>

スクロール可能にする

実験:見出しの縦の長さが画面より大きくなるとどうなるか?

              Container(
                width: 800.0,
                height: 5000.0,//とにかく長く
                margin: EdgeInsets.all(16.0),
                color: Colors.red,
                child: Center(
                  child: Text(
                    "運勢占い",
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 50.0
                    ),
                  ),
                ),
              )

結果:動くけど怒られる

デバッグ画面に詳細が出てくる

══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════
The following assertion was thrown during layout:
A RenderFlex overflowed by 3999 pixels on the bottom.

The relevant error-causing widget was:
  Column file:///D:/Projects/springboot/aic_flutter_5/lib/main.dart:53:18

The overflowing RenderFlex has an orientation of Axis.vertical.
The edge of the RenderFlex that is overflowing has been marked in the rendering with a yellow and
black striped pattern. This is usually caused by the contents being too big for the RenderFlex.
Consider applying a flex factor (e.g. using an Expanded widget) to force the children of the
RenderFlex to fit within the available space instead of being sized to their natural size.
This is considered an error condition because it indicates that there is content that cannot be
seen. If the content is legitimately bigger than the available space, consider clipping it with a
ClipRect widget before putting it in the flex, or using a scrollable container rather than a Flex,
like a ListView.
The specific RenderFlex in question is: RenderFlex#60548 relayoutBoundary=up2 OVERFLOWING:
  creator: Column ← Center ← _BodyBuilder ← MediaQuery ← LayoutId-[<_ScaffoldSlot.body>] ←
    CustomMultiChildLayout ← AnimatedBuilder ← DefaultTextStyle ← AnimatedDefaultTextStyle ←
    _InkFeatures-[GlobalKey#97391 ink renderer] ← NotificationListener<LayoutChangedNotification> ←
    PhysicalModel ← ⋯
  parentData: offset=Offset(441.7, 0.0) (can use size)
  constraints: BoxConstraints(0.0<=w<=1715.4, 0.0<=h<=1033.1)
  size: Size(832.0, 1033.1)
  direction: vertical
  mainAxisAlignment: start
  mainAxisSize: max
  crossAxisAlignment: center
  verticalDirection: down
◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤
════════════════════════════════════════════════════════════════════════════════════════════════════

スクロールして無限に表示できるようにしたい

→ SingleChildScrollView!

  • SingleChildScrollView: 囲うとスクロール可能になる

以下のように配置

SingleChildScrollView(
	child: Column(
		children: [
			//...
		]
	)
)

見出しの高さは戻す

ラジオボタンを使う

質問1で性別を聞いてみる

こういうのを作りたい

Radio(
	value: "男性",
	groupValue: genderAns,
	onChanged: (value) {
		setState(() {
			genderAns = value;
		});
	}
)
  • value: ラジオボタンの持つ値(String)
  • groupValue: ラジオボタンのグループで何が選択されているか(String)
  • onChanged: ラジオボタンが選択された時に呼ばれる
    • value: value変数には押されたラジオボタンのvalueが入っている(String)

ラジオボタンのグループごとに一つの変数を共有し、setStateで中身を更新することで見た目も更新される。

変数をclassの中に作る

class _MyHomePageState extends State<MyHomePage> {

  String genderAns = "男性";

以下をColumnに追加

              Text(
                  "質問1. 性別を教えて下さい",
                style: TextStyle(
                  fontSize: 30.0
                ),
              ),
              Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text("男性"),
                  Radio(
                    value: "男性",
                    groupValue: genderAns,
                    onChanged: (value) {
                      setState(() {
                        genderAns = value;
                      });
                    },
                  ),
                  Text("女性"),
                  Radio(
                    value: "女性",
                    groupValue: genderAns,
                    onChanged: (value) {
                      setState(() {
                        genderAns = value;
                      });
                    },
                  ),
                ],
              )

RowはColumnの横並び版。MainAxisSize.minで中央寄せしている

<iframe style="width:800px;height:400px;" src="https://dartpad.dev/embed-flutter.html?id=1d3ec0dfd48cacc1f0f240c01a96a8ff"></iframe>

スイッチを使う

              Switch(
                value: haveDog,
                onChanged: (value) {
                  setState(() {
                    haveDog = value;
                  });
                },
              )
  • value: スイッチの状態を管理する変数(bool)
  • onChanged: スイッチが変化した時に呼ばれる
    • value: オフにされたらfalse, オンになったらtrue

setStateしないと見た目が変わらない

実はどこでSetStateしてもよかったりする

変数をclassの中に作る

class _MyHomePageState extends State<MyHomePage> {

  String genderAns = "男性";
  bool haveDog = false;

Columnに以下を追加

              Text(
                "質問2. 犬を飼っていますか?",
                style: TextStyle(
                    fontSize: 30.0
                ),
              ),
              Switch(
                value: haveDog,
                onChanged: (value) {
                  setState(() {
                    haveDog = value;
                  });
                },
              )

<iframe style="width:800px;height:400px;" src="https://dartpad.dev/embed-flutter.html?id=a10bb34cddc778d7cd3e642d6da6e50f"></iframe>

スライダーを使う

                  Slider(
                    value: feel,
                    onChanged: (value) {
                      setState(() {
                        feel = value;
                      });
                    },
                  ),
  • value: スライダーの現在の値を持つ変数(double)
  • onChanged: スライドされたらその値がvalueに入って呼ばれる

変数をclassの中に作る

class _MyHomePageState extends State<MyHomePage> {

  String genderAns = "男性";
  bool haveDog = false;
  double feel = 0.4;

Columnに以下を追加

              Text(
                "質問3. 現在の気分は?",
                style: TextStyle(
                    fontSize: 30.0
                ),
              ),
              Text("${(feel * 100).round()}"),
              Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text("悪い"),
                  Slider(
                    value: feel,
                    onChanged: (value) {
                      setState(() {
                        feel = value;
                      });
                    },
                  ),
                  Text("良い")
                ],
              )

<iframe style="width:800px;height:400px;" src="https://dartpad.dev/embed-flutter.html?id=b735a2d215ee07eb08c46f589d32236a"></iframe>

2つ目のTextでは100倍して四捨五入した値を表示している

入力欄

前回やったTextFieldを使う

変数とかをclassに記述する

class _MyHomePageState extends State<MyHomePage> {

  String genderAns = "男性";
  bool haveDog = false;
  double feel = 0.4;
  TextEditingController favoriteNumberController;
  
  
  void initState() {
    super.initState();
    favoriteNumberController = new TextEditingController(text: "334");
  }
  
  
  void dispose() {
    favoriteNumberController.dispose();
    super.dispose();
  }

Columnに以下を追加

              Text(
                "質問4. 好きな数字は?",
                style: TextStyle(
                    fontSize: 30.0
                ),
              ),
              Container(
                width: 400.0,
                child: TextField(
                  controller: favoriteNumberController,
                  textAlign: TextAlign.center,
                ),
              )

<iframe style="width:800px;height:400px;" src="https://dartpad.dev/embed-flutter.html?id=2c4103104b79c27360e318aa04e2cde2"></iframe>

チェックボックスを使う

                Checkbox(
                  value: haveDog,
                  onChanged: (value) {
                    setState(() {
                      haveDog = value;
                    });
                  },
                ),
  • value: チェックボックスの状態を管理する変数
  • onChanged: チェックを入れる:true チェックを外す:false

Columnに以下を追加

                Text(
                  "質問5. チェックしてみてください",
                  style: TextStyle(
                      fontSize: 30.0
                  ),
                ),
                Checkbox(
                  value: haveDog,
                  onChanged: (value) {
                    setState(() {
                      haveDog = value;
                    });
                  },
                ),

Switchと同じ変数で管理しているので見た目が連動する

<iframe style="width:800px;height:400px;" src="https://dartpad.dev/embed-flutter.html?id=595713ffb79c60c6ab548658b1aaf239"></iframe>

結果ページを作る

libフォルダにresult.dartを追加

[libフォルダを右クリック] -> [New] -> [Dart file]

以下をそのまま貼り付ける

import 'dart:math';

import 'package:flutter/material.dart';

class ResultPage extends StatelessWidget {

  List<String> result = ["大吉!", "中吉!", "吉!", "凶", "大凶!!!"];

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text("結果は..."),
            Text(
              result[Random().nextInt(result.length)],
              style: TextStyle(
                color: Colors.red,
                fontSize: 60.0
              ),
            ),
            FlatButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              child: Text("戻る"),
            )
          ],
        ),
      ),
    );
  }
}

Columnにボタンを追加

                RaisedButton(
                  onPressed: () {
                    Navigator.of(context).push(
                      MaterialPageRoute(
                        builder: (context) {
                          return ResultPage();
                        },
                      ),
                    );
                  },
                  color: Colors.red,
                  child: Text(
                      "結果を見る",
                    style: TextStyle(
                      color: Colors.white,
                      fontWeight: FontWeight.bold
                    ),
                  ),
                )

ランダムで運勢が出てくる

回答結果を次のページに反映する

結果に反映させるかはさておいて、結果のページで回答結果を使いたい

→コンストラクタを使う

結果ページに以下のように追加

class ResultPage extends StatelessWidget {
  String _gender;
  bool _haveDog;
  double _feel;
  String _favoriteNumber;

  ResultPage(String gender, bool haveDog, double feel, String favoriteNumber) {
    _gender = gender;
    _haveDog = haveDog;
    _feel = feel;
    _favoriteNumber = favoriteNumber;
  }
            FlatButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              child: Text("戻る"),
            ),
            Text("性別"),
            Text(_gender),
            Text("犬を飼っているか"),
            Text("$_haveDog"),
            Text("今の気分"),
            Text("$_feel"),
            Text("好きな数字"),
            Text("$_favoriteNumber"),

main.dart

                RaisedButton(
                  onPressed: () {
                    Navigator.of(context).push(
                      MaterialPageRoute(
                        builder: (context) {
                          return ResultPage(genderAns, haveDog, feel, favoriteNumberController.text); //ここ変更
                        },
                      ),
                    );
                  },

コンストラクタ

  ResultPage(String gender, bool haveDog, double feel, String favoriteNumber) {
    _gender = gender;
    _haveDog = haveDog;
    _feel = feel;
    _favoriteNumber = favoriteNumber;
  }

この部分。classの中でclass名と同じ名前の関数を定義すると、
それがコンストラクタになってページを生成する時に呼び出されるようになる。
普通の関数と違い関数名の前に返り値の型を書かない

呼ばれると関数の中括弧{}内の処理を行い、変更を反映したobjectを生成する。

Dartではコンストラクタはclassに一つしか作れない

結果を反映してみる

完全ランダムは悲しいのでコンストラクタで引数の値によって結果を変えてみる

class ResultPage extends StatelessWidget {
  String _gender;
  bool _haveDog;
  double _feel;
  String _favoriteNumber;

  String resultValue; //追加

  ResultPage(String gender, bool haveDog, double feel, String favoriteNumber) {
    _gender = gender;
    _haveDog = haveDog;
    _feel = feel;
    _favoriteNumber = favoriteNumber;

    //追加
    if(_haveDog) {
      resultValue = "大吉!";
    }
    else {
      resultValue = result[Random().nextInt(result.length)];
    }
  }
            Text("結果は..."),
            Text(
              resultValue, //変更
              style: TextStyle(
                color: Colors.red,
                fontSize: 60.0
              ),
            ),

犬を飼っているならば無条件で大吉になる

https://github.com/organic-nailer/aic_flutter_web_5: 全体のコード

main.dart

result.dart

おしまい