Re:birth エンジニアリング

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

Flutterで画面を縦or横に固定する方法 (Portrait, Landscape)

今回はFlutterでの画面の向きの制御について学習します。

画面を縦方向(Portrait)にのみ固定する

サンプルコードです。 SystemChrome.setPreferredOrientations(List<DeviceOrientation> orientations)を使えばいいだけです。 と思ったらFlutter SDKのバージョンによって書き方が違うのか数分ほどハマりました。

最初のコード (* services.dartが必要なのでimportします。)

main.dart

/// 追加する
import 'package:flutter/services.dart';

void main() => {
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown,
  ]);
  runApp(new MyApp());
};

エラーが発生してビルドできませんでした。

setPreferredOrientationsの中身を見に行きます。

  static Future<void> setPreferredOrientations(List<DeviceOrientation> orientations) async {
    await SystemChannels.platform.invokeMethod<void>(
      'SystemChrome.setPreferredOrientations',
      _stringify(orientations),
    );
  }

どうやら、setPreferredOrientationsは非同期のようです。Futureが返ってきます。 そのため、非同期として.thenでつなげます。

void main() => {
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown
  ]).then((_) {
    runApp(new MyApp());
  })
};

今度はビルドが通りましたが、画面が真っ白です。ピクリともしません。 正しいんじゃないの?と何回もビルドを実行しますが駄目でした。

どうやら、 SystemChrome.setPreferredOrientationsはお作法として事前に

WidgetsFlutterBinding.ensureInitialized()

を実行しないといけないらしいです。

main.dart

void main() => {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown
  ]).then((_) {
    runApp(new MyApp());
  })
};

これに書き換えますが、今度はWidgetsFlutterBinding.ensureInitialized();でエラーが発生しました。 フザケンナ。一体どうしたら画面固定が実現するのだろうと調査しまくっていたらやっと正解が分かりました。

正解のコード

main.dart

import 'package:flutter/services.dart';

void main() => {
  WidgetsFlutterBinding.ensureInitialized(),
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown
  ]).then((_) {
    runApp(new MyApp());
  })
};

より分かりませんが、(,)で繋げる書き方なのだと思います。 ただ自信がありません。

画面を横方向(Landscape)にのみ固定する

縦方向の固定ができたら横方向は簡単です。

main.dart

/// 追加する
import 'package:flutter/services.dart';

void main() => {
  WidgetsFlutterBinding.ensureInitialized(),
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.landscapeLeft,
    DeviceOrientation.landscapeRight
  ]).then((_) {
    runApp(new MyApp());
  })
};

これで横方向のみに画面を制御できるようになります。

Flutterで画面の向きを取得する方法

Flutterでも画面の向きを取得できます。 MediaQueryというクラスを使えば、画面の情報を取得できます。

    final Orientation orientation = MediaQuery.of(context).orientation;
    final isLandscape = MediaQuery.of(context).orientation == Orientation.landscape;
    if (isLandscape) {
      print('横向きである');
    }

Orientation型は次のように定義されています。

/// Whether in portrait or landscape.
enum Orientation {
  /// Taller than wide.
  portrait,

  /// Wider than tall.
  landscape
}

以上でFlutterでの画面制御の方法の学習が完了しました。

【クイズアプリ開発】 Flutterでリセット画面を実装する

前回のあらすじ

blog.tamappe.com

blog.tamappe.com

blog.tamappe.com

今回はFlutterで4択クイズが終わったあとのリセット画面を実装したいと思います。

f:id:qed805:20200311225052p:plain
リセット画面

Sketchでデザインしました。

とりあえず、

  • 正答数
  • リセットボタン

の2つを目標にしようと思います。 またリセットボタンを実装するためボタンの実装が必要になります。 前回もボタンの実装をしたかもしれません。

リセット画面の実装

今回はwidgetパッケージにresult_page.dartファイルを追加しています。

f:id:qed805:20200311225400p:plain
リセット画面用のウィジェット

result_page.dart

import 'package:flutter/material.dart';

class ResultPage extends StatelessWidget {

  final Function _tapResetButton;

