-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
337 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,58 @@ | ||
# Riverpod の紹介 | ||
## Riverpodとは | ||
RiverpodはDart/Flutterで使用できる状態管理ライブラリです。 | ||
[Remi Rousselet](https://github.com/rrousselGit) 氏によって作成されており、Riverpodの他に「freezed」「flutter_hooks」なども作成されております。 | ||
「Provider」についても同氏が作成している状態管理ライブラリですが、公式FAQにおいても「多分、Providerは非推奨にする予定で、Riverpodへの移行ツールも計画している」とのことで、作者自身もRiverpodの使用を推奨しております。 | ||
ちなみにRiverpodはProviderのアナグラムです。 | ||
|
||
## 1.1: Riverpod とは | ||
## Riverpodのメリット | ||
Flutterの状態管理手法はStatefulWidgetPatternをはじめ、様々あります。 | ||
その中でRiverpodを使用するメリットは以下が挙げられると考えております。 | ||
|
||
## 1.2: Riverpod が役立つ場面 | ||
- 複数のWidgetから状態にアクセスができる | ||
- keepAliveやautoDisposeにより、安全に破棄ができる | ||
- Consumer等を適切に使用することでパーフォマンスの向上(Widgetの再構築)に繋がる/selectによるスコープの絞り込みもできる | ||
- 複数のProvider(状態)を噛み合わせることができる | ||
- riverpod_generatorと併用することでclassやmethodの記法でProvderを作成できる | ||
- DIもしやすく、テストの容易性の向上に繋がる(Override) | ||
- Flutter Favoriteである | ||
|
||
## 1.3: Riverpod の使い方 | ||
## Riverpodの使い方 | ||
以下は公式ドキュメントのサンプルからriverpod_generatorを使用しない形式に修正したものです。 | ||
|
||
## メモ | ||
* 場合によってはfreezedやriverpod_generatorの説明も入るかも | ||
``` dart | ||
import 'package:flutter/material.dart'; | ||
import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||
// String の状態を取り扱いたい場合、以下のように記載します。 | ||
final helloWorldProvider = StateProvider<String>((ref) => 'Hello world'); | ||
// main.dartのrunAppの部分でProviderScopeを入れ込みます。 | ||
// ProviderScopeで囲うことによりアクセスが可能となります。 | ||
void main() { | ||
runApp( | ||
ProviderScope( | ||
child: MyApp(), | ||
), | ||
); | ||
} | ||
// providerを使用するWidgetでは基本的にStatelessWidgetの代わりにConsumerWidgetを使用します。 | ||
// 利用したいproviderをref.watchで参照することで状態を取り扱うことが可能です。 | ||
class MyApp extends ConsumerWidget { | ||
@override | ||
Widget build(BuildContext context, WidgetRef ref) { | ||
final String value = ref.watch(helloWorldProvider); | ||
return MaterialApp( | ||
home: Scaffold( | ||
appBar: AppBar(title: const Text('Example')), | ||
body: Center( | ||
child: Text(value), | ||
), | ||
), | ||
); | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,286 @@ | ||
# Firestoreを用いたリアルタイム対戦機能 (チャレンジ企画) | ||
## Firestoreに繋ぐ準備をする | ||
プロジェクトルートで以下のコマンドを実行し、必要なライブラリを取得しましょう。 | ||
```zsh | ||
flutter pub add firebase_core | ||
flutter pub add cloud_firestore | ||
``` | ||
|
||
## 5.1: Firestore とは | ||
[GitHub Discussions](https://github.com/FlutterKaigi/tic_tac_toe_handson/discussions) から `firebase-options.dart` を取得し、Libに追加します。 | ||
|
||
## 5.2: Firestore と Stream の基本的な使い方 | ||
次に `main.dart` を修正します。 | ||
```dart | ||
// importを追加 | ||
import 'package:firebase_core/firebase_core.dart'; | ||
import 'package:tic_tac_toe_handson/firebase_options.dart'; | ||
## 5.3: Riverpod と Firestore を組み合わせた対戦機能 | ||
// asyncに修正 | ||
void main() async { | ||
// 追加 | ||
WidgetsFlutterBinding.ensureInitialized(); | ||
await Firebase.initializeApp( | ||
options: DefaultFirebaseOptions.currentPlatform, | ||
); | ||
// 略 | ||
} | ||
``` | ||
|
||
### 1. Androidでのビルド準備を進める | ||
#### Android/app フォルダ | ||
`build.gradle` に以下を追記します。 | ||
```txt | ||
apply plugin: 'com.google.gms.google-services' | ||
``` | ||
[GitHub Discussions](https://github.com/FlutterKaigi/tic_tac_toe_handson/discussions) から `google-services.json` をを取得し、追加します。 | ||
|
||
#### Android フォルダ | ||
`build.gradle` に以下を追記します。 | ||
```txt | ||
classpath 'com.google.gms:google-services:4.3.10' | ||
``` | ||
|
||
### 2. iOSでのビルド準備を進める | ||
iOSフォルダをXcodeで開いたのちに、Runnerに`google-services.json` を追加します。 | ||
このとき、「Copy items if needed」にチェックを入れて追加してください。 | ||
|
||
![Alt text](image-1.png) | ||
|
||
|
||
これで基本的な準備は完了! | ||
ハンズオン用に手動でしましたが、[FlutterFire](https://firebase.flutter.dev/)を使用することでコマンドで簡単にできます。 | ||
|
||
## modelにjsonコンバートメソッドを追加する | ||
`lib/model/tic_tac_toe.json` の`TicTacToe`クラス内に以下を追加します。 | ||
[freezed](https://pub.dev/packages/freezed) を使用することで、jsonコンバートはコマンド1発で作成可能ですが、ここでは自作してみましょう。 | ||
|
||
``` dart | ||
factory TicTacToe.fromJson(Map<String, dynamic> json) { | ||
final flatBoard = List<String>.from(json['board']); | ||
return TicTacToe( | ||
// Firestore側を1次元配列にしているので、モデルの2次元配列とここで合わせる | ||
[ | ||
List<String>.from(flatBoard.sublist(0, 3)), | ||
List<String>.from(flatBoard.sublist(3, 6)), | ||
List<String>.from(flatBoard.sublist(6, 9)), | ||
], | ||
Players( | ||
playerX: json['players']['playerX'], | ||
playerO: json['players']['playerO'], | ||
), | ||
json['currentPlayer'], | ||
); | ||
} | ||
Map<String, dynamic> toJson() { | ||
return { | ||
// モデルが2次元配列なので、Firestore側の1次元配列にここで合わせる | ||
'board': [...board[0], ...board[1], ...board[2]], | ||
'players': { | ||
'playerX': players.playerX, | ||
'playerO': players.playerO, | ||
}, | ||
'currentPlayer': currentPlayer, | ||
}; | ||
} | ||
``` | ||
|
||
## リポジトリを作成する | ||
まずは、新しいファイルを作りましょう。 | ||
`lib/repository/tic_toc_toe_repository.dart` | ||
|
||
続いて、クラスを作成します。 | ||
```dart | ||
import 'package:cloud_firestore/cloud_firestore.dart'; | ||
import 'package:tic_tac_toe_handson/model/tic_tac_toe.dart'; | ||
/// 盤面のデータを管理するリポジトリ | ||
final class TicTacToeRepository { | ||
TicTacToeRepository(); | ||
// Firestoreインスタンス | ||
final _client = FirebaseFirestore.instance; | ||
// Firestoreのコレクション先 | ||
static const _collectionKey = 'tic_tac_toe'; | ||
// 対戦状況を保存するドキュメント先 | ||
String _documentKey(String playerX, String playerO) { | ||
return '${playerX}_$playerO'; | ||
} | ||
// jsonコンバート | ||
CollectionReference<TicTacToe> _colRef() => | ||
_client.collection(_collectionKey).withConverter( | ||
fromFirestore: (doc, _) => TicTacToe.fromJson(doc.data()!), | ||
toFirestore: (entity, _) => entity.toJson(), | ||
); | ||
} | ||
``` | ||
|
||
### 1. getメソッドを追加する | ||
リポジトリのクラスにFirestoreからデータを取得するメソッドを記載しましょう。 | ||
```dart | ||
/// 盤面のデータを取得する | ||
Stream<TicTacToe> 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, | ||
), | ||
); | ||
} | ||
``` | ||
|
||
### 2. updateメソッドを追加する | ||
リポジトリのクラスにFirestoreへデータを保存するメソッドを記載しましょう。 | ||
```dart | ||
/// 盤面のデータを更新する | ||
Future<void> update(TicTacToe ticTacToe) async { | ||
// ドキュメント名に変換する | ||
final documentKey = | ||
_documentKey(ticTacToe.players.playerX, ticTacToe.players.playerO); | ||
// モデルをjsonに変換し、firestoreへ保存する | ||
await _colRef().doc(documentKey).set(ticTacToe); | ||
} | ||
``` | ||
|
||
### 3. リポジトリをProvider化する | ||
リポジトリのファイルに以下を追加します。 | ||
この後、getとupdateをそれぞれProvider化する際に使用します。 | ||
```dart | ||
import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||
final ticTacToeRepositoryProvider = AutoDisposeProvider<TicTacToeRepository>( | ||
(ref) => TicTacToeRepository(), | ||
); | ||
``` | ||
|
||
## データを取得するProviderを作成する | ||
新しいファイルを作りましょう。 | ||
`lib/provider/get_tic_toc_toe_provider.dart` | ||
|
||
以下を記載してください。 | ||
```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/repository/tic_tac_toe_repository.dart'; | ||
final getTicTacToeProvider = AutoDisposeStreamProvider<TicTacToe>( | ||
(ref) => | ||
// 対戦相手同士のIDを設定する(プレイヤー名は後ほど変更します) | ||
ref.watch(ticTacToeRepositoryProvider).get( | ||
playerX: 'Dash', | ||
playerO: 'Sparky', | ||
), | ||
); | ||
``` | ||
|
||
`ticTacToeRepositoryProvider` を使用しています。 | ||
RiverpodではこのようにProviderの中で別のProviderを組み合わせることが可能です。 | ||
|
||
FirestoreはWebSocketが基盤になっているため、リアルタイムでデータを送受信することが可能です。 | ||
その利点を活かして、今回は`Stream`でデータを取得するようにします。 | ||
|
||
## データを保存するProviderを作成する | ||
新しいファイルを作りましょう。 | ||
`lib/provider/update_tic_toc_toe_provider.dart` | ||
|
||
以下を記載してください。 | ||
```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/repository/tic_tac_toe_repository.dart'; | ||
final updateTicTacToeProvider = | ||
AutoDisposeFutureProviderFamily<void, TicTacToe>( | ||
(ref, arg) => ref.watch(ticTacToeRepositoryProvider).update(arg), | ||
); | ||
``` | ||
|
||
引数を与えて、何かを実施したい場合、`Family` を付与することで実現できます。 | ||
今回は盤面の情報を引数にして渡したいので、使用しています。 | ||
|
||
ここでも `ticTacToeRepositoryProvider` を使用しています。 | ||
`ticTacToeRepositoryProvider` を使用することでget用のProviderと同一の `TicTacToeRepository` クラスのインスタンスを参照することができます。 | ||
|
||
余談ですが、今回の場合は `AsyncNotifier` を使用することもできます。 | ||
色々な種類のProviderを使用したいという思いがあり、ハンズオンではこの形式にしました。 | ||
|
||
## 作成したProviderをWidgetで使用する | ||
getとupdateをそれぞれProviderにしたため、そちらをWidgetで使用しましょう。 | ||
`lib/view/board.dart` を修正します。 | ||
|
||
まずは参照の追加です。 | ||
|
||
```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'; | ||
``` | ||
|
||
次に使用するProviderを変更しましょう。 | ||
|
||
```dart | ||
// final ticTacToe = ref.watch(ticTacToeProvider); | ||
final ticTacToeStream = ref.watch(getTicTacToeProvider); | ||
``` | ||
|
||
今、returnしているPaddingを以下のコードで囲みます。 | ||
|
||
```dart | ||
return ticTacToeStream.when( | ||
loading: () => const Center(child: CircularProgressIndicator()), | ||
error: (error, __) => Center(child: Text('エラーが発生しました: ${error.toString()}')), | ||
data: (ticTacToe) { | ||
return Padding( | ||
// 略 | ||
); | ||
}, | ||
); | ||
``` | ||
|
||
Riverpodを使用すると `AsyncValue` を返却するProviderでは、このように `when` を使用することが可能です。 | ||
|
||
`loading` はデータがローディングの際に実施したい処理とWidgetを記載します。 | ||
`error` はデータがエラーの際に実施したい処理とWidgetを記載します。 | ||
`data` はデータが取得できた際に実施したい処理とWidgetを記載します。 | ||
|
||
このように非同期処理の内容をWidgetで簡単に取り扱うことが可能です。 | ||
|
||
では、最後にupdate用のProviderもそれぞれ変更しましょう。 | ||
```dart | ||
// ref.read(ticTacToeProvider.notifier).state = ticTacToe.placeMark(row, col); | ||
ref.read(updateTicTacToeProvider(ticTacToe.placeMark(row, col)),); | ||
``` | ||
|
||
```dart | ||
// ref.read(ticTacToeProvider.notifier).state = ticTacToe.resetBoard(); | ||
ref.read(updateTicTacToeProvider(ticTacToe.resetBoard()),); | ||
``` | ||
|
||
これで準備は完了です! | ||
|
||
## リアルタイムでデームをプレイする | ||
[GithubDiscussions](https://github.com/FlutterKaigi/tic_tac_toe_handson/discussions) に対戦相手募集中のスレッドを用意しております。 | ||
|
||
対戦を待つ場合は、そちらに自身のプレイヤー名を記載してください。 | ||
対戦を申し込む場合は、返信形式でプレイヤー名を記載してください。 | ||
|
||
対戦相手が決まったら、`get_tic_tac_toe_provider.dart` を以下のように修正してください。 | ||
```dart | ||
final getTicTacToeProvider = AutoDisposeStreamProvider<TicTacToe>( | ||
(ref) => | ||
ref.watch(ticTacToeRepositoryProvider).get( | ||
playerX: '申し込まれたプレイヤー名', | ||
playerO: '申し込んだプレイヤー名', | ||
), | ||
); | ||
``` | ||
|
||
それでは遊んでみましょう〜!! |