Flutter
高校生
Supabase
17
どのような問題がありますか?

投稿日

更新日

高校の文化祭でソフトウェア開発をしたお話

はじめに

自己紹介

こんにちは、もぐもぐと申します。現在とある横浜のフロンティアなサイエンス高校というところに通っている高校3年生(17歳1)の自称プログラマーです。
趣味でEQMonitorという地震観測・速報アプリケーションの開発(主にFlutter)をしたりもしています。
高校の文化祭(通称:蒼煌祭)のクラス企画でいくつかソフトウェアを開発したので記事に残そうと思います。

「忘れないうちに急いで書いちゃえ〜!」という気持ちで ザーッと書いていたら相当分かりにくい記事になってしまいました
不明な点、ご意見、ご要望等ありましたらコメントお願いします
(TwitterDMでもお待ちしています)

文化祭でソフト開発!? どんなクラス企画やね~ん

例年3年生は食品販売をやるらしいです。しかし、今年は新型コロナウィルス感染症の影響で食品販売は禁止でした。高校生のうちに一度くらい、食品販売をしてみたかったのですが、残念ながら叶いませんでした。

私のクラスの企画名はマークシートマニアです。


えっ、もしかしてディズ○ー・シーのトイ・ストーリマ〇ア!?と思った方、
大正解です。僕が最後にディズニー関連に行ったのは、幼稚園に入る前なので当然トイストーリーマニアがどんなアトラクションなのか全然分からないわけですが....2
具体的なお話をすると、

ライドに乗ってもらって 4択問題に対して、ボールを穴に投げて回答してね!
ボール1個につき 正答なら8点、誤答なら1点追加。ボールは投げ放題!
楽しいね!
大問は3つ! 各大問につき小問が3つの合計9問に回答してもらうぞ!
小問1つあたり17.5秒の回答時間が与えられるよ!
簡単に言えば、「ライド型シューティングアトラクション」やな!
素早く答えを考えてボールを投げまくれ!!

という感じです。
このクラス企画のシステム関連全てを私1人で開発しました。

  1. Classroom33Admin: 総合管理ソフトウェア(Windows/iPadOS)
    • 来場者をデータベースに登録
    • 全プロジェクターの状況確認
    • 出題開始承認
    • 結果表示
  2. Classroom33PJ: 問題表示用ソフトウェア(Windows)
    • Adminの承認後にControllerが指定したユーザーの問題文を表示(リアルタイム同期)
  3. Classroom33Controller: ボールカウント用ソフトウェア(Android/iOS)
    • プロジェクターに表示するユーザーを指定
    • 穴に入ったボールをカウントして結果をデータベースへ投げる

見てわかる通り、様々なプラットフォームで動くようにしたかったので実装する選択肢として、Webアプリ/クロスプラットフォーム開発が出てきました。
今回は高校生の間、ずっとお世話になってきたFlutterで実装しました。ネイティブコードは一切書いていません。ありがてえ〜!!!

classroom33Admin 出題開始承認&来場者登録&結果表示 全体状況の確認来場者登録結果確認
classroom33PJ 問題表示
classroom33Controller ボールカウント ユーザー選択

の開発です。

ざっくりやっていることを図式化するとこんな感じです。

当日の様子

百聞は一見にしかず。文字で説明するより見てもらったほうがイメージが湧きやすいと思います。どういう雰囲気だったかはこちらの動画をご覧ください。
0:00~0:50: 全体の雰囲気
0:50~1:44: 制作過程(クラスメイト作成)
1:44~3:09: 実際に人が来た時の様子(クラスメイト作成)

利用した技術

言語 Dart(Flutter)
データベース Supabase(PostgresSQL)
コード管理 Git/GitHub
iOS向けのビルド CodeMagic

Flutter

全てのアプリケーションはFlutterを用いて実装しました。
状態管理はflutter_hooksRiverpodを利用しました。RiverpodのFutureProviderStreamProviderはエラーハンドリングがめちゃくちゃ楽で感動しました。(Riverpodを利用する1番のメリットはそこではない気がしますが)
Controller/Admin/Projectorで共通の実装はパッケージ化して管理しやすくしました。

データベース(PostgreSQL)

Supabase CloudというBaaSを利用してバックエンド構築をしました。WebSocket経由でINSERT/UPDATEを検知したりもできちゃうスグレモノ
テーブルはこんな感じです。

初期化用SQL文
init.sql

