
マイケル
みなさんこんにちは!
マイケルです!
マイケルです!

エレキベア
こんにちクマ〜〜〜

マイケル
今回は気分転換でFlutterを触ってみました。
作ったのは下記のような、シンプルな英単語学習アプリになります!
作ったのは下記のような、シンプルな英単語学習アプリになります!


エレキベア
Flutter久しぶりクマ〜〜〜

マイケル
内容としては単純なものになりますが、データはGoogleスプレッドシート上で作成したものをAPI経由で取得するようにしています!

エレキベア
DBの代わりにスプレッドシートを使った感じクマね

マイケル
今回はこのアプリの実装内容について解説していこうと思います!
ソースコードの方はGitHubにも上げていますので、こちらもよければご参照ください!
ソースコードの方はGitHubにも上げていますので、こちらもよければご参照ください!
GitHub – masarito617/flutter-english-study-app-sample

エレキベア
この量だとコードを読むのが一番手っ取り早そうクマね

マイケル
なお、Flutterのバージョンは3.3.9を使用しています。
バージョンによって差異が生じる可能性があるため、そちらはご了承ください!
バージョンによって差異が生じる可能性があるため、そちらはご了承ください!
作成するアプリ
画面構成

マイケル
今回作成したアプリは大きく
・タイトル画面
・単語選択画面
の2つの画面から出来ています。
・タイトル画面
・単語選択画面
の2つの画面から出来ています。
タイトル画面

単語選択画面




マイケル
英単語の日本語訳を4択から選ぶ構成となっています。

エレキベア
シンプルなクイズ形式のアプリクマね
Googleスプレッドシート

マイケル
そして一点工夫した点としては、英単語データはGoogleスプレッドシート上から読み込むようにしたことです。
下記のように英単語と日本語訳を設定しておくことで読み込むことができます。
下記のように英単語と日本語訳を設定しておくことで読み込むことができます。


エレキベア
これなら手軽に問題の編集や追加が行えそうクマね

マイケル
読込はGoogle Sheets APIというAPIを使用していました。
こちらも詳細は後ほど記載します!
こちらも詳細は後ほど記載します!
フォルダ構成

マイケル
ソースコードのフォルダ構成は下記のようにしています。
main.dartとpages配下のdartファイルがメインとなります。
main.dartとpages配下のdartファイルがメインとなります。
lib
├── components
│ └── layout_widgets.dart
├── data
│ └── english_word_data.dart
├── pages
│ ├── select_word_page.dart
│ └── title_page.dart
├── settings
│ └── googleapi_settings.dart
└── main.dart
↑ソースコードのフォルダ構成
エレキベア
このボリュームだとコンパクトクマね
タイトル画面

マイケル
それでは実装を見ていきます。
まずはタイトル画面の実装になります。
まずはタイトル画面の実装になります。

タイトル画面全体の実装

マイケル
main.dartファイルが処理の起点となっていて、ここで全体のテーマを設定しています。
homeにtitle_page.dartのTitlePage()を指定することでタイトル画面を表示します。
homeにtitle_page.dartのTitlePage()を指定することでタイトル画面を表示します。
import 'package:flutter/material.dart';
import 'pages/title_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'えいすた',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.lime,
),
home: const TitlePage(),
);
}
}
↑main処理には全体のテーマを設定
エレキベア
アプリ全体の設定をしているクマね

マイケル
TitlePageはStatefulWidgetとして定義していて、大枠となるWidget構成やボタン押下時の処理を記述しています。
import 'package:flutter/material.dart';
import 'select_word_page.dart';
import '../components/layout_widgets.dart';
import '../data/english_word_data.dart';
/// タイトル画面
class TitlePage extends StatefulWidget {
const TitlePage({super.key});
@override
State<TitlePage> createState() => _TitlePageState();
}
class _TitlePageState extends State<TitlePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('えいすた!'),
leading: Icon(
Icons.book,
color: Theme.of(context).primaryColor,
),
),
body: Center(
child: Column(
children: [
HalfScreenArea(
child: Center(
child: TitleTextAreaWidget(
errorMessage: _errorMessage,
),
),
),
HalfScreenArea(
child: Center(
child: TitleButtonAreaWidget(
isOptionShuffle: _isOptionShuffle,
onChangedOptionShuffleCheckBox:
onChangedOptionShuffleCheckBox,
onPressedStartButton: onPressedStartButton,
),
),
),
],
),
),
);
}
・・・
}
・・・
↑タイトル画面をStatefulWidgetとして定義
マイケル
HalfScreenAreaというのは画面半分サイズのエリアを取得するよう、共通のWidgetとして定義したものです。
こちらは問題選択画面でも使用します。
こちらは問題選択画面でも使用します。
import 'package:flutter/material.dart';
/// 画面半分サイズのエリア
class HalfScreenArea extends StatelessWidget {
final Widget child;
const HalfScreenArea({super.key, required this.child});
@override
Widget build(BuildContext context) {
// デバイスの高さを取得して設定
final double deviceHeight = MediaQuery.of(context).size.height;
return Expanded(
flex: 1,
child: SizedBox(
height: deviceHeight,
child: child,
),
);
}
}
↑画面半分のエリア定義
エレキベア
画面を半々で作っているクマね

