Chapter 09

状態保持部分のリファクタリング

Kurogoma4D
Kurogoma4D
2023.05.17に更新

辞書データと正解文字列

まずは辞書データと正解文字列の状態管理をRiverpodで行っていきましょう。entities/correct_answer.dart の内容を以下のように変更してください。

import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'correct_answer.g.dart';

(keepAlive: true)
List<String> wordDictionary(WordDictionaryRef ref) => [];

(keepAlive: true)
class CorrectAnswer extends _$CorrectAnswer {
  
  String build() => '';

  void update(String value) => state = value;
}

// List<String> wordDictionary = [];
// var correctAnswer = '';

今まで使っていたグローバル変数の wordDictionary をここではコメントアウトしていますが、実際にコメントアウトするとコンパイルエラーになるかと思います。
これはProviderを生成すると wordDictionary というシンボルが衝突する(Providerの元になる関数とグローバル変数)ためです。途中でアプリを実行できる状態を保ちたい場合は、適宜 wordDictionary をリネームしてください。VS Codeの機能を使ってリネームすると、変数の利用箇所も自動的に変更されるので便利です(右クリックのメニューから「シンボルの名前変更」を選択してください)。

rename

本書では riverpod_generator を活用してProviderを作っていきます。ただ上のコードを書いただけでは機能せず、Providerをコマンドにより生成する必要があります。

ターミナルでプロジェクトのルートディレクトリに移動して、以下のコマンドを実行してください。

dart run build_runner watch

build_runnerwatch コマンドは、一度実行しておくとファイルの変更を検知して必要に応じて成果物を自動生成してくれます。
もしくはVS Codeの拡張機能を利用しても構いません。

https://marketplace.visualstudio.com/items?itemName=Kaiqun.build-runner

期待通りProviderが生成されたら、次は利用側を変更していきます。
main.dart を開いて、以下のように変更してください。

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final wordsString = await rootBundle.loadString('assets/words.txt');

  final container = ProviderContainer(overrides: [
    wordDictionaryProvider.overrideWithValue(wordsString.split('\n')),
  ]);

  _decideAnswer(container);
  runApp(
    ProviderScope(
      parent: container,
      child: const App(),
    ),
  );
}

void _decideAnswer(ProviderContainer container) {
  final dictionary = container.read(wordDictionaryProvider);
  final index = _random.nextInt(dictionary.length);
  container.read(correctAnswerProvider.notifier).update(dictionary[index]);
  debugPrint('answer: ${dictionary[index]}');
}

wordDictionaryProvider の宣言部分( wordDictionary() )では、その実装は [](空のリストリテラル)としていました。辞書データは不変のため、後から変更できる形にしていないので、アプリ開始時に何もしなければ空のままになってしまいます。

そこで、辞書データをファイルから読み込んだ後、ProviderContainer を独自に宣言し、overrideを使って実装を上書きします。
ProviderContainer はFlutterアプリではあまり使いませんが、これをいつもWidgetツリーのルートに置いておく ProviderScope に渡してやると、中身が継承されるのでそれ以下のツリーから参照できるようになります。

correctAnswercorrectAnswerProvider を使うように変更されています。
correctAnswerProvider は可変のものとして宣言されており、update() を使って変更することが可能です。これを利用して、正解の選定方法は変えずに状態のみ更新しています。

回答の状態

次に回答の状態を書き換えていきます。 entities/answer_state.dart を次のように変更してください。

import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:wordle_clone/entities/entities.dart';

part 'answer_state.g.dart';

// ...
// 定数、型定義などはそのまま
// ...

int currentCursor = 0;
int answerCount = 0;

// ここから追加
(keepAlive: true)
class CurrentAnswer extends _$CurrentAnswer {
  
  AnswerState build() => createInitialAnswerState();

  List<List<AnswerPanelState>> get _stateClone => [
        for (final row in super.state) [...row]
      ];

  void update(int answerCount, int cursor, AnswerPanelState newPanel) {
    final currentState = _stateClone;

    currentState[answerCount][cursor] = newPanel;
    state = currentState;
  }

  void updateRow(int answerCount, AnswerRowState newState) {
    final currentState = _stateClone;

    currentState[answerCount] = newState;
    state = currentState;
  }
}

(keepAlive: true)
class AnswerCount extends _$AnswerCount {
  
  int build() => 0;

  void increment() => state += 1;

  void update(int value) => state = value;
}

(keepAlive: true)
class CurrentCursor extends _$CurrentCursor {
  
  int build() => 0;

  void increment() => state += 1;

  void update(int value) => state = value;
}

currentCursoranswerCount はそのまま可変のProvider currentCursorProvider answerCountProvider に置き換えました。
後のロジックのリファクタリングにも影響しますが、これらを更新するときの操作は以下の二通りしか無いので、予めそのようなメソッドを用意しておきます。

  • 1だけ増加させる(increment)
  • 任意の値に変更する(update)

また、もともと answer_panels.dart に実態を持っていた全体の回答の状態 answerState も、currentAnswerProvider としてここで管理することにしました。
初期化処理は createInitialAnswerState() をそのまま利用、そして更新処理は以下の二通りを用意しています。

  • 任意の行・列に該当する回答の変更(update)
  • 任意の行全体の変更(updateRow)