Fletで実践的なアプリ作成に向けて！セミナー運営サイトの設計・実装を進める


ここ1カ月くらい、**Flet**というFlutter × Pythonのライブラリを使った開発を最近しばしばやっていまして、QiitaでFlet関連の記事を数本投稿しています。

**公式サイト**

https://flet.dev/docs/

今回の記事は、以下の続きとなりますが、前回の内容は確認いただかなくても、読める内容だと思います！！前回は、[**Google認証と表示制御の機能**](https://qiita.com/eno49conan/items/a79b2a29fee08e249a27)を実装してみました。

さて、`設計`とタイトルに入れたことでハードルを感じる方が、いらっしゃるかもしれませんが、今回の場合、`設計`というとちょっと大袈裟では？と思っている部分もあります。**どんな機能をつけようかな～～、その機能をどうやって実装しようかな～～**と考える作業をしました！

今回は各画面の機能を実装してみました。題材としたのは「**セミナー運営サイト**」です。勉強会に参加された経験がある方はご存知かもしれませんが、conpassがその一例ですね。
（UIまで真似るというよりは、機能をそれっぽいものにする、といった気持ちで始めました）

https://connpass.com/

# 今回作成した機能
- **トップ画面**
    - 参加一覧セミナーを5件くらい表示（もっと確認する、をクリックすると設定画面へ移動）
    - お知らせ一覧
- **セミナー一覧画面**
    - 現在どういったセミナーの開催が予定されているか、カード形式で一覧表示（検索機能も実装予定）
- **セミナー作成画面**
    - 入力フォームを作成し、入力内容を確認するモーダルを表示して、内容に問題が無ければ、バナーを表示する
- **設定画面**
    - 主催セミナー一覧、参加予定セミナー一覧、アカウント削除、といった3つの項目をタブ切り替えで表示する形
:::note warn
**テーブル定義について**
今回機能を作成していますが、このセミナーアプリのためのテーブル定義・作成は行っていません。ある程度機能が固まった段階で実施予定です。（実際の現場とは順番が異なると思います）
ちなみに今回、トップ画面で表示しているデータはDBから取得していますが、別作業で既に作成済だったcloudflareのD1上に作成したテーブルを利用しています。

:::

## 実装にあたり
:::note

Fletというライブラリはまだまだ機能が追加されているライブラリです。作成時のバージョン`0.7.4`です。
ある程度の規模のアプリケーションを作成することで、実現しやすさや、どういったことをするとエラーが発生するのか、といったことを知りながら・感じながら作成していました。

**実装を通じてどういったことを感じたのか、各画面の機能の話をした後に、記載していますので、よろしければ見てみてください！**

:::

## トップ画面
上半分（60%）には参加予定のセミナーを5件表示、
下半分（40%）はお知らせを表示、
といった形で上下で機能を分割する実装を取り入れてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/2102d60e-bd7d-b3a5-dfd3-a048e5441259.png)
40%と60％という分け方は、`expand`というプロパティを調整することで実現できます。
コードを見ていただきますが、`expand=6`と`expand=4`という記述を確認できます。これにより、それぞれの高さを確保しています。
（土台に`Column`を使用しているので、高さの確保です。`Row`を使用した場合は、幅の確保に変わります。）
```python
class Top():
    def __init__(
        self,
        page: Page,
        result: list,
        * args,
        **kwargs,
    ):
        super().__init__(*args, **kwargs)
        self.page = page
        # 画面表示の土台
        self.contents = Column(expand=True, auto_scroll=False)
        # 画面の60%を占める要素
        lv = ft.ListView(expand=6, spacing=15, auto_scroll=False)
        # ・・・略・・・
        self.contents.controls.append(lv)
        # 区切り線（高さ1なので、占める割合はわずか）
        self.contents.controls.append(Divider(height=1, thickness=3))
        # 画面の40%を占める要素（区切り線分は削られるが・・・）
        self.contents.controls.append(Text("お知らせ", expand=4, size=24))
```
また、スクロールして5件表示後、「もっと確認する」というボタンが見えてきます。これをクリックすると、、、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/515e1704-2bdf-81c4-5a18-5452578ac4f1.png)
以下の別タブに移動するような機能を入れてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/b1dea5e2-cc20-3349-1542-c0cc3d04561a.png)

## セミナー一覧画面
スクロールして、カード一覧を確認できる状態にしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/d86e5b72-bd1d-acde-a1d4-7ed53f1fcfd2.png)

