Skip to content

Commit

Permalink
docs: chapter1と5を仮置き
Browse files Browse the repository at this point in the history
  • Loading branch information
okaryo committed Oct 21, 2023
1 parent fb548ea commit 8a30e77
Show file tree
Hide file tree
Showing 2 changed files with 337 additions and 10 deletions.
60 changes: 54 additions & 6 deletions docs/outline/chapter1.md
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),
),
),
);
}
}
```
287 changes: 283 additions & 4 deletions docs/outline/chapter5.md
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: '申し込んだプレイヤー名',
),
);
```

それでは遊んでみましょう〜!!

0 comments on commit 8a30e77

Please sign in to comment.