マイケル
上半分をタイトルテキストエリア、下半分をタイトルボタンエリアとしてStatelessWidgetとして分割しています。
こちらパラメータやボタン押下処理を受け取って設定しているだけですね。
こちらパラメータやボタン押下処理を受け取って設定しているだけですね。
/// タイトルテキストエリア
class TitleTextAreaWidget extends StatelessWidget {
final String errorMessage; // エラーメッセージ
const TitleTextAreaWidget({super.key, required this.errorMessage});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// タイトル
const Text(
'Let\'s English!!',
style: TextStyle(
fontSize: 24,
),
),
const SizedBox(height: 20),
// エラーメッセージ
SizedBox(
height: 28,
child: Text(
errorMessage,
style: const TextStyle(
fontSize: 16,
color: Colors.red,
),
),
),
const SizedBox(height: 40),
],
);
}
}
/// タイトルボタンエリア
class TitleButtonAreaWidget extends StatelessWidget {
final bool isOptionShuffle;
final Function onChangedOptionShuffleCheckBox;
final Function onPressedStartButton;
const TitleButtonAreaWidget(
{super.key,
required this.isOptionShuffle,
required this.onChangedOptionShuffleCheckBox,
required this.onPressedStartButton});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// シャッフルオプション
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Checkbox(
value: isOptionShuffle,
onChanged: (isOn) => onChangedOptionShuffleCheckBox(isOn),
),
const Text(
'問題をシャッフルする',
style: TextStyle(
fontSize: 16,
),
),
],
),
const SizedBox(height: 20),
// STARTボタン
SizedBox(
width: 120,
height: 60,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
textStyle: const TextStyle(fontSize: 20),
foregroundColor: Theme.of(context).backgroundColor,
backgroundColor: Theme.of(context).primaryColor,
),
onPressed: () => onPressedStartButton(),
child: const Text('START'),
),
),
],
);
}
}
↑WidgetはそれぞれStatelessWidgetとして分離
エレキベア
一つにまとめて書いてしまうとかなり長くなってしまうクマからね
ボタン押下処理

マイケル
各ボタンの押下処理は下記のようになっています。
シャッフルするかのチェックボックスは単純にフラグを切り替えるだけで、STARTボタン押下時には英単語データを取得して単語選択ページに遷移するよう実装しています。
シャッフルするかのチェックボックスは単純にフラグを切り替えるだけで、STARTボタン押下時には英単語データを取得して単語選択ページに遷移するよう実装しています。
・・・
class _TitlePageState extends State<TitlePage> {
・・・
bool _isOptionShuffle = false; // シャッフルするか?
String _errorMessage = ""; // エラーメッセージ
bool _isDoProcess = false; // 処理中か?
/// シャッフルチェックボックス切替時
void onChangedOptionShuffleCheckBox(bool isOn) {
setState(() {
_isOptionShuffle = isOn;
});
}
/// STARTボタン押下時
void onPressedStartButton() async {
if (_isDoProcess) return;
_isDoProcess = true;
setState(() => _errorMessage = "");
// データ取得
var englishWordDataList =
await EnglishWordDataRepository.getEnglishWordDataListFromApi();
if (englishWordDataList.isEmpty) {
setState(() => _errorMessage = "データが取得できません");
_isDoProcess = false;
return;
}
// データを渡してページ遷移
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SelectWordPage(
englishWordDataList: englishWordDataList,
isOptionShuffle: _isOptionShuffle,
),
),
);
_isDoProcess = false;
}
}
・・・
↑各ボタンの処理
エレキベア
setStateで状態を変更するために、
StatefullWidget内で処理を記述する必要があるクマね
StatefullWidget内で処理を記述する必要があるクマね

