Fletを試す(3) - 画面遷移（ルーティング）の調査

# はじめに

PythonでFlutterアプリが作れるフレームワーク「Flet」の検証をしました。
簡単なツールは1画面で済むときもありますが、ある程度のアプリを作るとなると2画面以上になると思います。
そうなると画面遷移が必要になるので、画面遷移（ルーティング）の実装方法を調査しました。

Flet公式サイトにおけるルーティングの説明はこちら。
https://flet.dev/docs/guides/python/navigation-and-routing

参考）過去記事はこちら。

https://qiita.com/donraq/items/5182e3f01bd177bb6ff0

https://qiita.com/donraq/items/043bde2e480d5359261b

# 前提事項

- 「Flet」に関する詳細は省略
- インストール方法、実行方法、デプロイ方法なども省略
- 試した環境
    - Windows10
    - Python 3.11.1
    - Flet 0.3.2

# 概要

- Fletの画面遷移（ルーティング）の仕様を確認
- 実装としては2パターンあると思ったので、2パターンのサンプルを作成
- パターン1：画面間を単純に移動する方式
- パターン2：画面に階層を持たせて進んだり戻ったりする方式（上に画面を被せて行くイメージ）

今回のソースの最終形をFly.ioにアップしたのがこちら。
https://flet-report03.fly.dev

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273897/65a36ca5-997d-f7dc-8c55-9c8f5730db9c.gif)


# 詳細

## Fletの画面遷移（ルーティング）の仕様

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273897/fb3c429c-352a-cb45-484b-aec05080f9e4.png)

↑が公式サイトの図ですが、画面表示の仕組みとしてはまさにこの図の通りです。
まずPageがあって、その中にViewが複数入っているイメージ。
Viewは最初がRootで、Viewを追加すると上に被さっていくイメージで、一番上のViewが見えている状態になります。
スタックのイメージだと思うのですが、popで一番上のViewが外れてその下の画面が表示されます。

## パターン1：画面間を単純に移動する方式を実装

1つ目は、敢えてシンプルに1つのViewを表示するだけで画面を切り替える方法を実装しました。

サンプルでは、view1とview2を作成する関数を用意し、ボタンクリックでviewを切り替えるようにしてあります。
view切替は、`page.views.clear()`でクリアして`page.views.append()`でviewを追加するだけ。
これにより、元の画面が消えて新たな画面が表示されます。

流れとしては以下です。
`page.go()`を呼ぶ→`page.on_route_change`に設定した関数が実行される→その中で画面切り替えする

動作イメージは↓。
![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273897/734a0737-6266-1e1f-bd26-f9706a391d3c.gif)

サンプルソースは↓。

```python
import flet as ft


def main(page: ft.Page):

    def create_view1():
        return ft.View("/view1", [
            ft.AppBar(title=ft.Text("view1"),
                      bgcolor=ft.colors.BLUE),
            ft.TextField(value="view1"),
            ft.ElevatedButton(
                "Go to view2", on_click=lambda _: page.go("/view2")),
        ])

    def create_view2():
        return ft.View("/view2", [
            ft.AppBar(title=ft.Text("view2"),
                      bgcolor=ft.colors.RED),
            ft.TextField(value="view2"),
            ft.ElevatedButton(
                "Go to view1", on_click=lambda _: page.go("/view1")),
        ])

    def route_change(handler):
        troute = ft.TemplateRoute(handler.route)
        page.views.clear()
        if troute.match("/view1"):
            page.views.append(create_view1())
        elif troute.match("/view2"):
            page.views.append(create_view2())
        page.update()

    # ルート変更時のロジック設定
    page.on_route_change = route_change

    # Page レイアウト
    page.title = "Navigation and routing"
    # 初期表示
    page.go("/view1")


if __name__ == "__main__":
    ft.app(target=main)
```

## パターン2：画面に階層を持たせて進んだり戻ったりする方式（上に画面を被せて行くイメージ）

2つ目は、Fletの仕様にあるViewを上に被せていくイメージの画面遷移を実装をしました。

