Flutterを使って、バスケットボールのスコア入力アプリ(ランニングスコア)を作成する方法について紹介します。このアプリでは、各クォーターごとの得点を入力し、リストで管理することができます。また、スコアの追加、削除、リストの並べ替えなどの機能も備えています。
環境設定
まず、Flutterの開発環境をセットアップする必要があります。詳細な手順については、Flutter公式サイトのインストールガイドや当ブログの「MacにFlutter開発環境を構築する方法」参照してください。
プロジェクトの作成
ターミナルまたはコマンドプロンプトで以下のコマンドを実行して、新しいFlutterプロジェクトを作成します。
flutter create basketball_score_app
プロジェクトのディレクトリに移動します。
cd basketball_score_app
必要なパッケージの追加
プロジェクトのpubspec.yaml
ファイルを開き、intl
パッケージを追加します。このパッケージは、日時のフォーマットに使用します。具体的には、スコアが入力された時間をフォーマットして表示するために使用します。
dependencies:
flutter:
sdk: flutter
intl: ^0.17.0
その後、以下のコマンドを実行してパッケージをインストールします。
flutter pub get
コードの実装
以下は、lib/main.dart
ファイルの完全なコードです。各部分について詳しく説明します。
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
void main() {
runApp(BasketballScoreApp());
}
// アプリケーションのエントリーポイント
class BasketballScoreApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'バスケットボール スコア入力アプリ',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ScoreHomePage(),
);
}
}
// プレイ内容エントリのデータモデル
class PlayEntry {
final String team; // チーム名
final int points; // 得点
final int quarter; // クォーター
final String playType; // プレイ内容タイプ
final String time; // 時間
final String playerNumber; // 選手番号
final Key key; // ユニークキー
PlayEntry(this.team, this.points, this.quarter, this.playType, this.time,
this.playerNumber)
: key = UniqueKey(); // ユニークキーを初期化
}
// スコアホームページのステートフルウィジェット
class ScoreHomePage extends StatefulWidget {
@override
_ScoreHomePageState createState() => _ScoreHomePageState();
}
class _ScoreHomePageState extends State<ScoreHomePage> {
List<PlayEntry> _plays = []; // プレイ内容エントリのリスト
String? _selectedTeam; // 選択されたチーム
int _selectedQuarter = 1; // 選択されたクォーター
String? _selectedPlayer; // 選択された選手番号
// プレイ内容を追加するメソッド
void _addPlay(String team, int points, String playType) {
String formattedTime = DateFormat('H:mm:ss').format(DateTime.now());
setState(() {
_plays.insert(
0,
PlayEntry(team, points, _selectedQuarter, playType, formattedTime,
_selectedPlayer ?? 'TEAM'));
_selectedPlayer = null; // スコア追加後に選手選択をリセット
_selectedTeam = null; // スコア追加後にチーム選択をリセット
});
}
// チームの総得点を取得するメソッド
int _getTotalScore(String team) {
return _plays
.where((entry) => entry.team == team)
.fold(0, (sum, item) => sum + item.points);
}
// クォーター切り替えのダイアログを表示するメソッド
void _showQuarterDialog(int quarter) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("クォーター切り替え"),
content: Text("本当にクォーターを $quarter に切り替えますか?"),
actions: [
TextButton(
onPressed: () {
setState(() {
_selectedQuarter = quarter;
});
Navigator.of(context).pop();
},
child: Text("はい"),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text("いいえ"),
),
],
);
},
);
}
// プレイ内容の削除確認ダイアログを表示するメソッド
void _showDeleteConfirmationDialog(PlayEntry entry) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("プレイ削除"),
content: Text("本当にこのプレイ内容を削除しますか?"),
actions: [
TextButton(
onPressed: () {
setState(() {
_plays.remove(entry);
});
Navigator.of(context).pop();
},
child: Text("はい"),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text("いいえ"),
),
],
);
},
);
}
// 選手番号を設定するメソッド
void _setPlayer(String player) {
setState(() {
_selectedPlayer = player;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('バスケットボール スコア入力アプリ'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'チームA',
style: TextStyle(fontSize: 24),
),
SizedBox(width: 10),
Text(
'${_getTotalScore('A')}',
style: TextStyle(fontSize: 24),
),
SizedBox(width: 10),
Text(
'-',
style: TextStyle(fontSize: 24),
),
SizedBox(width: 10),
Text(
'${_getTotalScore('B')}',
style: TextStyle(fontSize: 24),
),
SizedBox(width: 10),
Text(
'チームB',
style: TextStyle(fontSize: 24),
),
],
),
),
SizedBox(height: 20),
Text(
'クォーターを選択してください:',
style: TextStyle(fontSize: 20),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => _showQuarterDialog(1),
child: Text('1Q'),
style: ElevatedButton.styleFrom(
backgroundColor:
_selectedQuarter == 1 ? Colors.blue : Colors.grey,
),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () => _showQuarterDialog(2),
child: Text('2Q'),
style: ElevatedButton.styleFrom(
backgroundColor:
_selectedQuarter == 2 ? Colors.blue : Colors.grey,
),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () => _showQuarterDialog(3),
child: Text('3Q'),
style: ElevatedButton.styleFrom(
backgroundColor:
_selectedQuarter == 3 ? Colors.blue : Colors.grey,
),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () => _showQuarterDialog(4),
child: Text('4Q'),
style: ElevatedButton.styleFrom(
backgroundColor:
_selectedQuarter == 4 ? Colors.blue : Colors.grey,
),
),
],
),
SizedBox(height: 20),
Text(
'チームを選択してください:',
style: TextStyle(fontSize: 20),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => setState(() => _selectedTeam = 'A'),
child: Text('チームA'),
style: ElevatedButton.styleFrom(
backgroundColor:
_selectedTeam == 'A' ? Colors.blue : Colors.grey,
),
),
SizedBox(width: 20),
ElevatedButton(
onPressed: () => setState(() => _selectedTeam = 'B'),
child: Text('チ
ームB'),
style: ElevatedButton.styleFrom(
backgroundColor:
_selectedTeam == 'B' ? Colors.blue : Colors.grey,
),
),
],
),
SizedBox(height: 20),
Text(
'選手の番号を選択してください:',
style: TextStyle(fontSize: 20),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List<Widget>.generate(
12,
(int index) {
int playerNumber = index + 4;
return ElevatedButton(
onPressed: () => _setPlayer(playerNumber.toString()),
child: Text(playerNumber.toString()),
style: ElevatedButton.styleFrom(
backgroundColor:
_selectedPlayer == playerNumber.toString()
? Colors.blue
: Colors.grey,
),
);
},
)..add(
ElevatedButton(
onPressed: () => _setPlayer('TEAM'),
child: Text('TEAM'),
style: ElevatedButton.styleFrom(
backgroundColor:
_selectedPlayer == 'TEAM' ? Colors.blue : Colors.grey,
),
),
),
),
),
SizedBox(height: 20),
Text(
'内容を選択してください:',
style: TextStyle(fontSize: 20),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _selectedTeam == null || _selectedPlayer == null
? null
: () => _addPlay(_selectedTeam!, 1, 'FT'),
child: Text('FT'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: _selectedTeam == null || _selectedPlayer == null
? null
: () => _addPlay(_selectedTeam!, 2, '2P'),
child: Text('2P'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: _selectedTeam == null || _selectedPlayer == null
? null
: () => _addPlay(_selectedTeam!, 3, '3P'),
child: Text('3P'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: _selectedTeam == null || _selectedPlayer == null
? null
: () => _addPlay(_selectedTeam!, 0, 'ファウル'),
child: Text('ファウル'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: _selectedPlayer != 'TEAM'
? null
: () => _addPlay(_selectedTeam!, 0, 'タイムアウト'),
child: Text('タイムアウト'),
),
],
),
SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('クォーター', style: TextStyle(fontWeight: FontWeight.bold)),
Text('チーム', style: TextStyle(fontWeight: FontWeight.bold)),
Text('選手', style: TextStyle(fontWeight: FontWeight.bold)),
Text('内容', style: TextStyle(fontWeight: FontWeight.bold)),
Text('時間', style: TextStyle(fontWeight: FontWeight.bold)),
Text('操作', style: TextStyle(fontWeight: FontWeight.bold)),
],
),
),
Expanded(
child: ReorderableListView(
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final item = _plays.removeAt(oldIndex);
_plays.insert(newIndex, item);
});
},
children: _plays
.where((entry) => entry.quarter == _selectedQuarter)
.map((entry) => Container(
key: entry.key,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.grey),
),
),
child: ListTile(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('${entry.quarter}Q'),
Text('チーム${entry.team}'),
Text('${entry.playerNumber}'),
Text('${entry.playType}'),
Text('${entry.time}'),
],
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () =>
_showDeleteConfirmationDialog(entry),
),
contentPadding:
EdgeInsets.symmetric(vertical: 0, horizontal: 16),
dense: true,
),
))
.toList(),
),
),
],
),
);
}
}
コードの説明
- アプリケーションのエントリーポイント:
BasketballScoreApp
クラスはアプリケーションのルートウィジェットです。MaterialApp
ウィジェットを使用して、アプリのテーマとホームページを設定します。
- プレイ内容エントリのデータモデル:
PlayEntry
クラスはプレイ内容エントリのデータモデルを定義します。- 各プレイ内容エントリは、チーム名、得点、クォーター、プレイ内容タイプ、時間、選手番号、およびユニークキーを持ちます。
- スコアホームページのステートフルウィジェット:
ScoreHomePage
クラスは、アプリのメイン画面を提供します。_ScoreHomePageState
クラスは、状態を管理します。
- メソッドの実装:
_addPlay
メソッドは、新しいプレイ内容を追加し、選手選択とチーム選択をリセットします。_getTotalScore
メソッドは、特定のチームの総得点を計算します。_showQuarterDialog
メソッドは、クォーターを切り替えるための確認ダイアログを表示します。_showDeleteConfirmationDialog
メソッドは、プレイ内容を削除するための確認ダイアログを表示します。_setPlayer
メソッドは、選手番号を設定します。
- ウィジェットの構成:
Scaffold
ウィジェットを使用して、アプリの基本的なレイアウトを提供します。Column
ウィジェットを使用して、各セクション(スコア表示、クォーター選択、チーム選択、選手番号選択、内容選択、リスト表示)を垂直に並べます。Row
ウィジェットを使用して、各セクション内のアイテムを水平に配置します。ElevatedButton
ウィジェットを使用して、ボタンを作成します。ReorderableListView
ウィジェットを使用して、リストの並べ替えを可能にします。
画面イメージ
これで、バスケットボールのスコア入力アプリ(ランニングスコア)の基本的な構成が完成しました。ここまでの時点で先月試してみた Python x Kivy で作ったアプリより自由度が高い感覚ですので、今後は Flutterでの開発を進めて不足している機能追加など続けていく予定です。
コメント