マイケル
アクションも分離する方法はあるだろうけど、この量ならこれでも充分そうだね
英単語データの取得処理についてはこの後解説します!
英単語データの取得処理についてはこの後解説します!
Gooogleスプレッドシートからのデータ取得

マイケル
Googleスプレッドシートからデータを取得する処理に関しては下記のように実装しています。
一般的なAPI取得の処理で、指定URLからJSONレスポンスを取得する実装となっています。
一般的なAPI取得の処理で、指定URLからJSONレスポンスを取得する実装となっています。
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../settings/googleapi_settings.dart';
/// 英単語データ
class EnglishWordData {
EnglishWordData({required this.englishWord, required this.japaneseWord});
final String englishWord; // 英単語
final String japaneseWord; // 日本語
}
/// 英単語データリポジトリ
class EnglishWordDataRepository {
/// APIからのデータ取得
static Future<List<EnglishWordData>> getEnglishWordDataListFromApi() async {
final List<EnglishWordData> result = [];
try {
// レスポンス取得
final url = GoogleApiSettings.createGoogleSheetsApiGetUrl();
final res = await http.get(Uri.parse(url));
if (res.statusCode != 200) {
throw ('Failed to Load English Word Data.');
}
// 中身のチェック
final Map<String, dynamic> message = json.decode(res.body);
if (message['values'] == null) {
throw ('Please Set English Word Data.');
}
// 英単語データとして変換
final List<dynamic> values = message['values'];
values.forEach((value) => {
result.add(
EnglishWordData(englishWord: value[0], japaneseWord: value[1]))
});
} catch (e) {
print(e); // エラーはログに出力して握りつぶす
}
return result;
}
}
↑英単語データの取得処理
マイケル
リクエストにはhttpパッケージが必要なため、pubspac.yamlに指定してflutter pub getしておく必要があります。
dependencies:
flutter:
sdk: flutter
http: ^0.13.5
↑httpパッケージを追加
エレキベア
API実行は定番クマね

マイケル
そして肝心のデータ取得のURLについてですが、今回はGoogle Sheets APIの
https://sheets.googleapis.com/v4/spreadsheets/${spreadsSheetsUrl}/values/${sheetName}?key=${apiKey}
の形式でリクエストしています。
settings/googleapi_settings.dartに各値からURLを生成するようにしてあるので、もし動かす場合はこちらに各自のスプレッドシートID、シート名、APIキーを設定しましょう。
https://sheets.googleapis.com/v4/spreadsheets/${spreadsSheetsUrl}/values/${sheetName}?key=${apiKey}
の形式でリクエストしています。
settings/googleapi_settings.dartに各値からURLを生成するようにしてあるので、もし動かす場合はこちらに各自のスプレッドシートID、シート名、APIキーを設定しましょう。
class GoogleApiSettings {
// この辺は各自設定してください
static const String spreadsSheetsUrl = "";
static const String sheetName = "";
static const String apiKey = "";
/// GoogleSheetsAPI(v4.spreadsheets.values - get)のURL生成
/// 詳細: https://developers.google.com/sheets/api/reference/rest
static String createGoogleSheetsApiGetUrl() {
if (spreadsSheetsUrl.isEmpty || sheetName.isEmpty || apiKey.isEmpty) {
throw ('please set google api settings.');
}
return 'https://sheets.googleapis.com/v4/spreadsheets/${spreadsSheetsUrl}/values/${sheetName}?key=${apiKey}';
}
}
↑Google Sheets API のURL生成
エレキベア
この辺は各々設定する必要があるクマね

マイケル
それぞれの取得方法について簡単に書いておくと、
・スプレッドシートID
→スプレッドシートを公開した際のURLから取得
・シート名
→スプレッドシートで読み込むシート名
・APIキー
→Google Cloudでプロジェクトを生成してGoogle Sheets APIを有効化して認証情報から生成
からそれぞれ取得することができます。
・スプレッドシートID
→スプレッドシートを公開した際のURLから取得
・シート名
→スプレッドシートで読み込むシート名
・APIキー
→Google Cloudでプロジェクトを生成してGoogle Sheets APIを有効化して認証情報から生成
からそれぞれ取得することができます。