  ResultPage(this._tapResetButton);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('問題が終了しました!'),
        RaisedButton(
          child: Text('リセットする'),
          onPressed: _tapResetButton,
        )
      ],
    );
  }
}

最初はこれどう考えてもStatefullWidgetじゃないとindexを0にリセットできないよなと思って StatefullWidgetで実装していました。 というのもsetState() {}メソッドで画面を更新したかったからです。 ですが、どうやら、親から子に渡したindexを子で更新しても親には反映されないので画面切り替えができなかったのです。

ミスった設計

class ResultPage extends StatefulWidget {
  int quesitonIndex;
  
  ResultPage(this.quesitonIndex);
  
  @override
  _ResultPageState createState() => _ResultPageState();
}

class _ResultPageState extends State<ResultPage> {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('問題が終了しました!'),
        RaisedButton(
          child: Text('リセットする'),
          onPressed: () {
            setState(() {
              widget.quesitonIndex = 0;
            });
          },
        )
      ],
    );
  }
}

これでハマって1日やる気がなくなりました。 思い返して、Functionでメソッドを渡してやればいいだけだと気づいてStatelessWidgetで実装してみることになりました。

ボタンタップ時の実装

今回は選択肢ボタンをタップしたときの処理とリセットボタンの処理を実装していきます。

選択肢ボタンをタップしたときは問題文のindexを1増やしてやればいいです。 リセットボタンをタップしたときはindexを0にすればいいですね。

つまり、main.dartにて次の関数を宣言します。

main.dart

  void _answerQuestion() {
    setState(() {
      _questionIndex++;
    });
  }

  void _resetIndex() {
    setState(() {
      _questionIndex = 0;
    });
  }

あとはそれぞれのButtonのonPressedにこのメソッドを渡せばいいという感じです。

前回は選択肢ボタンをAnswerButton命名していましたので次のように代入していきます。

AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        answerQuestion: _answerQuestion,
                        keyString: 'd')

ただ、これだとAnswerButtonはButtonクラスだと分かりますが、どれがonPressedに該当するのか読み取りにくくなりました。 無念です。 今回みたいにFunction型のプロパティを使う場合は変数名に気をつけないといけないのが分かりました。 多分、onPressedに該当するプロパティはonPressedAnswerButtonというような命名のほうがわかりやすいのかもしれません。

それはまあ今後検討してみます。

問題画面からリセット画面に遷移させる方法

率直に言えば、if文を使うことになります。

前回、

main.dart

  /// 問題文のindex
  var _questionIndex = 0;
  /// 問題
  var _questions = [
    {
      'question':
          'The weather in Merizo is very (x) year-round, though there are showers almost daily from December through March.',
      'a': 'agreeable',
      'b': 'agree',
      'c': 'agreement',
      'd': 'agreeably',
      'correctAnswer': 'A'
    },
    {
      'question':
          '(x) for the competition should be submitted by November 28 at the latest.',
      'a': 'Enter',
      'b': 'Entered',
      'c': 'Entering',
      'd': 'Entries',
      'correctAnswer': 'D'
    }
  ];

と宣言したので

body: Center(
        child: _questionIndex < _questions.length ? 問題文 : リセット画面

というふうに三項演算子を使って切り替えようと思います。 実際はデザインペターンみたいな設計があるように思いますがまだ設計は学習前なので難しいことはできません。

まとめ

それでは上記を総括してサンプルコードを載せたいと思います。

result_page.dart

import 'package:flutter/material.dart';

class ResultPage extends StatelessWidget {

  final Function _tapResetButton;

  ResultPage(this._tapResetButton);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('問題が終了しました!'),
        RaisedButton(
          child: Text('リセットする'),
          onPressed: _tapResetButton,
        )
      ],
    );
  }
}

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_quiz_app/widget/result_page.dart';
import './utils/constants.dart';
import './widget/answer_button.dart';
import './widget/question_view.dart';


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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

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

