Flutter Puzzle Hackに参加した感想


はじめに

Flutter Puzzle Hackに参加しました。
そこで得られた知見をまとめたいと思います。

Flutter Puzzle Hackって?

Flutterを使ってスライドパズルを作成するハッカソンです。公式による開催としてはFlutter Clock Challengeに続いて2回目でした。

開発した作品

Just Look Upというパズルゲームを作成しました。こちらから飛べるので是非遊んでみてください。

https://youtu.be/_DZ8pI_mUR8

ソースコード: https://github.com/tomohiko-tanihata/just_look_up

工夫したポイント

  • パズル生成のアルゴリズム
  • Flutter webでの並列計算
  • 無駄なリビルドを防ぐレスポンシブデザイン

パズル生成のアルゴリズム

パズルの元ネタはポケモン金銀のこおりのぬけみちなのですが、幅優先探索を用いて解けることに気が付きました。
アルゴリズム概要
こんなアルゴリズムをDartで実装しパズルを自動生成できるようにしました。

Flutter for webでの並列計算

並列計算を用いてプレイヤーがパズルを解いている間に次のパズルを事前に計算しています。Dart Native Platformだとisolateを使えばよいのですが、webに対しては公式ドキュメントにかかれている通り、isolateライブラリが対応していないためweb workerを使う必要がありました。今回はisolated_workerを使いました。並列計算する処理はJavascriptでコードを書く必要があるので面倒でした。

無駄なリビルドを防ぐレスポンシブデザイン

公式でも推奨されているLayoutBuilderを用いた公式ドキュメントの実装例です。

class MyStatelessWidget extends StatelessWidget {
  const MyStatelessWidget({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('LayoutBuilder Example')),
      body: LayoutBuilder(
        builder: (BuildContext context, BoxConstraints constraints) {
          if (constraints.maxWidth > 600) {
            return _buildWideContainers();
          } else {
            return _buildNormalContainer();
          }
        },
      ),
    );
  }

constraintsの幅によってWidget _buildWideContainers()Widget _buildNormalContainer()を出し分けています。このケースでは切り替えのタイミングでWidget Treeの構成自体が変わりWidget _buildWideContainers()もしくはWidget _buildNormalContainer()がビルドされます。この例ではWidget自体が小さいので問題にならないですが、ネストが深いWidgetの場合は無駄なリビルドが走るという問題が発生することがあります。この問題を避けるために、レスポンシブによるビルド対象を最低限に留める3つの工夫をしています。

  • Mobile, Tablet, Desktopのサイズ違いによってWidget Treeの構成を変えない
  • サイズの状態に依存しているWidgetのみリビルド
  • ヘルパー関数ではなくprivateなStateless Widgetで切り出す

サイズ違いによってWidget Treeの構成を変えない

Appの根本でLayoutBuilderを用いている箇所のソースコードを抜粋します。

class _Puzzle extends StatelessWidget {
  const _Puzzle({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) =>
      LayoutBuilder(builder: (context, constraints) {
        context.read<PuzzleResponsiveCubit>().init(context);
        return SingleChildScrollView(
          child: ConstrainedBox(
            constraints: BoxConstraints(minHeight: constraints.maxHeight),
            child: Center(
              child: Flex(
                mainAxisAlignment: context.watch<PuzzleResponsiveCubit>().state
                        is PuzzleDesktopResponsive
                    ? MainAxisAlignment.spaceEvenly
                    : MainAxisAlignment.center,
                direction: context.watch<PuzzleResponsiveCubit>().state
                        is PuzzleDesktopResponsive
                    ? Axis.horizontal
                    : Axis.vertical,
                children: [
                  SizedBox(
                    width: context
                        .watch<PuzzleResponsiveCubit>()
                        .state
                        .size
                        .boardDimension,
                    child: const PuzzleHeader(),
                  ),
                  SizedBox(
                    width: context
                        .watch<PuzzleResponsiveCubit>()
                        .state
                        .size
                        .boardDimension,
                    child: Column(
                      children: const [
                        PuzzleBoard(),
                        PuzzleChangeStatusButton(),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      });
}

ここで重要なのはLayoutBuilder以下のchildで条件分岐を与えていない点です。RowColumnを使わずにFlexdirectionプロパティを用いることでWidget Treeの構成を変えない方法でレイアウトの切り替えを実現しました。

サイズの状態に依存しているWidgetのみリビルド

今回は日本ではあまり人気の無いBlocライブラリを使って状態を管理しています。サイズの状態をPuzzleResponsiveCubitとして持たせ、context.watch<PuzzleResponsiveCubit>().state.size.boardDimensionという風にして値を参照することでサイズの状態が変わった際に影響のあるWidgetのリビルドが走ります。

ヘルパー関数ではなくprivateなStateless Widgetで切り出す

ヘルパー関数ではなくStateless Widgetで切り出すことでビルド単位を細分化し、無駄なリビルドを防ぎました。下記の説明が参考になると思います。

https://youtu.be/IOyq-eTRhvo

学んだこと

技術的な学びももちろん色々ありますが、意外と技術以外での学びが大きかったです。

アプリ開発びっくりするくらい楽しい

開発期間中の2ヶ月間本当に楽しくて、作業中はいつもあっという間に時間が過ぎていきました。自分のアイデアを形にして、世に出すことってこんなにもわくわくするんですね。個人開発にハマってる人の気持ちが少し分かった気がします。

誰かと一緒にやるって大切

はじめは1人だったのでデザインも自分でやってましたが、デザインが致命的にイケてなかったのでいつもお世話になっているデザイナーさんにお願いして途中で入ってもらいました。デザインが良くなったのはもちろんのこと、コンセプトや機能的なところのご意見も幅広く聞けたことが良かったです。1人でやるとどうしても盲目的になってしまうため、誰かと一緒にやって客観的な目線を取り入れるのがすごく大切だと感じました。

さいごに

充実した2ヶ月でした。もともと「勝つためじゃなくスキルアップのためというマインドセットで臨もう」と決めており、スキルアップという目標については達成できたかなっと思っているので自分としては満足しています。ちなみに他の参加者の作品を見れるのですが、本当にどれも完成度が高くてすごいので是非見てみてください。