CREATE TABLE
  IF NOT EXISTS public.users (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    created_at TIMESTAMP DEFAULT now() NOT NULL,
    big_question_group_id SMALLINT NOT NULL,
    number_of_people SMALLINT NOT NULL DEFAULT 1,
    total_point INT,
    result jsonb DEFAULT '{ "items": [] }',
    ride_id INT NOT NULL
  );

CREATE TABLE
  IF NOT EXISTS public.state (
    position TEXT PRIMARY KEY,
    big_question_state TEXT NOT NULL DEFAULT 'projector1',
    big_question_group_id INT
  );

DELETE from
  public.state;

SELECT
  SETVAL ('users_id_seq', 1, false);

DELETE FROM
  public.users;

--INSERT INTO public.users (big_question_group_id, ride_id) VALUES
--  (1,1),(2,2);
INSERT INTO
  public.state (
    position,
    big_question_state,
    big_question_group_id
  )
VALUES
  ('projector1', 'waitingForController', null),
  ('projector2', 'waitingForController', null),
  ('projector3', 'waitingForController', null);

stateテーブル

1ACCD331-2785-4A1D-9223-53562FCB8C0E.jpeg

  • big_question_stateに各大問の状態が入ります。
classroom33Common/lib/schema/state/state.dart
enum BigQuestionState {
  /// 移動中(Controllerによるだいもんごとのユーザー登録処理待ち)
  waitingForController,
  /// 一斉開始待ち(Adminによる一斉開始処理待ち)
  waitingForAdmin,
  /// 出題中 OR ボールカウント結果送信待ち
  running,
}

各デバイスは、このStateテーブルの変化をリアルタイムで検知し、状態を変化させます。

  • big_question_group_idにはプロジェクターが表示すべき大問セット(大問3つのグループ)のIDが入ります。
    ユーザーがいない場合はnullが入ります。
classroom33Common/lib/provider/big_question_storage.dart
class BigQuestionSet {
  BigQuestionSet({
    required this.id,
    required this.title,
    required this.questions,
    required this.category,
  });
  /// 大問グループID
  final int id;
  /// 大問グループのタイトル
  final String title;
  /// 大問の配列(3つ)
  final List<BigQuestionItem> questions;
  /// 大問グループのカテゴリ
  final QuestionCategory category;
}

usersテーブル

08B7E50E-9A7F-4AAA-93A9-54074021423D.jpeg

  • result: 各大問ごとの得点結果(JSON)
    大問は3つ × 小問3つの計9つの要素を持ちます。
    初期値は{ "items": [] }なので、itemsの要素が、0→3(大問1終了)→6(大問2終了)→9(大問3終了)と増えていく感じです。

    得点データ(JSON)の例
    {
      "items": [
        {
          "wrong_count": 0,
          "correct_count": 29
        },
        {
          "wrong_count": 0,
          "correct_count": 33
        },
        {
          "wrong_count": 7,
          "correct_count": 21
        },
        {
          "wrong_count": 13,
          "correct_count": 16
        },
        {
          "wrong_count": 16,
          "correct_count": 9
        },
        {
          "wrong_count": 16,
          "correct_count": 10
        },
        {
          "wrong_count": 14,
          "correct_count": 8
        },
        {
          "wrong_count": 14,
          "correct_count": 10
        },
        {
          "wrong_count": 0,
          "correct_count": 21
        }
      ]
    }
    
  • total_point: 合計得点
    大問3の結果送信時に合計得点の更新をします。大問3が終わるまで(初期値)はnullです。
    コード(ここらへんはネストが深かったり、UIとロジックの分離が全然できていないので 相当アウトなコードですが…)

classroom33controller/lib/page/on_running.dart
final result = QuestionsResult(
  items: <QuestionResult>[
    // 今までの結果
    ...user.result.items,
    // 新しく追加する配列
    ...ref.read(counterProvider.notifier).items,
  ],
);
final res2 = await supabase
    .from('users')
    .update(<String, dynamic>{
      'result': result.toJson(),
      if (result.items.length == 9)
        'total_point': result.items.map((e) => e.toPoint).reduce(
              (value, element) => value + element,
            ),
    })
    .eq('id', user.id)
    .execute();

dartのMap内でif使って条件分岐できるの、めちゃくちゃ気持ち良いですね

Git/GitHub

相変わらず便利です… 万が一パソコンが逝ったら… の心配をしなくて良いの 本当にありがたいですね。急いでコーディングしていたらコミット/プッシュするのをすっかり忘れていてあまり意味がなかったのですが
公開しちゃマズイコード(問題文をハードコーディングしちゃっていて、権利的に不安)を省いたものをこのRepoに置いておきました。気が向いたらご覧ください。
YumNumm/MarkSheetMania - GitHub