class _MyHomePageState extends State<MyHomePage> {
  /// 問題文のindex
  var _questionIndex = 0;
  /// 問題
  var _questions = [
    {
      'question':
          'The weather in Merizo is very (x) year-round, though there are showers almost daily from December through March.',
      'a': 'agreeable',
      'b': 'agree',
      'c': 'agreement',
      'd': 'agreeably',
      'correctAnswer': 'A'
    },
    {
      'question':
          '(x) for the competition should be submitted by November 28 at the latest.',
      'a': 'Enter',
      'b': 'Entered',
      'c': 'Entering',
      'd': 'Entries',
      'correctAnswer': 'D'
    }
  ];

  void _answerQuestion() {
    setState(() {
      _questionIndex++;
    });
  }

  void _resetIndex() {
    setState(() {
      _questionIndex = 0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('文法問題'),
      ),
      body: Center(
        child: _questionIndex < _questions.length ? Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: [
            Container(
              height: Constants().questionAreaHeight,
              color: Colors.red,
              child: Center(
                child: Text(
                  '(x)に入る単語を答えよ。',
                  style: TextStyle(fontSize: 20),
                ),
              ),
            ),
            Container(
              height: Constants().questionAreaHeight,
              color: Colors.green,
              child: Center(
                child: Text(
                  'Q${_questionIndex + 1}',
                  style: TextStyle(fontSize: 18),
                ),
              ),
            ),
            QuestionView(questionIndex: _questionIndex, questions: _questions),
            Expanded(
              child: Padding(
                padding: const EdgeInsets.fromLTRB(50.0, 30.0, 50.0, 50.0),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        answerQuestion: _answerQuestion,
                        keyString: 'a'),
                    AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        answerQuestion: _answerQuestion,
                        keyString: 'b'),
                    AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        answerQuestion: _answerQuestion,
                        keyString: 'c'),
                    AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        answerQuestion: _answerQuestion,
                        keyString: 'd'),
                  ],
                ),
              ),
            )
          ],
        ) : ResultPage(_resetIndex),
      ),
    );
  }
}

このように変更してみました。 ビルドが成功したら、選択肢ボタンをタップすると問題が切り替わって3回目でリセット画面が表示されるはずです。

f:id:qed805:20200311231632p:plain
リセット画面

デザインはまだ反映していません。 リセットボタンをタップするとまた問題1に戻る画面遷移になります。

残るタスクは

  • リセット画面のデザイン実装
  • 問題の正答数の計算

ぐらいは実装しようかなと思っています。 本日はこれくらいで終わりにします。

【クイズアプリ開発】選択肢のボタンデザインを整える

今回は選択肢のボタンのデザインを綺麗にしていきます。 それとボタンをタップしたときの処理も書いていきます。

f:id:qed805:20200307181004p:plain
想定しているデザイン

ボタンの角丸実装

まずはRaisedButtonに角丸を実装します。 過去記事に角丸を実装する方法について書いていますので復習します。

blog.tamappe.com

ClipRRectを使ってborderRadiusプロパティで角丸の度合いを調整します。

ClipRRect(
      borderRadius: BorderRadius.circular(Constants().answerButtonHeight / 2),
      child: SizedBox(
          width: double.infinity,
          height: Constants().answerButtonHeight,
          child: RaisedButton(
            child: Text(questions[questionIndex][keyString]),
            onPressed: null,
          )
      ),
    )

値のConstants()は定数クラスから参照しています。

constants.dart

class Constants {
  /// 問題文ウィジェットの高さ
  final double questionAreaHeight = 70.0;
  /// 回答の選択肢ボタンの高さ
  final double answerButtonHeight = 50.0;
}

ボタンの高さが50なので、半分の25を指定すればきれいな角丸が完成します。

ボタンの色変更

次にRaisedButtonに色をつけます。 Sketchのデザインの指定色は#D6D7D7なので16進数で指定します。

FlutterのColorクラスには16進数で指定できます。

const color = const Color(0xffD6D7D7)

0xffは意味合いは違いますが#と同じものと思っていいでしょう。 Color(0xff[16進数])で16進数の色を反映できます。

Colorの16進数実装

ですが、実際のプロジェクトでその都度0xffを書くのは煩わしいと思います。 なので、何かしら簡略化する方法はないものかを調査してみたらさすがに議論されていました。