エレキベア
これならサクッと作れそうクマね
単語選択画面

マイケル
そして次は単語選択画面です!
こちらは画面内で、
・選択前
・選択後
・結果表示
の3つの表示状態に切り替わる内容になっています。
こちらは画面内で、
・選択前
・選択後
・結果表示
の3つの表示状態に切り替わる内容になっています。




エレキベア
状態を管理して表示を分ける必要があるクマね
単語選択画面全体の実装

マイケル
大枠のWidget表示については下記のようになっています。
Enum型でQuestionDisplayStateとして状態を管理し、その状態を監視して表示を切り替えています。
Enum型でQuestionDisplayStateとして状態を管理し、その状態を監視して表示を切り替えています。
import 'package:flutter/material.dart';
import '../components/layout_widgets.dart';
import '../data/english_word_data.dart';
/// 問題の表示状態
enum QuestionDisplayState {
none,
ok,
ng,
result,
}
/// 単語選択画面
class SelectWordPage extends StatefulWidget {
final List<EnglishWordData> englishWordDataList;
final bool isOptionShuffle;
const SelectWordPage(
{super.key,
required this.englishWordDataList,
required this.isOptionShuffle});
@override
State<SelectWordPage> createState() => _SelectWordPageState();
}
class _SelectWordPageState extends State<SelectWordPage> {
// 英単語データリスト
List<EnglishWordData> _englishWordDataList = [];
// 問題の表示状態
QuestionDisplayState _questionDisplayState = QuestionDisplayState.none;
// 問題データ
EnglishWordData? _questionWordData;
int _questionIndex = 0;
int _okAnswerCount = 0;
// 選択単語リスト
List<String> _selectWordList = [];
// 選択単語数
static const int selectWordCount = 4;
・・・
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(''),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: displaySelectWordPageWidgetList(),
),
),
);
}
List<Widget> displaySelectWordPageWidgetList() {
// 結果表示
if (_questionDisplayState == QuestionDisplayState.result) {
return [
SelectWordResultAreaWidget(
totalIndex: _englishWordDataList.length,
correctCount: _okAnswerCount,
),
];
}
// 問題表示
return [
HalfScreenArea(
child: SelectWordQuestionAreaWidget(
question: _questionWordData?.englishWord ?? 'empty',
answer: _questionWordData?.japaneseWord ?? 'empty',
index: _questionIndex + 1,
totalIndex: _englishWordDataList.length,
questionDisplayState: _questionDisplayState,
),
),
HalfScreenArea(
child: SelectWordButtonsAreaWidget(
selectWordList: _selectWordList,
onPressedSelectWordButton: onPressedSelectWordButton,
isShowNext: _questionDisplayState == QuestionDisplayState.ok ||
_questionDisplayState == QuestionDisplayState.ng,
onPressedNextButton: onPressedNextButton,
),
),
];
}
・・・
}
・・・
↑QuestionDisplayStateの状態によって表示するWidgetを切り替える
エレキベア
それぞれのWidgetについてはまた分割しているクマね
選択前の初期表示

マイケル
まずは選択前の初期表示から見ていきます。


マイケル
こちらはinitState内でタイトル画面から受け取った英単語データから問題と選択リストを生成しています。
@override
void initState() {
super.initState();
// 遷移元からデータを受け取る
_englishWordDataList = widget.englishWordDataList;
// オプション指定されていたらシャッフルする
if (widget.isOptionShuffle) {
_englishWordDataList.shuffle();
}
// 最初の問題を生成
_questionIndex = 0;
_okAnswerCount = 0;
createQuestion(_questionIndex);
}
/// 問題の生成
void createQuestion(int index) {
_questionDisplayState = QuestionDisplayState.none;
_questionWordData = _englishWordDataList[index];
_selectWordList =
createRandomSelectWordList(_questionWordData?.japaneseWord ?? 'empty');
}
/// 単語選択リスト生成
List<String> createRandomSelectWordList(String answer) {
// 単語データをコピー
var copyEnglishWordDataList = List.of(_englishWordDataList);
copyEnglishWordDataList.shuffle();
// 選択する単語リストを生成
List<String> selectWordList = [];
selectWordList.add(answer);
for (var i = 0; i < copyEnglishWordDataList.length; i++) {
var japaneseWord = copyEnglishWordDataList[i].japaneseWord;
// 答えとなる日本語は除く
if (answer == japaneseWord) {
continue;
}
// 指定数設定したら抜ける
selectWordList.add(japaneseWord);
if (selectWordList.length >= selectWordCount) {
break;
}
}
selectWordList.shuffle();
return selectWordList;
}
↑問題と選択リストの生成
マイケル
選択リストに関しては、答えとなる日本語に加えてランダムで選択した日本語を加えて4つ分生成するようにしてあります。