Codemagic

Macなんていうイケてるデバイスを持っていない私にとってまさに神のサービスです(去年からお世話になっています)
無料利用制限枠はあるものの、iOS向けにビルドをしたり、SSH/VNCしてMac Miniを操作出来ちゃうんです。本当にありがたいですね…(Mac欲しいよ〜〜!!!!)
Codemagicでビルドするとzipファイルで出力されるので、解凍した中身をPayloadフォルダ(新規作成)に入れて、Payloadフォルダをもう一度圧縮して、拡張子を.ipaに変えれば完成。あとは、Sideloadlyというサイドロード用のツールを使ってあげればiOSにインストールできます。(無料署名だと1週間で起動できなくなります。本質ではないので、詳しくは別の記事をお探しください。)

利用したデバイス

  • 開発機(緊急時対応)
    • Surface laptop2 (Windows 11 Corei5-8250U 8GB 私物)
  • Admin
    • iPad第8世代(iPadOS 15.0 私物)
    • Surface Pro 7(Windows 11 クラスメイト)
  • Projector
    • Surface laptop 2(Windows 10 クラスメイト)
    • Surface Pro 7(Windows 11 クラスメイト)
    • ThinkPad X11 Gen1(Windows 10 クラスメイト)
  • Controller (予備含む)
    • Xiaomi Mi Note 10(Android 12 私物)
    • Nexus 6P (Andorid 8.1.0 私物)
    • Moto g7(Android 10 私物)
    • Xperia X Performance(Android 8.0.0 私物)
    • iPhone 7(iOS 14.5 私物)
  • 結果表示
    • HP envy(Windows 11 クラスメイト)

開発の流れ

夏休み前 〜夢を見る〜

主に、システム設計を進めていました。当時の日記によるとB4ルーズリーフ 両面4枚くらい書いていたらしいですね。さすが授業中も準備を進める暇人!!(勉強? ヤバいです!!)
この時は、サーバサイドAPIもPythonかNode.jsあたりで実装して、クラス内にRaspberry Pi 4を置いてWiFi経由でAPIを叩く/WebSocket接続するようにするつもりでした。クラス内で全てが完結するので分かりやすいですし。
殴り書きです。まあ正直自分しか読まないので自分が読めればヨシ!

夏休み中 〜少しずつ前へ〜

学校の夏期講習(午前中のみ)が合計14日ありました。その日の午後数時間くらいをクラス企画の開発に費やしました。

夏休み前半

前述した通り、APIもオンプレミスで運用するつもりだったので、Node.jsでAPI建ててみたり 触ったことがなかったGoを学んでみたり…
でもやっぱりクラウドでやってみたくなってCloudflare Workers上で動くHonoというフレームワークを使ってみたり…

こういう、時間にあまり追われずに色々試行錯誤しながら開発して 色々吸収できる時間はとても幸せに感じます。(悪い言い方をすれば、右往左往しているわけですが)

この日はDartでAPIサーバを建てようとしていました

夏休み後半

この頃になってバックエンドも開発している時間が無くなってきた(正確にはそこに時間を割きたくなくなった)のでもうSaaSに任せようと、An open source Firebase alternativeを謳っていて、利用経験のあるSupabaseを使うことにしました。
PostgreSQLやKongが裏で動いていて、セルフホストもできます。3
Supabase Cloud(AWS上)の無料枠は制限があるので、超えそうになったらセルフホストすればどうにかなるだろ! の勢いで利用を決定しました。(結局余裕で無料枠に収まったので良かったです)
(ちなみに、なぜFirebaseを利用しなかったのかというとFlutter(Windows向けビルド)でも使いたかったからです。動くのかどうか詳しく分からないのですが、その検証に時間をあまり費やしたくなかったので却下しました。)

これは 進捗ヤバイ! になっていた時ですね

夏休み明け 〜限界開発界隈〜

放課後に準備をする人がだんだん増えてきて、「ついに文化祭が近づいてきたんだな…」と実感するよううになります。ちなみに、システムは全然実装が終わっていません。ようやくここで危機感を覚えます。放課後や家に帰ってからの時間を開発時間に割り当て、自分のフルパワーで開発を進めていきます。

文化祭前日準備 〜終わりが見える〜

この段階でようやくほとんど システムは完成しました。

クラス備え付けのプロジェクターで動作チェック


コントローラや結果表示の動作チェック

当日の状況

発生した問題と解決策

1. 数式がぶっ飛んでる!(1日目11時頃発生)