stackoverflow.com

こちらのところから

class HexColor extends Color {
  static int _getColorFromHex(String hexColor) {
    hexColor = hexColor.toUpperCase().replaceAll("#", "");
    if (hexColor.length == 6) {
      hexColor = "FF" + hexColor;
    }
    return int.parse(hexColor, radix: 16);
  }

  HexColor(final String hexColor) : super(_getColorFromHex(hexColor));
}

を拝借します。

新しいファイルhex_color.dartを追加して下記のコードを書きます。

hex_color.dart

import 'package:flutter/material.dart';

class HexColor extends Color {
  static int _getColorFromHex(String hexColor) {
    hexColor = hexColor.toUpperCase().replaceAll("#", "");
    if (hexColor.length == 6) {
      hexColor = "FF" + hexColor;
    }
    return int.parse(hexColor, radix: 16);
  }

  HexColor(final String hexColor) : super(_getColorFromHex(hexColor));
}

これを使うことで

HexColor('#D6D7D7')

と書けるようになります。

ボタンのタップイベント

そして、最後にRaisedButtonのタップイベントの処理を実装します。 ボタンタップしたら問題のindexを+1にして画面を更新させればいいわけですね。

main.dartにその更新の処理を記載します。

  void _answerQuestion() {
    setState(() {
      _questionIndex++;
    });
  }

まだproviderと言った高等技術は使えませんのでsetState()を呼ぶことで画面を更新させることにします。 あとはこの_answerQuestionをボタンの引数で渡してやればいいでしょう。

最終的にanswer_button.dartを次のように書き直します。

answer_button.dart

import 'package:flutter/material.dart';
import 'package:flutter_quiz_app/utils/hex_color.dart';
import '../utils/constants.dart';

class AnswerButton extends StatelessWidget {
  final int questionIndex;
  final List<Map<String, Object>> questions;
  final Function answerQuestion;
  final String keyString;

  AnswerButton(
      {this.questionIndex,
      this.questions,
      this.answerQuestion,
      this.keyString});

  @override
  Widget build(BuildContext context) {
    return ClipRRect(
      borderRadius: BorderRadius.circular(Constants().answerButtonHeight / 2),
      child: SizedBox(
          width: double.infinity,
          height: Constants().answerButtonHeight,
          child: RaisedButton(
            color: HexColor('#D6D7D7'),
            child: Text(questions[questionIndex][keyString]),
            onPressed: answerQuestion,
          )
      ),
    );
  }
}

final Function answerQuestion;という関数型のプロパティを用意してonPressedに代入します。 あとはmain.dartで実装した_answerQuestionRaisedButtonanswerQuestionに渡してやればOKです。

結果的にmain.dartは次のように変更されます。

main.dart

import 'package:flutter/material.dart';
import './utils/constants.dart';
import './widget/answer_button.dart';
import './widget/question_view.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

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

class _MyHomePageState extends State<MyHomePage> {
  /// 問題文のindex
  var _questionIndex = 0;

  var _questions = [
    {
      'question':
          'The weather in Merizo is very (x) year-round, though there are showers almost daily from December through March.',
      'a': 'agreeable',
      'b': 'agree',
      'c': 'agreement',
      'd': 'agreeably',
      'correctAnswer': 'A'
    },
    {
      'question':
          '(x) for the competition should be submitted by November 28 at the latest.',
      'a': 'Enter',
      'b': 'Entered',
      'c': 'Entering',
      'd': 'Entries',
      'correctAnswer': 'D'
    }
  ];

  void _answerQuestion() {
    setState(() {
      _questionIndex++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('文法問題'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: [
            Container(
              height: Constants().questionAreaHeight,
              color: Colors.red,
              child: Center(
                child: Text(
                  '(x)に入る単語を答えよ。',
                  style: TextStyle(fontSize: 20),
                ),
              ),
            ),
            Container(
              height: Constants().questionAreaHeight,
              color: Colors.green,
              child: Center(
                child: Text(
                  'Q${_questionIndex + 1}',
                  style: TextStyle(fontSize: 18),
                ),
              ),
            ),
            QuestionView(questionIndex: _questionIndex, questions: _questions),
            Expanded(
              child: Padding(
                padding: const EdgeInsets.fromLTRB(50.0, 30.0, 50.0, 50.0),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        answerQuestion: _answerQuestion,
                        keyString: 'a'),
                    AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        answerQuestion: _answerQuestion,
                        keyString: 'b'),
                    AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        answerQuestion: _answerQuestion,
                        keyString: 'c'),
                    AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        answerQuestion: _answerQuestion,
                        keyString: 'd'),
                  ],
                ),
              ),
            )
          ],
        ),
      ),
    );
  }
}