エレキベア
選択できる日本語も設定したデータから適当に選んでいるクマね

マイケル
問題の表示部分に関してもタイトル画面と同様、画面半分ずつに区切って表示しています。
上半分を問題文エリア、下半分を単語選択ボタンエリアとして定義しました。
問題文に関しては下記のように受け取ったパラメータの表示に加えて、stateも受け取ることで答えも表示できるよう対応しています。
上半分を問題文エリア、下半分を単語選択ボタンエリアとして定義しました。
問題文に関しては下記のように受け取ったパラメータの表示に加えて、stateも受け取ることで答えも表示できるよう対応しています。
/// 問題文エリア
class SelectWordQuestionAreaWidget extends StatelessWidget {
final String question;
final String answer;
final int index;
final int totalIndex;
final QuestionDisplayState questionDisplayState;
const SelectWordQuestionAreaWidget(
{super.key,
required this.question,
required this.answer,
required this.index,
required this.totalIndex,
required this.questionDisplayState});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
// 問題数
Padding(
padding: const EdgeInsets.all(24),
child: Text(
'$index / $totalIndex',
style: const TextStyle(
fontSize: 20,
),
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 16),
// 問題文
Text(
question,
style: const TextStyle(
fontSize: 28,
),
),
const SizedBox(height: 8),
// 結果表示
getAnsewerWordTextWidget(answer, questionDisplayState),
const SizedBox(height: 32),
getAnsewerResultTextWidget(questionDisplayState),
],
),
),
],
);
}
Widget getAnsewerWordTextWidget(String answer, QuestionDisplayState state) {
var color = Colors.white;
if (state == QuestionDisplayState.none) {
color = color.withOpacity(0.0);
}
return Text(
answer,
style: TextStyle(
fontSize: 18,
color: color,
),
);
}
Widget getAnsewerResultTextWidget(QuestionDisplayState state) {
var message = "";
var color = Colors.black;
switch (state) {
case QuestionDisplayState.ok:
message = "○";
color = Colors.red;
break;
case QuestionDisplayState.ng:
message = "×";
color = Colors.blue;
break;
case QuestionDisplayState.none:
case QuestionDisplayState.result:
break;
}
return SizedBox(
height: 42, // 文字の内容に限らず高さを固定
child: Text(
message,
style: TextStyle(
fontSize: 32,
color: color,
),
),
);
}
}
↑状態に応じて答えの表示可否を切り替える

マイケル
単語選択ボタン群に関しても、状態からボタンの活性可否やNEXTボタンの表示可否を切り替えるようにしています。
選択時には選んだ日本語をパラメータとして押下処理に渡すようにしています。
選択時には選んだ日本語をパラメータとして押下処理に渡すようにしています。
/// 単語選択ボタンエリア
class SelectWordButtonsAreaWidget extends StatelessWidget {
final List<String> selectWordList;
final Function onPressedSelectWordButton;
final bool isShowNext;
final Function onPressedNextButton;
const SelectWordButtonsAreaWidget(
{super.key,
required this.selectWordList,
required this.onPressedSelectWordButton,
required this.isShowNext,
required this.onPressedNextButton});
@override
Widget build(BuildContext context) {
return Column(children: [
// 単語選択ボタン群
Expanded(
flex: 5,
child: GridView.count(
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: 20,
crossAxisSpacing: 20,
padding: const EdgeInsets.all(50.0),
childAspectRatio: 2.5,
crossAxisCount: 2,
scrollDirection: Axis.vertical,
children: selectWordList
.map((selectWord) => SelectWordButtonWidget(
text: selectWord,
onPressed: isShowNext ? null : onPressedSelectWordButton,
))
.toList(),
),
),
// NEXTボタン
if (isShowNext)
Expanded(
flex: 3,
child: Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(right: 48),
child: TextButton(
onPressed: () => onPressedNextButton(),
child: Text(
'NEXT ▶︎',
style: TextStyle(
fontSize: 18,
color: Theme.of(context).primaryColor,
),
),
),
),
),
),
]);
}
}
/// 単語選択ボタン
class SelectWordButtonWidget extends StatelessWidget {
final String text;
final Function? onPressed;
const SelectWordButtonWidget(
{super.key, required this.text, required this.onPressed});
@override
Widget build(BuildContext context) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
textStyle: const TextStyle(fontSize: 14),
foregroundColor: Theme.of(context).backgroundColor,
backgroundColor: Theme.of(context).primaryColor,
),
onPressed: onPressed == null ? null : () => onPressed!(text),
child: Text(text),
);
}
}
↑単語選択ボタンの表示
エレキベア
StatelessWidgetでも親から状態を受け取ることで表示を切り替えることができるクマね
選択処理