プロジェクターでは、極限記号や積分記号を利用するような数学の問題文も表示しないといけません。なので、LaTeXを表示できるライブラリ flutter_math_forkを利用していました。
問題文はなるべく大きな文字で表示したいですよね! 大きい方が読みやすいですもの!(プロジェクターの問題で解像度/輝度が低いという問題もありましたし)
そのために、数式表示のフォントサイズは1000にしてFittedBoxでラップしていました。これにより、横方向にはOverflowしないようになりました。ヨシ!(現場猫風)

お気づきでしょうか。これだと問題文が短い時に文字サイズがめちゃデカくなっちゃう!
そう。その通りです。僕は気がつきませんでした。

数学(高3向け)の大問3-1で問題が発生しました。問題文はこれです。

n=113n?


問題文が短いお陰で、問題文の一部・選択肢・プログレスバーが表示されない問題が発生しました。この問題を認知してから応急処置としてこの問題文を含む 数学-高3向けを選ぶのをやめてもらいました。
このスクリーンショットは1日目のお昼休憩(12時〜13時)に検証した時のものです。
8C6FAFD6-5E9E-4F37-AC56-8DC5C2819EE6.jpeg

「ウワー ヤッチモータ!!」な気分でしたね…これは…(答えは12です。)

対処

この問題をうまく解決する手法がなかなか思いつかなかったので、フォントサイズを一律で小さめにすることでGET KOTONAKIしました。プロジェクターに表示される文字が小さくて読みにくくなってしまいますが、仕方なし…(1日目12:50頃解消 - 午後の部開始まであと10分! 昼ご飯食べる時間なんてない! 食べません!)

(無意味なコードがいくつか紛れ込んでいますが、どうかお気になさらず…)
Flutterのレイアウト関連のWidgetに対する理解不足を深く実感しました。要勉強。
(Flutterで文字をうまく表示させる手段 知らないので早めに調べたい)

2. 選んだ問題と違う!!(2回発生)

1回目 Controllerの設定した大問位置が違う! の巻

Controllerは、自分の端末がどの大問に対して設定を更新するか(どのstateをいじるか)を起動時に選択します。しかし、何らかの問題でアプリが落ちたときに4私が焦って設定をミスってしまったようです…

対処

とりあえず、問題がズレてしまった時にライドに乗っていた人は登録からやり直してもらいました。
システム全体に関わる変更は指差し確認ヨシ!をするように徹底しました。
本質は、注釈にも書いたけれど 非同期処理を含むボタンの連打対策はしようね という話。
すぐにコードを修正してリビルドするのは無理だったので、シフトに入っているクラスメイトに「連打しないでくれ〜!」と口酸っぱく伝えておいた。GET KOTONAKI

2回目 Controllerのユーザ選択がズレている! の巻

Controllerは大問1つ終わるたびに 移動後のユーザの位置をデータベースに登録する設計になっています。(当初はデータベースが自動で位置移動処理を行うようにするつもりでしたがバグりそうだったので却下)
じゃあControllerは何を見てユーザーの位置登録をするのかというと各ユーザーが乗っているライドのIDと結果用紙です。ユーザー登録時にユーザーIDとライドIDをメモしてもらい、ライドの移動に合わせて紙も移動していき、Controllerは紙の情報とライドIDが一致しているかを確認してユーザーの位置登録をしてもらいます。
のはずだったのですが、何らかの影響で紙とライドの位置がズレてしまい、現場混乱。

シフトに入っているクラスメイト「なんか、結果用紙とライドIDが合致しないんだけど…」
ワイ「う〜ん 登録ミスっちゃったかな?
結果用紙が合っているはずなので、紙のを入力しちゃって!」
シフトに入っているクラスメイト「でも……」
ワイ「いいよ! 多分合ってる!」
(数分後…)
クルー「なんか問題がズレてるっぽいっす…」
ワイ「ギャー! なんで! なんで!(アッ)」

100%僕のせいでした…(本当に申し訳ない)

対処

1回目と同様に問題文がズレてしまった時にライドに乗っていた人はもう1周して登録からやり直し。
Controllerを操作しているクラスメイトには、登録前にユーザーIDがちゃんと連番になっているかを確認してもらうようにしました。(ユーザー登録時に発行されるユーザーIDは連番なので)
どうにかGET KOTONAKI
あと、私は障害発生時に落ち着いて対応することを肝に銘じました。急いじゃダメ ゼッタイ。