これでビルドすると選択肢のボタンのデザインが角丸に変わっているはずです。 またボタンをタップすると次の問題に切り替わります。

問題1 問題2
f:id:qed805:20200307183754p:plain
問題1
f:id:qed805:20200307183808p:plain
問題2

ですが、問題3以降がありませんので選択肢のボタンを3回押すとエラー画面が発生します。

f:id:qed805:20200307183911p:plain
エラー画面

簡単いえば、questionsの配列が2つしかないのにindex=2 にアクセスしようとしてエラーが発生するものです。 この対応を次回に行おうと思います。

【クイズアプリ開発】定数クラスの作成とウィジェットの共通化

今回は前回作成したクイズアプリのレイアウトのリファクタリングを行います。

blog.tamappe.com

前回はレイアウトの組立自体は完了しましたがwidgetのインデントが深くなりすぎていました。 Flutter開発の肝はwidgetのインデントはなるべく深くならないようにすること、 つまりwidgetの粒度をなるべく小さくしてメンテナンス性をあげることらしいです。

そのため今回は画面の見え方は同じですが、裏側のロジックを分割してコードの見た目を綺麗にしていきたいと思います。

widgetの分割方法について

以前の記事にカスタムウィジェットクラスの作成方法について解説したことがあります。

blog.tamappe.com

blog.tamappe.com

前回のクラスでは4つの選択肢のRaisedButtonの部分が共通化できそうだったので、選択肢のボタン部分を綺麗にしていきたいです。 また問題文の英文は動的に変わる部分なので、文のStringをプロパティとしたカスタムなTextウィジェットクラスも作成したいです。 そして、今回から定数クラスも作成していきたいので新たに新規作成するクラスは

これらを作成していきます。

定数クラス

定数クラスはconstants.dartで作成します。

Constants.dart

class Constants {
  /// 問題文ウィジェットの高さ
  final double questionAreaHeight = 70.0;
  /// 回答の選択肢ボタンの高さ
  final double answerButtonHeight = 50.0;
}

とりあえず、このようにして数値に関わる部分を定数化していきます。

問題文Textのカスタムウィジェットクラス

次に問題文のウィジェット(widget)をカスタムクラスにしておきます。 ここは問題のデータに応じてtextが動的に変更されてしまいます。 そこでこのtext部分だけ渡したいですが、データを配列で渡す想定なので問題データ配列とindexを 引数にしてmain.dartから渡せるようにします。

blog.tamappe.com

それを踏まえてカスタムクラスquestion_view.dartを作成します。

question_view.dart

import 'package:flutter/material.dart';

class QuestionView extends StatelessWidget {
  /// 問題文のidかindexを渡す想定
  final int questionIndex;
  /// 問題文オブジェクト
  final List<Map<String, Object>> questions;

  QuestionView({@required this.questionIndex, @required this.questions});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellow,
      child: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Center(
          child: Text(
            questions[questionIndex]['question'],
            style: TextStyle(fontSize: 20),
          ),
        ),
      ),
    );
  }
}

これで問題文を動的に渡す準備ができました。

選択肢RaisedButtonのカスタムウィジェットクラス

最後に回答の選択肢のボタンが4つ同じような構成で存在しますので共通できないかを検討します。 こちらはanswer_button.dartというファイル名で作成します。

blog.tamappe.com

answer_button.dart

import 'package:flutter/material.dart';
import '../utils/constants.dart';

class AnswerButton extends StatelessWidget {
  final int questionIndex;
  final List<Map<String, Object>> questions;
  final String keyString;