こちらは`page.views.clear()`しないで`page.views.append()`することで、新しいviewが表示されます。
画面に`AppBar`を配置しておくと自動的に左端に戻るのアイコンが出てきます。
戻るクリック時の処理は`page.on_view_pop`に関数を割り当てて書いておく必要があります。

戻るときの流れは以下。
戻るクリック→`page.on_view_pop`に設定した関数が実行される→その中で`page.views.pop()`する

動作イメージは↓。
![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273897/22555bf9-e06d-b7bd-8267-f0c8a5f9d001.gif)

サンプルソースは↓
```python
import flet as ft


def main(page: ft.Page):

    def create_view1():
        ・・・上と同じなので省略・・・

    def create_view2():
        ・・・上と同じなので省略・・・

    def route_change(handler):
        troute = ft.TemplateRoute(handler.route)
        if troute.match("/view1"):
            page.views.append(create_view1())
        elif troute.match("/view2"):
            page.views.append(create_view2())
        page.update()

    # ルート変更時のロジック設定
    page.on_route_change = route_change

    def view_pop(handler):
        page.views.pop()  # 1つ前に戻る
        page.go("/back")
        # page.update()
        # update() だと route が変更されない。
        # そうなると1つ戻ってまた進むことができなくなるので go("/back") で回避。不具合？

    # 戻る時のロジック設定
    page.on_view_pop = view_pop

    # Page レイアウト
    page.title = "Navigation and routing"
    # 初期表示
    page.views.clear()
    page.go("/view1")


if __name__ == "__main__":
    ft.app(target=main)
```

:::note info
メモ
page.go("/back") については、コメント記載の通り、暫定的な対処です。
もっと良い方法があるかもしれませんし、もしかしたら書き方に問題があるだけかもしれません。
:::

## ソースの最終形

2パターンを一緒にまとめた形にしたソースがこちら。
動作イメージは概要に貼ってある通りです。

<details><summary>サンプルソースはこちら。</summary>

```python
import flet as ft


def main(page: ft.Page):

    def create_view(route: str, title: str, next_route1: str, next_route2: str, color: ft.colors):
        return ft.View(route, [
            ft.AppBar(title=ft.Text(title), bgcolor=color),
            ft.TextField(value=title),
            ft.ElevatedButton(
                f"{next_route1} へ移動", on_click=lambda _: page.go(next_route1)),
            ft.ElevatedButton(
                f"{next_route2} を被せる", on_click=lambda _: page.go(next_route2)),
            ft.ElevatedButton(
                f"Root に移動", on_click=lambda _: page.go("/")),
        ])

    def route_change(handler):
        troute = ft.TemplateRoute(handler.route)
        if troute.match("/"):
            page.views.clear()
            page.views.append(
                create_view("/", "Root", "/view1", "/cover/view1", ft.colors.BLUE))
        elif troute.match("/view1"):
            page.views.clear()
            page.views.append(
                create_view("/view1", "View1", "/view2", "/cover/view2", ft.colors.RED))
        elif troute.match("/view2"):
            page.views.clear()
            page.views.append(
                create_view("/view2", "View2", "/view1", "/cover/view1", ft.colors.RED))
        elif troute.match("/cover/view1"):
            page.views.append(
                create_view("/cover/view1", "View1", "/view2", "/cover/view2", ft.colors.GREEN))
        elif troute.match("/cover/view2"):
            page.views.append(
                create_view("/cover/view2", "View2", "/view1", "/cover/view1", ft.colors.YELLOW_800))
        page.update()

    # ルート変更時のロジック設定
    page.on_route_change = route_change

    def view_pop(handler):
        page.views.pop()  # 1つ前に戻る
        page.go("/back")
        # page.update()
        # update() だと route が変更されない。
        # そうなると1つ戻ってまた進むことができなくなるので go("/back") で回避。不具合？

    # 戻る時のロジック設定
    page.on_view_pop = view_pop

    # Page レイアウト
    page.title = "Navigation and routing"
    # 初期表示
    page.go("/")


if __name__ == "__main__":
    ft.app(target=main)

```
</details>

以上です。