Flutterでバスケットボールスコア入力アプリを作成

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(),
            ),
          ),
        ],
      ),
    );
  }
}

コードの説明

  1. アプリケーションのエントリーポイント
    • BasketballScoreApp クラスはアプリケーションのルートウィジェットです。
    • MaterialApp ウィジェットを使用して、アプリのテーマとホームページを設定します。
  2. プレイ内容エントリのデータモデル
    • PlayEntry クラスはプレイ内容エントリのデータモデルを定義します。
    • 各プレイ内容エントリは、チーム名、得点、クォーター、プレイ内容タイプ、時間、選手番号、およびユニークキーを持ちます。
  3. スコアホームページのステートフルウィジェット
    • ScoreHomePage クラスは、アプリのメイン画面を提供します。
    • _ScoreHomePageState クラスは、状態を管理します。
  4. メソッドの実装
    • _addPlay メソッドは、新しいプレイ内容を追加し、選手選択とチーム選択をリセットします。
    • _getTotalScore メソッドは、特定のチームの総得点を計算します。
    • _showQuarterDialog メソッドは、クォーターを切り替えるための確認ダイアログを表示します。
    • _showDeleteConfirmationDialog メソッドは、プレイ内容を削除するための確認ダイアログを表示します。
    • _setPlayer メソッドは、選手番号を設定します。
  5. ウィジェットの構成
    • Scaffold ウィジェットを使用して、アプリの基本的なレイアウトを提供します。
    • Column ウィジェットを使用して、各セクション(スコア表示、クォーター選択、チーム選択、選手番号選択、内容選択、リスト表示)を垂直に並べます。
    • Row ウィジェットを使用して、各セクション内のアイテムを水平に配置します。
    • ElevatedButton ウィジェットを使用して、ボタンを作成します。
    • ReorderableListView ウィジェットを使用して、リストの並べ替えを可能にします。

画面イメージ

これで、バスケットボールのスコア入力アプリ(ランニングスコア)の基本的な構成が完成しました。ここまでの時点で先月試してみた Python x Kivy で作ったアプリより自由度が高い感覚ですので、今後は Flutterでの開発を進めて不足している機能追加など続けていく予定です。

関連記事

コメント

タイトルとURLをコピーしました