今回のアップデートでは、バスケットボールスコア入力アプリにいくつかの新機能を追加しました。これにより、ユーザーは試合データの管理がより簡単になり、試合情報を柔軟に編集できるようになりました。以下に新機能の詳細と、対応するコードを紹介します。
新機能の概要
- 試合データの削除機能: 保存された試合データを削除する機能を追加しました。
- 試合会場の項目追加: 試合情報に会場名を追加しました。
- 試合名と会場名の編集機能: 保存された試合データの試合名と会場名を編集できるようにしました。
試合データの削除機能
試合管理画面で、不要な試合データを簡単に削除できる機能を追加しました。削除アイコンをクリックすると、削除確認ダイアログが表示され、ユーザーが確認した場合にのみデータが削除されます。
// manage_games_screen.dartの一部
void _showDeleteConfirmationDialog(GameEntry game) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("試合削除"),
content: Text("本当にこの試合を削除しますか?"),
actions: [
TextButton(
onPressed: () async {
await _gameProvider.deleteGame(game.id!);
_loadGames();
Navigator.of(context).pop();
},
child: Text("はい"),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text("いいえ"),
),
],
);
},
);
}
試合会場の項目追加
新たに試合会場の情報を入力・編集できる項目を追加しました。試合名と同様に、会場名もアプリに保存されるようになりました。
// main.dartの一部
Future<void> _showGameNameDialog() async {
String? gameName = await showDialog<String>(
context: context,
builder: (BuildContext context) {
String name = '';
String venue = '';
return AlertDialog(
title: Text('試合情報を入力'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
decoration: InputDecoration(hintText: '試合名'),
onChanged: (value) {
name = value;
},
),
TextField(
decoration: InputDecoration(hintText: '会場名'),
onChanged: (value) {
venue = value;
},
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(null);
},
child: Text('キャンセル'),
),
TextButton(
onPressed: () {
if (name.isNotEmpty) {
_currentGameName = name;
_currentVenue = venue;
Navigator.of(context).pop(name);
}
},
child: Text('開始'),
),
],
);
},
);
if (gameName != null && gameName.isNotEmpty) {
_saveNewGame(gameName);
}
}
試合データの試合名・会場名の編集機能
試合管理画面で、保存済みの試合データの試合名や会場名を編集できるようになりました。編集ボタンを押すと、編集ダイアログが表示され、入力内容を保存できます。
// manage_games_screen.dartの一部
void _showEditGameDialog(GameEntry game) async {
TextEditingController nameController =
TextEditingController(text: game.name);
TextEditingController venueController =
TextEditingController(text: game.venue);
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("試合情報を編集"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: InputDecoration(hintText: '試合名'),
),
TextField(
controller: venueController,
decoration: InputDecoration(hintText: '会場名'),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('キャンセル'),
),
TextButton(
onPressed: () async {
GameEntry updatedGame = GameEntry(
id: game.id,
name: nameController.text,
venue: venueController.text,
date: game.date);
await _gameProvider.updateGame(updatedGame);
_loadGames();
Navigator.of(context).pop();
},
child: Text('保存'),
),
],
);
},
);
}
完全版のコード
database_helper.dart
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
static Database? _database;
DatabaseHelper._privateConstructor();
Future<Database> get database async => _database ??= await _initDatabase();
Future<Database> _initDatabase() async {
String path = join(await getDatabasesPath(), 'basketball_score.db');
return await openDatabase(
path,
version: 1,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
Future _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE games (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
venue TEXT,
date TEXT
)
''');
await db.execute('''
CREATE TABLE plays (
id INTEGER PRIMARY KEY AUTOINCREMENT,
team TEXT,
points INTEGER,
quarter INTEGER,
playType TEXT,
time TEXT,
playerNumber TEXT,
gameId INTEGER
)
''');
}
Future _onUpgrade(Database db, int oldVersion, int newVersion) async {
// Perform any database upgrade tasks here
}
}
game_entry_provider.dart
import 'package:sqflite/sqflite.dart';
import 'database_helper.dart';
class GameEntry {
final int? id;
final String name;
final String venue;
final String date;
GameEntry({this.id, required this.name, required this.venue, required this.date});
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'venue': venue,
'date': date,
};
}
factory GameEntry.fromMap(Map<String, dynamic> map) {
return GameEntry(
id: map['id'],
name: map['name'],
venue: map['venue'],
date: map['date'],
);
}
}
class GameEntryProvider {
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
Future<int> insertGame(GameEntry game) async {
Database db = await _dbHelper.database;
return await db.insert('games', game.toMap(), conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<List<GameEntry>> getAllGames() async {
Database db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query('games');
return List.generate(maps.length, (i) {
return GameEntry.fromMap(maps[i]);
});
}
Future<int> updateGame(GameEntry game) async {
Database db = await _dbHelper.database;
return await db.update(
'games',
game.toMap(),
where: 'id = ?',
whereArgs: [game.id],
);
}
Future<int> deleteGame(int id) async {
Database db = await _dbHelper.database;
return await db.delete(
'games',
where: 'id = ?',
whereArgs: [id],
);
}
}
main.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'database_helper.dart';
import 'game_entry_provider.dart';
import 'play_entry_provider.dart';
import 'manage_games_screen.dart';
void main() {
runApp(BasketballScoreApp());
}
class BasketballScoreApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'バスケットボール スコア入力アプリ',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ScoreHomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class ScoreHomePage extends StatefulWidget {
@override
_ScoreHomePageState createState() => _ScoreHomePageState();
}
class _ScoreHomePageState extends State<ScoreHomePage>
with SingleTickerProviderStateMixin {
final PlayEntryProvider _playProvider = PlayEntryProvider();
final GameEntryProvider _gameProvider = GameEntryProvider();
List<PlayEntry> _plays = [];
String? _selectedTeam;
int _selectedQuarter = 1;
String? _selectedPlayer;
int? _currentGameId;
String _currentGameName = '';
String _currentVenue = '';
late AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
WidgetsBinding.instance.addPostFrameCallback((_) {
_showGameNameDialog();
});
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
Future<void> _showGameNameDialog() async {
String? gameName = await showDialog<String>(
context: context,
builder: (BuildContext context) {
String name = '';
String venue = '';
return AlertDialog(
title: Text('試合情報を入力'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
decoration: InputDecoration(hintText: '試合名'),
onChanged: (value) {
name = value;
},
),
TextField(
decoration: InputDecoration(hintText: '会場名'),
onChanged: (value) {
venue = value;
},
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(null);
},
child: Text('キャンセル'),
),
TextButton(
onPressed: () {
if (name.isNotEmpty) {
_currentGameName = name;
_currentVenue = venue;
Navigator.of(context).pop(name);
}
},
child: Text('開始'),
),
],
);
},
);
if (gameName != null && gameName.isNotEmpty) {
_saveNewGame(gameName);
}
}
Future<void> _saveNewGame(String gameName) async {
final newGame = GameEntry(
name: gameName, venue: _currentVenue, date: DateTime.now().toString());
_currentGameId = await _gameProvider.insertGame(newGame);
_plays = [];
_selectedTeam = null;
_selectedPlayer = null;
setState(() {});
}
Future<void> _addPlay(String team, int points, String playType) async {
if (_currentGameId == null) return;
String formattedTime = DateFormat('H:mm:ss').format(DateTime.now());
PlayEntry newPlay = PlayEntry(
team: team,
points: points,
quarter: _selectedQuarter,
playType: playType,
time: formattedTime,
playerNumber: _selectedPlayer ?? 'TEAM',
gameId: _currentGameId!,
);
await _playProvider.insertPlay(newPlay);
_loadPlaysForCurrentGame();
_selectedPlayer = null;
_selectedTeam = null;
}
Future<void> _loadPlaysForCurrentGame() async {
if (_currentGameId == null) return;
List<PlayEntry> plays =
await _playProvider.getPlaysByGameId(_currentGameId!);
plays.sort((a, b) {
int quarterComparison = b.quarter.compareTo(a.quarter);
if (quarterComparison == 0) {
return b.time.compareTo(a.time);
}
return quarterComparison;
});
setState(() {
_plays = plays;
});
}
Future<void> _showQuarterDialog(int quarter) async {
bool shouldSwitch = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("クォーター切り替え"),
content: Text("本当にクォーターを $quarter に切り替えますか?"),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text("はい"),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text("いいえ"),
),
],
);
},
) ??
false;
if (shouldSwitch) {
setState(() {
_selectedQuarter = quarter;
});
_loadPlaysForCurrentGame();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('バスケットボール スコア入力アプリ'),
actions: [
IconButton(
icon: Icon(Icons.list),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ManageGamesScreen(
onGameSelected: (game) {
setState(() {
_currentGameId = game.id;
_currentGameName = game.name;
_currentVenue = game.venue;
});
_loadPlaysForCurrentGame();
Navigator.pop(context); // メインスクリーンに戻る
},
currentGameId: _currentGameId, // 現在の試合IDを渡す
),
),
);
},
),
],
),
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),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Wrap(
spacing: 10,
children: List.generate(
5,
(int index) {
int playerNumber = index + 1;
return ElevatedButton(
onPressed: () => _setPlayer(playerNumber.toString()),
child: Text('$playerNumber'),
style: ElevatedButton.styleFrom(
backgroundColor:
_selectedPlayer == playerNumber.toString()
? Colors.blue
: Colors.grey,
),
);
},
).toList()
..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),
Text(
'プレイ内容一覧:',
style: TextStyle(fontSize: 20),
),
Container(
padding: EdgeInsets.symmetric(vertical: 4.0),
color: Colors.grey[300],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(child: Text('クォーター', textAlign: TextAlign.center)),
Expanded(child: Text('チーム名', textAlign: TextAlign.center)),
Expanded(child: Text('番号', textAlign: TextAlign.center)),
Expanded(child: Text('内容', textAlign: TextAlign.center)),
Expanded(child: Text('時間', textAlign: TextAlign.center)),
Expanded(child: Text('操作', textAlign: TextAlign.center)),
],
),
),
Expanded(
child: ReorderableListView.builder(
itemCount: _plays.length,
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final PlayEntry play = _plays.removeAt(oldIndex);
_plays.insert(newIndex, play);
});
},
itemBuilder: (context, index) {
final play = _plays[index];
return ListTile(
key: ValueKey(play.id),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text('${play.quarter}Q',
textAlign: TextAlign.center)),
Expanded(
child: Text('${play.team}',
textAlign: TextAlign.center)),
Expanded(
child: Text('#${play.playerNumber}',
textAlign: TextAlign.center)),
Expanded(
child: Text('${play.playType}',
textAlign: TextAlign.center)),
Expanded(
child: Text('${play.time}',
textAlign: TextAlign.center)),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.delete),
onPressed: () {
setState(() {
_plays.removeAt(index);
});
_playProvider.deletePlay(play.id!);
},
),
ReorderableDragStartListener(
index: index,
child: Icon(Icons.drag_handle),
),
],
),
),
],
),
);
},
),
),
],
),
);
}
int _getTotalScore(String team) {
return _plays
.where((play) => play.team == team)
.fold(0, (total, play) => total + play.points);
}
void _setPlayer(String playerNumber) {
setState(() {
_selectedPlayer = playerNumber;
});
}
}
manage_games_screen.dart
import 'package:flutter/material.dart';
import 'game_entry_provider.dart';
class ManageGamesScreen extends StatefulWidget {
final ValueChanged<GameEntry> onGameSelected;
final int? currentGameId;
ManageGamesScreen({required this.onGameSelected, this.currentGameId});
@override
_ManageGamesScreenState createState() => _ManageGamesScreenState();
}
class _ManageGamesScreenState extends State<ManageGamesScreen> {
List<GameEntry> _games = [];
final GameEntryProvider _gameProvider = GameEntryProvider();
@override
void initState() {
super.initState();
_loadGames();
}
Future<void> _loadGames() async {
List<GameEntry> games = await _gameProvider.getAllGames();
setState(() {
_games = games;
});
}
void _showEditGameDialog(GameEntry game) async {
TextEditingController nameController =
TextEditingController(text: game.name);
TextEditingController venueController =
TextEditingController(text: game.venue);
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("試合情報を編集"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: InputDecoration(hintText: '試合名'),
),
TextField(
controller: venueController,
decoration: InputDecoration(hintText: '会場名'),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('キャンセル'),
),
TextButton(
onPressed: () async {
GameEntry updatedGame = GameEntry(
id: game.id,
name: nameController.text,
venue: venueController.text,
date: game.date);
await _gameProvider.updateGame(updatedGame);
_loadGames();
Navigator.of(context).pop();
},
child: Text('保存'),
),
],
);
},
);
}
void _showDeleteConfirmationDialog(GameEntry game) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("試合削除"),
content: Text("本当にこの試合を削除しますか?"),
actions: [
TextButton(
onPressed: () async {
await _gameProvider.deleteGame(game.id!);
_loadGames();
Navigator.of(context).pop();
},
child: Text("はい"),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text("いいえ"),
),
],
);
},
);
}
void _showLoadConfirmationDialog(GameEntry game) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("試合読み込み"),
content: Text("本当にこの試合を読み込みますか?"),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(); // ダイアログを閉じる
},
child: Text("キャンセル"),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(); // ダイアログを閉じる
widget.onGameSelected(game); // 試合データを戻す
Navigator.of(context).pop(game); // メイン画面に戻る
},
child: Text("読み込む"),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('試合管理'),
),
body: _games.isEmpty
? Center(child: Text('保存された試合はありません'))
: ListView.builder(
itemCount: _games.length,
itemBuilder: (context, index) {
final game = _games[index];
return ListTile(
title: Text(game.name),
subtitle: Text(game.date),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.edit),
onPressed: () => _showEditGameDialog(game),
),
if (game.id != widget.currentGameId)
IconButton(
icon: Icon(Icons.delete),
onPressed: () => _showDeleteConfirmationDialog(game),
),
],
),
onTap: () => _showLoadConfirmationDialog(game),
);
},
),
);
}
}
play_entry_provider.dart
import 'package:sqflite/sqflite.dart';
import 'database_helper.dart';
class PlayEntry {
final int? id;
final String team;
final int points;
final int quarter;
final String playType;
final String time;
final String playerNumber;
final int gameId;
PlayEntry({
this.id,
required this.team,
required this.points,
required this.quarter,
required this.playType,
required this.time,
required this.playerNumber,
required this.gameId,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'team': team,
'points': points,
'quarter': quarter,
'playType': playType,
'time': time,
'playerNumber': playerNumber,
'gameId': gameId,
};
}
factory PlayEntry.fromMap(Map<String, dynamic> map) {
return PlayEntry(
id: map['id'],
team: map['team'],
points: map['points'],
quarter: map['quarter'],
playType: map['playType'],
time: map['time'],
playerNumber: map['playerNumber'],
gameId: map['gameId'],
);
}
}
class PlayEntryProvider {
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
Future<int> insertPlay(PlayEntry play) async {
Database db = await _dbHelper.database;
return await db.insert('plays', play.toMap(), conflictAlgorithm: ConflictAlgorithm.replace);
}
Future<List<PlayEntry>> getPlaysByGameId(int gameId) async {
Database db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
'plays',
where: 'gameId = ?',
whereArgs: [gameId],
);
return List.generate(maps.length, (i) {
return PlayEntry.fromMap(maps[i]);
});
}
Future<int> deletePlay(int id) async {
Database db = await _dbHelper.database;
return await db.delete(
'plays',
where: 'id = ?',
whereArgs: [id],
);
}
}
コメント