#### 無限スクロール or ページング
現状、無限スクロール機能を入れていますが、暫定的な対応です。
ある位置までスクロールした場合に、同じCardを10枚追加表示する処理にしていますが、DBから取得していませんので、これといった影響はないかもしれませんが、無限スクロールにもメリット・デメリットがあります。
「ページの読み込み速度が低下する」「情報の再発見が難しい」といった点を踏まえて、今後はページング形式に修正する予定です。

**無限スクロールについて**
無限スクロールの知識事項は以下記事を参考にしています。

https://rekaizen.com/article/detail/hp/16900

https://u-site.jp/alertbox/infinite-scrolling-tips

#### 検索機能
この記事公開段階では、まだ実装できていませんが、この後実装を進めます。
現状は、条件を絞らず全件表示された状態です。
セミナー名・開催日時・ジャンルといった項目でフィルターする機能を実装予定です。

## セミナー追加画面
:::note 

**今回の記事では、この画面に関する説明の割合が高めです。**
「セミナー追加画面」は、多少本格的に実装をしてみました。
以下の機能を眺めていただくとわかりますが、色々実装しています。

- 画面項目の実装
    - 「画面項目実装」で記載する各項目の実装
- バリデーション機能とメッセージ表示
    - 各入力項目に対する制約実装と画面への即時反映のための実装
- モーダル機能で内容確認
    - 入力内容を確認する方法として採用
- バナー表示
    - モーダル画面で作成を実行した場合にのアクションとして、採用

現状、処理や画面要素ごとに空行はありますが、
**空行を含めて500行くらいの実装となっています。もう少し分割できるような実装ができないか、今後の追加で調べてみてもいいかと思っているところです！**
:::

#### 画面項目実装
現状、最低限必要となりそうな項目しか用意していません。以下の項目があれば、
セミナー開催に必要な情報はそろうと思っています。
- セミナー名
- 概要
- 参加人数
- ジャンル（Python,Docker,AWSなど）
- 開催日時
- 公開設定（下書き中か否か）

以下のようなデザインになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/2f475607-a8a5-9533-22d7-1404ffdfb6ee.png)

#### バリデーション機能とメッセージ表示
##### 即時チェック or 送信時チェック
要件によって、どちらを選択するのか、ケースバイケースだと思います。
（入力中にチェックしてエラーメッセージが表示されるのはちょっと・・・）という方もいらっしゃると思います。
逆に送信時のみバリデーションチェックが実施されて、（えっ？違うの？）となり、メッセージを確認して修正が面倒という方もいます。

今回は、自分はどっちが使いやすいと感じるか、という観点で、即時チェックを採用することにしました。
また確認ボタンについて、各入力値が条件を満たしていない場合は、非活性状態にしてあるので、想定外の値が送信されることはまずないです。

##### 日付・時間項目の実装
以下の実装方法がよくあるパターンではないでしょうか。
1. **カレンダー形式**
    1. 存在しない日付を入力できない状態にする
1. **プルダウン形式**
    1. 2月30日など、他の月では存在する日付を、設定した場合に不正とする実装
1. **テキストフィールドに直接入力**
    1. フォーマットチェック・存在日付チェックなどを実装

それぞれ見ていきます。
> 1.**カレンダー形式**

ユーザさん視点で言えば一番手間がかからないパターンだと思います。
残念ながら、Fletにはまだカレンダー形式Inputは用意されていないので使えません。。。

> 2.**プルダウン形式**

ユーザさん視点で言えばカレンダーよりは手間がかかりますが、選択肢が限定されているので、存在しない日付を入力することはありません。
日付情報を固定値で管理し、画面表示の際に呼び出す構造になりそうです。

> 3.**テキストフィールドに直接入力**

ユーザさん視点で言えば一番手間のかかるパターンかもしれません。
実装側としては、存在しない日付のチェックも必要です。

選択肢としては、**2**か**3**ですが、今回は本格的なサービスに持ち込むことはないので、**3**を選択し、自分も多少は苦労してみることにしました。
以下の記事を参考に進めました。
**文字列日付に変換する処理をtry-exceptで行い、exceptが発生した場合は、存在しない日付と判断するといった処理です。**

https://joshi-engineer.com/python%E3%81%A7%E6%97%A5%E4%BB%98%E3%81%AE%E5%85%A5%E5%8A%9B%E3%83%BB%E6%97%A5%E4%BB%98%E5%A6%A5%E5%BD%93%E6%80%A7%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF

色々実装を行い、以下のようにバリデーションチェックが入力直後に実行され、
不正な値がある場合は、確認ボタンが非活性のままになるようにしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/ed2a4fa6-ddc0-7faf-be46-984311f447fa.png)


#### 送信前のモーダル表示
以下、チュートリアルのリンクです。

https://flet.dev/docs/controls/alertdialog

以下実装がチュートリアルを抜粋したものです。**画面構築時にUIを構築し、以降表示内容は更新できません**。（変数を使っても、値が空の状態でUI構築）
画面表示後の操作で変数に代入した値をモーダルで表示したい場合は、構造を変える必要があります。
```python
def main(page: ft.Page):
    page.title = "AlertDialog examples"

    def close_dlg(e):
        dlg_modal.open = False
        page.update()

    dlg_modal = ft.AlertDialog(
        modal=True,
        title=ft.Text("Please confirm"),
        content=ft.Text("Do you really want to delete all those files?"),
        actions=[
            ft.TextButton("Yes", on_click=close_dlg),
            ft.TextButton("No", on_click=close_dlg),
        ],
        actions_alignment=ft.MainAxisAlignment.END,
        on_dismiss=lambda e: print("Modal dialog dismissed!"),
    )
ft.app(target=main)
```
チュートリアルの構造のまま、進めて動作確認すると、何も表示されない・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/575e79ae-4f04-7244-dbc3-6e107b1f33d7.png)

`確認`ボタンクリック時に実行する処理で、UIを再構築するように手を加えます。`確認`ボタンをクリック時に呼び出す`open_dlg_modal`メソッド内で、実行の度にモーダル表示内容の再構築を行います。
```python
# 確認ボタンクリック時のイベント
def open_dlg_modal(e):
    self.page.dialog = ft.AlertDialog(
        modal=True,
        title=ft.Text("Confirm Create Seminar"),
        # 確認ボタン実行の度にUI構築
        content=ft.Container(
            height=200,
            width=300,
            content=Column(
                [
                    Row([
                        Text("セミナー名："),
                        Text(seminar_name_textfield.value)
                    ]),
            # ・・・略・・・
        )
    )

# 確認ボタン
submit_button = ft.ElevatedButton(
    disabled=True,
    text="確認", on_click=open_dlg_modal)
```
以下のように、入力情報が確認できる状態になりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/c7b07b4d-59f8-e64c-bd77-a836766e5fd7.png)

入力内容を確認します。
・問題がない場合：`Create Seminar!`をクリック
・修正したい場合：`Fix`をクリック
`Fix`をクリックした場合、モーダルが非表示となって、入力を受け付けます。

#### バナー表示
`Create Seminar!`をクリックした場合に、入力値は空欄に戻すようにしていますが、それだけでは、モーダルが閉じただけ？となるかもしれません。
`Create Seminar!`をクリックしたら、画面上部からバナーを表示し、作成できたこと通知します。
スナックバー、という選択肢はありましたが、画面の下から少し表示されるだけだったので、見逃すかな～～と思って、バナーにしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/642ef7da-c3ee-7955-b083-8ac87b226534.png)
**ここまで画面のスクリーンショットを見ていただきましたが、実装も少し見ていただこうと思います。**
全部は追うのは大変なので、参加人数関連の機能について、実装を記載しました。
イメージを掴んでいただけたらと思います。
```python
# 画面の要素を設定する土台を定義
self.contents = Column(expand=True, auto_scroll=False)
# 確認ボタンの活性状態制御
def update_disable_submit_button():
    # 入力前の段階では非活性確定
    for field in [participates_textfield,XXX.YYY]:
        if field.value == "" or field.value is None:
            return
    for valid_state in [invalid_msg_participates,XXXX]:
        if valid_state.visible:
            return
        else:
            submit_button.disabled = False
    self.page.update()
# バリデーションメッセージ
invalid_msg_participates = Row(
    [
        ft.Text("参加者は0より大きい数で入力してください",
                size=20, color=ft.colors.RED_500
                )
    ],
    visible=False
)

# 入力情報更新時の処理
def change_participates(e):
    invalid_msg_participates.visible = True
    try:
        if int(participates_textfield.value) > 0:
            invalid_msg_participates.visible = False
    except ValueError:
        pass
    self.page.update()
    update_disable_submit_button()

# テキストフィールド
participates_textfield = ft.TextField(
    keyboard_type=ft.KeyboardType.NUMBER,  # for mobile option
    height=50,
    width=200,
    cursor_height=20,
    label="number", on_change=change_participates)

# 参加人数に関する要素をまとめて、Columnに設定
participates_area = Column([
    Container(content=Text("参加人数", size=20),
              margin=ft.margin.symmetric(
        horizontal=20,)
    ),
     Container(
        content=participates_textfield,
        margin=ft.margin.only(
            left=30, top=10)
    ),
     Container(content=invalid_msg_participates,
              margin=ft.margin.symmetric(
                  horizontal=30)
              )
])

# 確認ボタン
submit_button = ft.ElevatedButton(
    disabled=True,
    text="確認", on_click=open_dlg_modal)
# スクロールできるように、ListViewを定義し、各要素を追加
lv = ft.ListView(expand=True, spacing=15, auto_scroll=False)
lv.controls.append(
    Column(
        [                  
            participates_area,  # 参加人数
            Container(content=submit_button,
                      margin=ft.margin.only(right=25, bottom=25),
                      alignment=ft.alignment.center_right
                      ),
        ],
    ))
# 画面の表示要素として、ListViewを追加
self.contents.controls.append(lv)
```
## 設定画面
こちらは、複数の機能を実装し、タブを使って、表示を切り替える方法で作成してみました。公式ドキュメントでいうと、以下の部分が該当しています。