  AnswerButton({
    this.questionIndex,
    this.questions,
    this.keyString
  });

  @override
  Widget build(BuildContext context) {
    return SizedBox(
        width: double.infinity,
        height: Constants().answerButtonHeight,
        child: RaisedButton(
          child: Text(questions[questionIndex][keyString]),
          onPressed: null,
        ));
  }
}

main.dartを書き換える

今回は3つのファイルを新しく作成しました。 今後も新しいウィジェットを作成して規模が大きくなるかもしれませんのでディレクトリ構成を考えておきます。 私は今回の想定では次にようにしました。

  • utils パッケージ (便利系クラス)
  • widgets パッケージ (見える部分のカスタムウィジェット)

f:id:qed805:20200307141426p:plain
パッケージ構成

それではこれを踏まえてmain.dartを書き換えます。

main.dart

import 'package:flutter/material.dart';
import './utils/constants.dart';
import './widget/answer_button.dart';
import './widget/question_view.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

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

class _MyHomePageState extends State<MyHomePage> {
  /// 問題文のindex
  var _questionIndex = 0;

  var _questions = [
    {
      'question':
          'The weather in Merizo is very (x) year-round, though there are showers almost daily from December through March.',
      'a': 'agreeable',
      'b': 'agree',
      'c': 'agreement',
      'd': 'agreeably',
      'correctAnswer': 'A'
    },
    {
      'question':
          '(x) for the competition should be submitted by November 28 at the latest.',
      'a': 'Enter',
      'b': 'Entered',
      'c': 'Entering',
      'd': 'Entries',
      'correctAnswer': 'D'
    }
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('文法問題'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: [
            Container(
              height: Constants().questionAreaHeight,
              color: Colors.red,
              child: Center(
                child: Text(
                  '(x)に入る単語を答えよ。',
                  style: TextStyle(fontSize: 20),
                ),
              ),
            ),
            Container(
              height: Constants().questionAreaHeight,
              color: Colors.green,
              child: Center(
                child: Text(
                  'Q${_questionIndex + 1}',
                  style: TextStyle(fontSize: 18),
                ),
              ),
            ),
            QuestionView(questionIndex: _questionIndex, questions: _questions),
            Expanded(
              child: Padding(
                padding: const EdgeInsets.fromLTRB(50.0, 30.0, 50.0, 50.0),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        keyString: 'a'),
                    AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        keyString: 'b'),
                    AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        keyString: 'c'),
                    AnswerButton(
                        questions: _questions,
                        questionIndex: _questionIndex,
                        keyString: 'd'),
                  ],
                ),
              ),
            )
          ],
        ),
      ),
    );
  }
}

今回は選択肢のボタンをタップすると問題が動的に切り替わるように変更したためMyHomePageStatefulWidgetに書き直しました。

これで一度ビルドしてみましょう。 見え方が前回と同じであればリファクタリングの成功です。

f:id:qed805:20200307141757p:plain
リファクタリング後の画面

次回はこの構成をもとにしてクイズアプリの機能を拡張していきます。

Flutter開発の逆引き用目次

Flutterをインストールして最初に行うこと

Textの使い方

blog.tamappe.com

blog.tamappe.com

ウィジェットのStateクラス

blog.tamappe.com

ボタンウィジェットの実装について

blog.tamappe.com

レイアウトの組み立て方

blog.tamappe.com

blog.tamappe.com

blog.tamappe.com

iOSアプリで言うところのUITableViewみたいな一覧を作れるウィジェット

blog.tamappe.com

blog.tamappe.com

blog.tamappe.com

UIScrollViewみたいなウィジェット

blog.tamappe.com

UITabbarControllerに似たウィジェット

blog.tamappe.com

blog.tamappe.com

blog.tamappe.com

画面遷移

blog.tamappe.com

blog.tamappe.com

データベース

blog.tamappe.com

RSSアプリ

blog.tamappe.com

blog.tamappe.com

blog.tamappe.com

画像の扱い方

blog.tamappe.com

カスタムなウィジェットクラスを作成する方法

blog.tamappe.com

blog.tamappe.com