マイケル
選択処理に関しては、選んだ日本語が合っているかどうかの答え合わせをしています。
NEXTボタンを押下した際には次の問題へ進み、最後の問題の場合には最終的な結果を表示します。
NEXTボタンを押下した際には次の問題へ進み、最後の問題の場合には最終的な結果を表示します。
class _SelectWordPageState extends State<SelectWordPage> {
・・・
/// 単語ボタン押下処理
void onPressedSelectWordButton(String selectWord) {
// 答え合わせ
setState(() {
var isCorrect = selectWord == _questionWordData?.japaneseWord;
if (isCorrect) _okAnswerCount++;
_questionDisplayState =
isCorrect ? QuestionDisplayState.ok : QuestionDisplayState.ng;
});
}
/// NEXTボタン押下処理
void onPressedNextButton() {
setState(() {
// 最後まで問題を出したら結果表示
_questionIndex++;
if (_englishWordDataList.length <= _questionIndex) {
_questionDisplayState = QuestionDisplayState.result;
return;
}
// 次の問題を表示
createQuestion(_questionIndex);
});
}
}
・・・

エレキベア
この辺はシンプルクマね
結果表示

マイケル
そして最終的な結果表示に関しては、カウントした正解数を表示させているだけになります。

/// 結果表示
class SelectWordResultAreaWidget extends StatelessWidget {
final int totalIndex;
final int correctCount;
const SelectWordResultAreaWidget(
{super.key, required this.totalIndex, required this.correctCount});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: [
Text(
'正解数: $correctCount / $totalIndex',
style: const TextStyle(
fontSize: 24,
),
),
const SizedBox(height: 40),
SizedBox(
width: 120,
height: 60,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
textStyle: const TextStyle(fontSize: 20),
foregroundColor: Theme.of(context).backgroundColor,
backgroundColor: Theme.of(context).primaryColor,
),
onPressed: () {
Navigator.pop(context);
},
child: const Text('BACK'),
),
),
],
),
);
}
}

エレキベア
この辺は説明不要クマね

マイケル
アプリ全体の解説は以上になります!
流れについては大体分かったのではないでしょうか!
流れについては大体分かったのではないでしょうか!
おわりに

マイケル
というわけで今回はFlutterで簡単なアプリを作ってみました!
どうだったかな??
どうだったかな??

エレキベア
何も考えなくてもある程度UIが綺麗になるのと、
これだけでアプリが作れるのは素晴らしいと思ったクマ〜〜
これだけでアプリが作れるのは素晴らしいと思ったクマ〜〜

マイケル
Flutter感出まくりだけど、パッと作れるから本当楽だね!
ただUI部分のコードが作り上どうしてもインデントが深くなってしまうから、整理してなるべく可読性はよくしておきたいなと思ったよ
ただUI部分のコードが作り上どうしてもインデントが深くなってしまうから、整理してなるべく可読性はよくしておきたいなと思ったよ

エレキベア
この規模ならいいクマが、
大きなアプリを作る場合はアーキテクチャもちゃんと調べた方がよさそうクマね
大きなアプリを作る場合はアーキテクチャもちゃんと調べた方がよさそうクマね

マイケル
楽しかったからまた触ってみよう!
それでは今日はこの辺で!!
アデューー!!
それでは今日はこの辺で!!
アデューー!!

エレキベア
クマ〜〜〜〜
【Flutter3】Googleスプレッドシートと連携した英単語学習アプリを作る 〜完〜
コメント