https://flet.dev/docs/controls/tabs

表示項目としては、以下を用意してみました。
- **参加予定のセミナー一覧**
- **主催予定のセミナー一覧**
まだ固定表示ですが、後々データベースから情報取得に切り替えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/955db672-ea89-fd54-0b26-7805da4b7979.png)
- アカウント削除
こちらはテーブル定義が固まってから、処理を追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/80141cf1-2f12-5e6f-2a95-8d4e537184f8.png)

## 色々実装して見えてきたこと
##### 全体的な話
冒頭に記載した感じたことについて、記載しました。
- 個人開発レベルではあるが、Flutterで既に開発をやっていたので、それほど戸惑いなく実装ができました。（`Streamlit`経験者もそれほど戸惑わずにできる気がします）
- **土台次第で開発しやすさが変わる**
    - サンプルアプリケーションを土台にしましたが、結果的にはその土台のおかげで（せいで）機能作成が左右される結果となりました。**実は前回と今回の間で、実は土台を途中で変えることも経験しました**。
どういったレイアウト・機能の場合のときに、どういった土台を使えば、よりFletを活かせるか調べつつやるのがいいと思いました。（Fletに限った話ではないですが。。。）
- **パスが変わったときにどんな処理を行うか**
パス（route）が変わる度にある処理が実施できるのですが、その部分でどういった処理を実装するか、そこがアプリケーション全体を構成する上で重要になってきそうです。
- 画面サイズを超えてしまった場合の挙動
    - 設定次第でFlutterと同じ形で実現できる日がくるかもしれないですが、pixelがオーバーしてても、Fletはそれを教えてくれません。
■Flutter：黄色と黒の警告が表示され、アプリケーションが機能しなくなります。（画像参照）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/c1f8963e-1d3e-ea83-570e-47fcb530782e.png)
■Flet：はみ出した部分が表示されないだけで、エラーは特に表示されません。

#### より細かい実装に関する気付き
- `ft.Page`のプロパティ(page.XXX)を知ることで様々な実装ができそうです。（自力で頑張らなくても、ある機能を実現できる）
- Tabsでタブごとに表示切替をする場合に、各タブの表示内容だけスクロールすることはどうやらできない（各タブのラベルも含めて、スクロールすることになる）
- Flutterと同様にViewを重ねることができて、clearしたりpopしたりができるが、土台の構築方法次第では、思ったように機能させるまでに時間を要したり、既存実装の修正を余儀なくされる可能性がある

## 今後
セミナーを新しく作成する機能や各画面のレイアウトをはじめ、DBからのデータ取得処理自体は出来上がったので、
以降は、以下のような機能の実装が必要になりそうです。
- DBテーブルの定義（今回は、以前作成したテーブルからデータを取得してきただけ）
- セミナー参加申し込み
- セミナー検索
- アカウント削除

多少時間はかかるかもしれませんが、コツコツと進めていきたいです。