From f11d485ad394b0b6d81726aacb9edb2839751623 Mon Sep 17 00:00:00 2001 From: okaryo Date: Sat, 23 Sep 2023 14:52:39 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=E7=AC=AC5=E7=AB=A0=E3=81=AE=E5=88=9D?= =?UTF-8?q?=E6=9C=9F=E7=89=88=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + android/app/build.gradle | 4 + android/build.gradle | 3 + ios/Runner.xcodeproj/project.pbxproj | 4 + ios/firebase_app_id_file.json | 7 ++ lib/firebase_options.dart | 80 +++++++++++++ lib/main.dart | 9 +- lib/model/players.dart | 9 ++ lib/model/tic_tac_toe.dart | 64 ++++++++-- lib/provider/tic_tac_toe_provider.dart | 18 +-- lib/repository/tic_tac_toe_repository.dart | 26 ++++ lib/view/board.dart | 111 ++++++++++-------- macos/Flutter/GeneratedPluginRegistrant.swift | 4 + macos/Runner.xcodeproj/project.pbxproj | 6 +- macos/firebase_app_id_file.json | 7 ++ pubspec.lock | 79 ++++++++++++- pubspec.yaml | 2 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 19 files changed, 362 insertions(+), 79 deletions(-) create mode 100644 ios/firebase_app_id_file.json create mode 100644 lib/firebase_options.dart create mode 100644 lib/model/players.dart create mode 100644 lib/repository/tic_tac_toe_repository.dart create mode 100644 macos/firebase_app_id_file.json diff --git a/.gitignore b/.gitignore index 9782b64..7e5f0df 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,7 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# Firebase +**/google-services.json +**/GoogleService-Info.plist diff --git a/android/app/build.gradle b/android/app/build.gradle index d4edea2..f169119 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -22,6 +22,9 @@ if (flutterVersionName == null) { } apply plugin: 'com.android.application' +// START: FlutterFire Configuration +apply plugin: 'com.google.gms.google-services' +// END: FlutterFire Configuration apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" @@ -51,6 +54,7 @@ android { targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName + multiDexEnabled true } buildTypes { diff --git a/android/build.gradle b/android/build.gradle index 3cdaac9..2139d26 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,6 +7,9 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:7.1.2' + // START: FlutterFire Configuration + classpath 'com.google.gms:google-services:4.3.10' + // END: FlutterFire Configuration classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 8e91fc4..e2ac4b8 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 752703CAEFDAEFDAB76B9856 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3867D3A3D7E5FDA190F32018 /* GoogleService-Info.plist */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -31,6 +32,7 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3867D3A3D7E5FDA190F32018 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -72,6 +74,7 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 3867D3A3D7E5FDA190F32018 /* GoogleService-Info.plist */, ); sourceTree = ""; }; @@ -163,6 +166,7 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + 752703CAEFDAEFDAB76B9856 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/firebase_app_id_file.json b/ios/firebase_app_id_file.json new file mode 100644 index 0000000..86682a1 --- /dev/null +++ b/ios/firebase_app_id_file.json @@ -0,0 +1,7 @@ +{ + "file_generated_by": "FlutterFire CLI", + "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", + "GOOGLE_APP_ID": "1:807943088200:ios:125d7bfaa751582d21907d", + "FIREBASE_PROJECT_ID": "tic-tac-toe-handson", + "GCM_SENDER_ID": "807943088200" +} \ No newline at end of file diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..97abf92 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,80 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyAoY0U1veRmqWWMmFBULkerX6X5HYjMmIs', + appId: '1:807943088200:web:eda1735cfd79d15e21907d', + messagingSenderId: '807943088200', + projectId: 'tic-tac-toe-handson', + authDomain: 'tic-tac-toe-handson.firebaseapp.com', + storageBucket: 'tic-tac-toe-handson.appspot.com', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyCCWnRieFsedimIvgsjNevAdUSb0dm1faY', + appId: '1:807943088200:android:d989c8a1b020562321907d', + messagingSenderId: '807943088200', + projectId: 'tic-tac-toe-handson', + storageBucket: 'tic-tac-toe-handson.appspot.com', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyBZHELrAr4SRfXLYrxc4zF27P0R3sGsbi0', + appId: '1:807943088200:ios:125d7bfaa751582d21907d', + messagingSenderId: '807943088200', + projectId: 'tic-tac-toe-handson', + storageBucket: 'tic-tac-toe-handson.appspot.com', + iosBundleId: 'com.example.ticTacToeHandson', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyBZHELrAr4SRfXLYrxc4zF27P0R3sGsbi0', + appId: '1:807943088200:ios:125d7bfaa751582d21907d', + messagingSenderId: '807943088200', + projectId: 'tic-tac-toe-handson', + storageBucket: 'tic-tac-toe-handson.appspot.com', + iosBundleId: 'com.example.ticTacToeHandson', + ); +} diff --git a/lib/main.dart b/lib/main.dart index 48640a1..ceb80de 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,15 @@ +import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:tic_tac_toe_handson/firebase_options.dart'; import 'package:tic_tac_toe_handson/view/board.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + runApp( const ProviderScope( child: MyApp(), diff --git a/lib/model/players.dart b/lib/model/players.dart new file mode 100644 index 0000000..c4df92f --- /dev/null +++ b/lib/model/players.dart @@ -0,0 +1,9 @@ +class Players { + final String playerX; + final String playerO; + + Players({ + required this.playerX, + required this.playerO, + }); +} diff --git a/lib/model/tic_tac_toe.dart b/lib/model/tic_tac_toe.dart index 6c742d9..e86b15e 100644 --- a/lib/model/tic_tac_toe.dart +++ b/lib/model/tic_tac_toe.dart @@ -1,24 +1,68 @@ +import 'package:tic_tac_toe_handson/model/players.dart'; + class TicTacToe { final List> board; + final Players players; final String currentPlayer; - factory TicTacToe.start() { - return TicTacToe._([ - ['', '', ''], - ['', '', ''], - ['', '', ''], - ], 'X'); + factory TicTacToe.start({ + playerX = 'X', + playerO = 'O', + }) { + final players = Players( + playerX: playerX, + playerO: playerO, + ); + + return TicTacToe( + [ + ['', '', ''], + ['', '', ''], + ['', '', ''], + ], + players, + players.playerX, + ); } - TicTacToe._(this.board, this.currentPlayer); + TicTacToe(this.board, this.players, this.currentPlayer); + + factory TicTacToe.fromJson(Map json) { + final flatBoard = List.from(json['board']); + + return TicTacToe( + [ + List.from(flatBoard.sublist(0, 3)), + List.from(flatBoard.sublist(3, 6)), + List.from(flatBoard.sublist(6, 9)), + ], + Players( + playerX: json['players']['playerX'], + playerO: json['players']['playerO'], + ), + json['currentPlayer'], + ); + } + + Map toJson() { + return { + // Firestoreではネストした配列を扱えないため、1次元配列に変換する + 'board': [...board[0], ...board[1], ...board[2]], + 'players': { + 'playerX': players.playerX, + 'playerO': players.playerO, + }, + 'currentPlayer': currentPlayer, + }; + } TicTacToe placeMark(int row, int col) { if (board[row][col].isEmpty) { final newBoard = List.of(board); newBoard[row][col] = currentPlayer; - String nextPlayer = currentPlayer == 'X' ? 'O' : 'X'; + String nextPlayer = currentPlayer == players.playerX ? players.playerO : players.playerX; - return TicTacToe._(newBoard, nextPlayer); + return TicTacToe(newBoard, players, nextPlayer); } return this; } @@ -50,6 +94,6 @@ class TicTacToe { } TicTacToe resetBoard() { - return TicTacToe.start(); + return TicTacToe.start(playerX: players.playerX, playerO: players.playerO); } } diff --git a/lib/provider/tic_tac_toe_provider.dart b/lib/provider/tic_tac_toe_provider.dart index 9eb8ef2..b9cf7b3 100644 --- a/lib/provider/tic_tac_toe_provider.dart +++ b/lib/provider/tic_tac_toe_provider.dart @@ -1,18 +1,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tic_tac_toe_handson/model/tic_tac_toe.dart'; +import 'package:tic_tac_toe_handson/repository/tic_tac_toe_repository.dart'; -final ticTacToeProvider = StateNotifierProvider.autoDispose((ref) { - return TicTacToeProvider(); +final ticTacToeProvider = StreamProvider.autoDispose((ref) { + // 対戦相手同士のIDを設定する + return getTicTacToe(playerX: 'flutter', playerO: 'kaigi'); }); - -class TicTacToeProvider extends StateNotifier { - TicTacToeProvider() : super(TicTacToe.start()); - - placeMark(int row, int col) { - state = state.placeMark(row, col); - } - - resetBoard() { - state = state.resetBoard(); - } -} diff --git a/lib/repository/tic_tac_toe_repository.dart b/lib/repository/tic_tac_toe_repository.dart new file mode 100644 index 0000000..8a48bf1 --- /dev/null +++ b/lib/repository/tic_tac_toe_repository.dart @@ -0,0 +1,26 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:tic_tac_toe_handson/model/tic_tac_toe.dart'; + +final client = FirebaseFirestore.instance; +const rootCollectionKey = 'tic_tac_toe'; + +Stream getTicTacToe({String playerX = 'X', String playerO = 'O'}) { + final documentKey = _documentKey(playerX, playerO); + return client.collection(rootCollectionKey).doc(documentKey).snapshots().map((snapshot) { + final data = snapshot.data(); + if (data == null) { + return TicTacToe.start(playerX: playerX, playerO: playerO); + } + + return TicTacToe.fromJson(data); + }); +} + +Future updateTicTacToe(TicTacToe ticTacToe) async { + final documentKey = _documentKey(ticTacToe.players.playerX, ticTacToe.players.playerO); + await client.collection(rootCollectionKey).doc(documentKey).set(ticTacToe.toJson()); +} + +String _documentKey(String playerX, String playerO) { + return '${playerX}_${playerO}'; +} diff --git a/lib/view/board.dart b/lib/view/board.dart index 9de90ac..b9b7794 100644 --- a/lib/view/board.dart +++ b/lib/view/board.dart @@ -2,69 +2,76 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tic_tac_toe_handson/model/tic_tac_toe.dart'; import 'package:tic_tac_toe_handson/provider/tic_tac_toe_provider.dart'; +import 'package:tic_tac_toe_handson/repository/tic_tac_toe_repository.dart'; class Board extends ConsumerWidget { const Board({Key? key}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { - final ticTacToe = ref.watch(ticTacToeProvider); + final ticTacToeStream = ref.watch(ticTacToeProvider); - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Text( - _statusMessage(ticTacToe), - style: Theme.of(context).textTheme.headlineSmall, - ), - ), - GridView.builder( - shrinkWrap: true, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - ), - itemCount: 9, - itemBuilder: (context, index) { - final row = index ~/ 3; - final col = index % 3; - final mark = ticTacToe.board[row][col]; + return ticTacToeStream.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, __) => Center(child: Text('エラーが発生しました: ${error.toString()}')), + data: (ticTacToe) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + _statusMessage(ticTacToe), + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + GridView.builder( + shrinkWrap: true, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + ), + itemCount: 9, + itemBuilder: (context, index) { + final row = index ~/ 3; + final col = index % 3; + final mark = ticTacToe.board[row][col]; - return GestureDetector( - onTap: () { - final winner = ticTacToe.getWinner(); - if (mark.isEmpty && winner.isEmpty) { - ref.read(ticTacToeProvider.notifier).placeMark(row, col); - } - }, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - ), - child: Center( - child: Text( - mark, - style: const TextStyle(fontSize: 32), + return GestureDetector( + onTap: () { + final winner = ticTacToe.getWinner(); + if (mark.isEmpty && winner.isEmpty) { + updateTicTacToe(ticTacToe.placeMark(row, col)); + } + }, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + ), + child: Center( + child: Text( + mark, + style: const TextStyle(fontSize: 32), + ), + ), ), - ), + ); + }, + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + updateTicTacToe(ticTacToe.resetBoard()); + }, + child: const Text('ゲームをリセット'), ), - ); - }, - ), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - ref.read(ticTacToeProvider.notifier).resetBoard(); - }, - child: const Text('ゲームをリセット'), - ), + ), + ], ), - ], - ), + ); + }, ); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..1102414 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,10 @@ import FlutterMacOS import Foundation +import cloud_firestore +import firebase_core func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) } diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index ef1767e..0547043 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 390C5232493D4C6BB5C85996 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 03B35A50CC857F3315B76937 /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -52,9 +53,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 03B35A50CC857F3315B76937 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* tic_tac_toe_handson.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "tic_tac_toe_handson.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* tic_tac_toe_handson.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = tic_tac_toe_handson.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -99,6 +101,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + 03B35A50CC857F3315B76937 /* GoogleService-Info.plist */, ); sourceTree = ""; }; @@ -227,6 +230,7 @@ files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + 390C5232493D4C6BB5C85996 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/macos/firebase_app_id_file.json b/macos/firebase_app_id_file.json new file mode 100644 index 0000000..86682a1 --- /dev/null +++ b/macos/firebase_app_id_file.json @@ -0,0 +1,7 @@ +{ + "file_generated_by": "FlutterFire CLI", + "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", + "GOOGLE_APP_ID": "1:807943088200:ios:125d7bfaa751582d21907d", + "FIREBASE_PROJECT_ID": "tic-tac-toe-handson", + "GCM_SENDER_ID": "807943088200" +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 6793191..a9c08ea 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "2d8e8e123ca3675625917f535fcc0d3a50092eef44334168f9b18adc050d4c6e" + url: "https://pub.dev" + source: hosted + version: "1.3.6" async: dependency: transitive description: @@ -33,6 +41,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: "50e1ffa143fc5c49db1800392f8d9524fd015f9d26a9e4fc01b5ddb1e603e01b" + url: "https://pub.dev" + source: hosted + version: "4.9.2" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: "150e603a40d52b3199e46b1e38d9f8ef8c2dee9e1fb2122d58d456c50015bf7c" + url: "https://pub.dev" + source: hosted + version: "5.16.1" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: f033aef13b13f94b0f361898df39307d8710859c8912626cfb08e439e350bd66 + url: "https://pub.dev" + source: hosted + version: "3.7.1" collection: dependency: transitive description: @@ -57,6 +89,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "675c209c94a1817649137cbd113fc4c9ae85e48d03dd578629abbec6d8a4d93d" + url: "https://pub.dev" + source: hosted + version: "2.16.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: b63e3be6c96ef5c33bdec1aab23c91eb00696f6452f0519401d640938c94cba2 + url: "https://pub.dev" + source: hosted + version: "4.8.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: e8c408923cd3a25bd342c576a114f2126769cd1a57106a4edeaa67ea4a84e962 + url: "https://pub.dev" + source: hosted + version: "2.8.0" flutter: dependency: "direct main" description: flutter @@ -83,6 +139,19 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" lints: dependency: transitive description: @@ -123,6 +192,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.3" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + url: "https://pub.dev" + source: hosted + version: "2.1.6" riverpod: dependency: transitive description: @@ -210,4 +287,4 @@ packages: version: "0.1.4-beta" sdks: dart: ">=3.1.0 <4.0.0" - flutter: ">=3.0.0" + flutter: ">=3.3.0" diff --git a/pubspec.yaml b/pubspec.yaml index 654a147..c1f0d47 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,8 @@ dependencies: sdk: flutter cupertino_icons: ^1.0.2 flutter_riverpod: ^2.4.0 + firebase_core: ^2.16.0 + cloud_firestore: ^4.9.2 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..1a82e7d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..fa8a39b 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + firebase_core ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From 34e0250a2a3a102dc31c0c6b6051d6b4077aed68 Mon Sep 17 00:00:00 2001 From: Daichi Aoki Date: Sun, 1 Oct 2023 23:52:39 +0900 Subject: [PATCH 2/6] =?UTF-8?q?:recycle:=20=E3=83=95=E3=82=A9=E3=83=BC?= =?UTF-8?q?=E3=83=9E=E3=83=83=E3=83=88=E4=BF=AE=E6=AD=A3=EF=BC=88=E6=A9=9F?= =?UTF-8?q?=E8=83=BD=E5=A4=89=E6=9B=B4=E3=81=AA=E3=81=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/model/tic_tac_toe.dart | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/model/tic_tac_toe.dart b/lib/model/tic_tac_toe.dart index e86b15e..bbdcc3e 100644 --- a/lib/model/tic_tac_toe.dart +++ b/lib/model/tic_tac_toe.dart @@ -60,7 +60,8 @@ class TicTacToe { if (board[row][col].isEmpty) { final newBoard = List.of(board); newBoard[row][col] = currentPlayer; - String nextPlayer = currentPlayer == players.playerX ? players.playerO : players.playerX; + String nextPlayer = + currentPlayer == players.playerX ? players.playerO : players.playerX; return TicTacToe(newBoard, players, nextPlayer); } @@ -70,27 +71,36 @@ class TicTacToe { String getWinner() { for (int i = 0; i < 3; i++) { // row = i における横の判定 - if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][0].isNotEmpty) { + if (board[i][0] == board[i][1] && + board[i][1] == board[i][2] && + board[i][0].isNotEmpty) { return board[i][0]; } // col = i における縦の判定 - if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i].isNotEmpty) { + if (board[0][i] == board[1][i] && + board[1][i] == board[2][i] && + board[0][i].isNotEmpty) { return board[0][i]; } } // 左上から右下への斜めの判定 - if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[0][0].isNotEmpty) { + if (board[0][0] == board[1][1] && + board[1][1] == board[2][2] && + board[0][0].isNotEmpty) { return board[0][0]; } // 右上から左下への斜めの判定 - if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[0][2].isNotEmpty) { + if (board[0][2] == board[1][1] && + board[1][1] == board[2][0] && + board[0][2].isNotEmpty) { return board[0][2]; } return ''; } bool isDraw() { - return getWinner().isEmpty && board.every((row) => row.every((cell) => cell.isNotEmpty)); + return getWinner().isEmpty && + board.every((row) => row.every((cell) => cell.isNotEmpty)); } TicTacToe resetBoard() { From 93c8cc0c5fb15d32d7f505a06009ae320a38f384 Mon Sep 17 00:00:00 2001 From: Daichi Aoki Date: Sun, 1 Oct 2023 23:54:21 +0900 Subject: [PATCH 3/6] =?UTF-8?q?:+1:=20=E3=83=AA=E3=83=9D=E3=82=B8=E3=83=88?= =?UTF-8?q?=E3=83=AA=E3=82=92=E3=82=AF=E3=83=A9=E3=82=B9=EF=BC=86Provider?= =?UTF-8?q?=E5=8C=96=EF=BC=88=E6=A9=9F=E8=83=BD=E5=A4=89=E6=9B=B4=E3=81=AA?= =?UTF-8?q?=E3=81=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/repository/tic_tac_toe_repository.dart | 58 +++++++++++++++------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/lib/repository/tic_tac_toe_repository.dart b/lib/repository/tic_tac_toe_repository.dart index 8a48bf1..9d857dd 100644 --- a/lib/repository/tic_tac_toe_repository.dart +++ b/lib/repository/tic_tac_toe_repository.dart @@ -1,26 +1,48 @@ import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tic_tac_toe_handson/model/tic_tac_toe.dart'; -final client = FirebaseFirestore.instance; -const rootCollectionKey = 'tic_tac_toe'; +final ticTacToeRepositoryProvider = AutoDisposeProvider( + (ref) => TicTacToeRepository(), +); -Stream getTicTacToe({String playerX = 'X', String playerO = 'O'}) { - final documentKey = _documentKey(playerX, playerO); - return client.collection(rootCollectionKey).doc(documentKey).snapshots().map((snapshot) { - final data = snapshot.data(); - if (data == null) { - return TicTacToe.start(playerX: playerX, playerO: playerO); - } +/// 盤面のデータを管理するリポジトリ +final class TicTacToeRepository { + TicTacToeRepository(); - return TicTacToe.fromJson(data); - }); -} + final _client = FirebaseFirestore.instance; + static const _collectionKey = 'tic_tac_toe'; -Future updateTicTacToe(TicTacToe ticTacToe) async { - final documentKey = _documentKey(ticTacToe.players.playerX, ticTacToe.players.playerO); - await client.collection(rootCollectionKey).doc(documentKey).set(ticTacToe.toJson()); -} + String _documentKey(String playerX, String playerO) { + return '${playerX}_$playerO'; + } + + CollectionReference _colRef() => + _client.collection(_collectionKey).withConverter( + fromFirestore: (doc, _) => TicTacToe.fromJson(doc.data()!), + toFirestore: (entity, _) => entity.toJson(), + ); + + /// 盤面のデータを取得する + Stream get({ + String playerX = 'X', + String playerO = 'O', + }) { + final documentKey = _documentKey(playerX, playerO); + return _colRef().doc(documentKey).snapshots().map( + (e) => + e.data() ?? + TicTacToe.start( + playerX: playerX, + playerO: playerO, + ), + ); + } -String _documentKey(String playerX, String playerO) { - return '${playerX}_${playerO}'; + /// 盤面のデータを更新する + Future update(TicTacToe ticTacToe) async { + final documentKey = + _documentKey(ticTacToe.players.playerX, ticTacToe.players.playerO); + await _colRef().doc(documentKey).set(ticTacToe); + } } From 7e78deb09fc03e68546da6bfd17489b8df98fa02 Mon Sep 17 00:00:00 2001 From: Daichi Aoki Date: Sun, 1 Oct 2023 23:55:39 +0900 Subject: [PATCH 4/6] =?UTF-8?q?:recycle:=20stream=E3=81=AE=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E3=81=A8Provider=E3=81=AE=E5=AE=9A=E7=BE=A9=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/provider/get_tic_tac_toe_provider.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 lib/provider/get_tic_tac_toe_provider.dart diff --git a/lib/provider/get_tic_tac_toe_provider.dart b/lib/provider/get_tic_tac_toe_provider.dart new file mode 100644 index 0000000..b334de5 --- /dev/null +++ b/lib/provider/get_tic_tac_toe_provider.dart @@ -0,0 +1,12 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:tic_tac_toe_handson/model/tic_tac_toe.dart'; +import 'package:tic_tac_toe_handson/repository/tic_tac_toe_repository.dart'; + +final getTicTacToeProvider = AutoDisposeStreamProvider( + (ref) => + // 対戦相手同士のIDを設定する + ref.watch(ticTacToeRepositoryProvider).get( + playerX: 'flutter', + playerO: 'kaigi', + ), +); From 93b4ad42e479a161eb4f70365977d3de3341373d Mon Sep 17 00:00:00 2001 From: Daichi Aoki Date: Sun, 1 Oct 2023 23:56:16 +0900 Subject: [PATCH 5/6] =?UTF-8?q?:sparkles:=20update=E3=82=92Provider?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...toe_provider.dart => update_tic_tac_toe_provider.dart} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename lib/provider/{tic_tac_toe_provider.dart => update_tic_tac_toe_provider.dart} (51%) diff --git a/lib/provider/tic_tac_toe_provider.dart b/lib/provider/update_tic_tac_toe_provider.dart similarity index 51% rename from lib/provider/tic_tac_toe_provider.dart rename to lib/provider/update_tic_tac_toe_provider.dart index b9cf7b3..7e2713a 100644 --- a/lib/provider/tic_tac_toe_provider.dart +++ b/lib/provider/update_tic_tac_toe_provider.dart @@ -2,7 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tic_tac_toe_handson/model/tic_tac_toe.dart'; import 'package:tic_tac_toe_handson/repository/tic_tac_toe_repository.dart'; -final ticTacToeProvider = StreamProvider.autoDispose((ref) { - // 対戦相手同士のIDを設定する - return getTicTacToe(playerX: 'flutter', playerO: 'kaigi'); -}); +final updateTicTacToeProvider = + AutoDisposeFutureProviderFamily( + (ref, arg) => ref.watch(ticTacToeRepositoryProvider).update(arg), +); From 767482dd7d2893ad48ed06734ec17fc2ae2e2dac Mon Sep 17 00:00:00 2001 From: Daichi Aoki Date: Sun, 1 Oct 2023 23:56:49 +0900 Subject: [PATCH 6/6] =?UTF-8?q?:recycle:=20UI=E3=81=A7Provider=E5=8C=96?= =?UTF-8?q?=E3=81=97=E3=81=9Fupdate=E3=82=92=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/view/board.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/view/board.dart b/lib/view/board.dart index b9b7794..82262d5 100644 --- a/lib/view/board.dart +++ b/lib/view/board.dart @@ -1,19 +1,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tic_tac_toe_handson/model/tic_tac_toe.dart'; -import 'package:tic_tac_toe_handson/provider/tic_tac_toe_provider.dart'; -import 'package:tic_tac_toe_handson/repository/tic_tac_toe_repository.dart'; +import 'package:tic_tac_toe_handson/provider/get_tic_tac_toe_provider.dart'; +import 'package:tic_tac_toe_handson/provider/update_tic_tac_toe_provider.dart'; class Board extends ConsumerWidget { const Board({Key? key}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { - final ticTacToeStream = ref.watch(ticTacToeProvider); + final ticTacToeStream = ref.watch(getTicTacToeProvider); return ticTacToeStream.when( loading: () => const Center(child: CircularProgressIndicator()), - error: (error, __) => Center(child: Text('エラーが発生しました: ${error.toString()}')), + error: (error, __) => + Center(child: Text('エラーが発生しました: ${error.toString()}')), data: (ticTacToe) { return Padding( padding: const EdgeInsets.all(16), @@ -41,7 +42,11 @@ class Board extends ConsumerWidget { onTap: () { final winner = ticTacToe.getWinner(); if (mark.isEmpty && winner.isEmpty) { - updateTicTacToe(ticTacToe.placeMark(row, col)); + ref.read( + updateTicTacToeProvider( + ticTacToe.placeMark(row, col), + ), + ); } }, child: Container( @@ -63,7 +68,9 @@ class Board extends ConsumerWidget { width: double.infinity, child: ElevatedButton( onPressed: () { - updateTicTacToe(ticTacToe.resetBoard()); + ref.read( + updateTicTacToeProvider(ticTacToe.resetBoard()), + ); }, child: const Text('ゲームをリセット'), ),