サーバ監視でお世話になっているGrafanaで得点分布を可視化してみました。数分でこういうことができてしまうのがデジタルのメリット!
最終的な来場グループ数は174、平均得点は673でした。
23CE03C5-C59C-4A2E-A6C3-7302DCE56707.jpeg

感想(ポエム)

正直、このシステム開発で犠牲にしたものは大きかったです。合計開発時間は59時間13分。(wakatime入れておいて良かった)
文化祭2日間も、ずっとクラスで障害対応、ログ監視、プログラム修正等していました。当然、他のクラス企画を見に行っているヒマなんぞありません。
文化祭が終わった日の夜に、友達が文化祭を全力で楽しんでいた話を聞いて、「自分も他のクラス企画を見て もっと楽しみたかった… なぜ自分だけが…」と悩む時もありました。
でも、この文化祭で全てを懸けて開発したからこそ見れた世界、新たな関わり、経験があったと思います。これは、全力でやらずに途中でサボっていたら得ることが出来なかったと思います。
そういう観点で言えば、私はこの学校の誰よりも全力で楽しむことができたと思います。

感謝

そう、感謝でいっぱいです。

  • この分かりにくく、バグりがちなシステムに対して誰一人として文句を言わずにシフトに入ってくれたクラスメイト
  • 文化祭準備後に一緒に帰り、楽しい話ができた 1年の頃からの友達
  • Fusion360を使って全体設計をした上に、ソフトウェア設計について議論してくれた⬛︎⬛︎⬛︎さん
  • 全てを仕切って、先導してくれたクラス企画幹部のみなさん
  • 感染対策をした上での文化祭実施を決断してくれた文化祭実行委員会の方々
  • 企画に来て、「スゴい!」「デザイン良いね!」と言ってくださった来場者のみなさん

    一見、一人で開発していたように思われたこのプロジェクトも多くの人に支えられていたことに気が付きました。
    全ての関係者に感謝しています。



おそらく、これが高校生活の中で最後に開発するアプリケーションです。
高校生のうちにプログラミングを始めておいて良かったと感じる時は、多々あります。
例えば、お金お金を稼ぐためではなく、純粋なスキルアップ/自分の欲しいソフトウェアを創ることを目的としたプログラミングができました。
私の場合、親の保護を受けています。なので、正直プログラミングをしていようが、していまいが、「生きられなくなる!」ほどの大きな影響を受けません。(要はプログラミングでご飯を食べているという訳でない)

しかし、大人になって社会に出たら、話は大きく変わると思います。プログラミングで生計を建てている人は、明日も、今後もご飯を食べ続けるためにプログラミングをしなければなりません。
プログラミングをする目的/目標が、お金を得ることになってしまっているのではないでしょうか?
まあ、お金が無いと生活できないので、それも当然…という感じなのでしょうか?
(どう表現すれば良いのだろう…語彙力不足…)

私は将来、お金を得るためではなく、多くの人の暮らしを便利にしたい 自分の欲しいソフトウェアを創る という目的/目標意識を持って、プログラミングと向き合っていきたいと思っています。
そして、これが 色々な会社さんを調べている内に「ここなら 成長し続けられる/スキルアップできる」と感じた株式会社ゆめみさん で働きたいと思っている理由の1つでもあります。
学生の今だからこその 目的意識、観点を持てたのは、大きなメリットだと思います。

最後の方はメンヘラポエムみたいな記事になってしまいました。
ここまで読んでいただきありがとうございます。感想等あったらコメントをぜひお願いします!


最後はデータベースいじって遊びました。

  1. 9/17で18歳の誕生日を迎えます。時が経つのは早いなあ あっという間だ… 誕生日プレゼントは常時受付中です!

  2. 中学卒業時に友達とディズニーランドに行く予定でした....が 無事にコロナウィルスのお陰でオジャンになりました。コロナ許さん!
    高校卒業したら行きたいよ〜!!!

  3. 冒頭で述べた地震観測・速報アプリケーション EQMonitorのバックエンドは、Supabase Cloudとセルフホストで処理を分散させています。

  4. 送信ボタンを2連打しちゃってasync処理の後にNavigator.of(context).pop();が2回コールされて画面が真っ黒になってしまったと推測。ボタン連打対策はしっかりしておくべき…(特にasync/await処理関連)

新規登録して、もっと便利にQiitaを使ってみよう

  1. ユーザーやタグをフォローできます
  2. 便利な情報をストックできます
  3. 記事の編集提案をすることができます
ログインすると使える機能について
YumNumm

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
新規登録
すでにアカウントを持っている方はログイン
17
どのような問題がありますか?
新規登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる
新規登録ログイン
ストックするカテゴリー