PR

Fletで画面遷移が動かない理由|page.go廃止とft.View引数順の罠、TemplateRouteの使い方

Python

前回、Fletのレイアウトがエラーもコンソール警告も出さずに崩れる、という話を書きました。

🥕 前回の記事 Fletのレイアウトが崩れてもエラーが出ない理由|Row/Column・expand・ResponsiveRowで直す方法 AIがよくやるexpand忘れ・ネスト崩壊のパターンと、ResponsiveRow・scrollを使った直し方をまとめました。 →

今回は画面遷移(ルーティング)を試したのですが、ここでも同じ「エラーが出ないのに動かない」パターンに出会いました。AIに「設定画面への遷移を追加して」と頼むと、page.go("/settings")を使ったコードが返ってきます。ところがFlet 1.0でこれを実行すると、そもそもpage.goというメソッド自体が存在しません。さらに厄介なことに、これを素直に直したつもりのコードでも、画面が真っ白になって固まることがありました。

この記事では、Fletの画面遷移まわりでFlet 1.0が変えた3つのポイントと、実際に踏んだ「エラーなしで真っ白になる」不具合の原因、正しい直し方までをまとめます。

この記事で分かること

  • Fletで画面遷移を実装する基本(page.views・ft.View・on_route_change)
  • page.go()がFlet 1.0で完全に削除されている件
  • ft.Viewの引数順序が変わり、エラーなく画面が真っ白になる罠
  • 初期表示をpage.navigate(page.route)任せにすると何も起きない理由
  • ft.TemplateRouteで動的ルート(/customers/:id)を扱う方法
  • 顧客一覧→詳細ページを例にしたBefore/Afterの直し方

スポンサーリンク

Fletのpage/route入門:正しい画面遷移の書き方

Fletで複数の画面を切り替えるには、page.viewsというリストにft.Viewを積んで、URLの変化(page.route)に応じて中身を入れ替える、という仕組みを使います。まずは正しく動く最小構成から見てみます。

import flet as ft
from nav_common import setup_page


def main(page: ft.Page):
    setup_page(page, "Route Basics")

    def route_change():
        page.views.clear()
        page.views.append(
            ft.View(
                route="/",
                controls=[
                    ft.Text("ホーム画面", size=22, weight=ft.FontWeight.BOLD),
                    ft.Text("page.route = " + page.route, size=13, color=ft.Colors.GREY_600),
                    ft.Container(height=16),
                    ft.Button(
                        "設定画面へ",
                        icon=ft.Icons.SETTINGS,
                        on_click=lambda e: page.navigate("/settings"),
                    ),
                ],
                padding=30,
            )
        )
        if page.route == "/settings":
            page.views.append(
                ft.View(
                    route="/settings",
                    controls=[
                        ft.Text("設定画面", size=22, weight=ft.FontWeight.BOLD),
                        ft.Text("page.route = " + page.route, size=13, color=ft.Colors.GREY_600),
                        ft.Container(height=16),
                        ft.Button(
                            "ホームへ戻る",
                            icon=ft.Icons.ARROW_BACK,
                            on_click=lambda e: page.navigate("/"),
                        ),
                    ],
                    padding=30,
                )
            )
        page.update()

    page.on_route_change = route_change
    route_change()


ft.run(main, view=ft.AppView.WEB_BROWSER, port=8580)

nav_commonのsetup_pageは、これまでの記事と同じく共通の初期設定をまとめた補助関数です。本記事のコードでは検証環境固有のフォント設定は省いており、手元の環境では日本語表示に問題は起きません。

import flet as ft


def setup_page(page: ft.Page, title="Flet Navigation Demo", bgcolor=None):
    page.title = title
    page.theme = ft.Theme(color_scheme_seed=ft.Colors.INDIGO)
    page.bgcolor = bgcolor or ft.Colors.WHITE

ボタンを押すと、ホーム画面と設定画面が正しく切り替わります。

ホーム画面 page.route = /
▲ ホーム画面。page.route = /
設定画面 page.route = /settings
▲ 「設定画面へ」を押した後。page.route = /settings に変わっている
  • ft.View(route=..., controls=[...]): 1つの画面を表す部品。どのURL(route)でどの中身(controls)を出すかをセットで持つ
  • page.on_route_change: page.routeが変化するたびに呼ばれる関数を登録する
  • page.navigate("/settings"): URLを変更し、on_route_changeを発火させて画面を切り替える

CustomTkinterやtkinterには無い、Webアプリらしい仕組みです。ここまでは素直に動くのですが、AIが出しがちなコードには、Flet 1.0で変わった3つのポイントが引っかかることがあります。


スポンサーリンク

核心①:page.go()はFlet 1.0で完全に削除されている

旧バージョンのFletでは、画面遷移にpage.go("/settings")という書き方が定番でした。ネット上の情報にはこの書き方がまだ数多く残っているため、AIも当然のようにこの形で出してきます。

import flet as ft
from nav_common import setup_page


def main(page: ft.Page):
    setup_page(page, "Nav Old Style (page.go)")

    def route_change(e):
        page.views.clear()
        page.views.append(
            ft.View(
                "/",
                [
                    ft.Text("ホーム画面"),
                    ft.Button("設定へ", on_click=lambda _: page.go("/settings")),
                ],
            )
        )
        page.update()

    page.on_route_change = route_change
    page.go(page.route)  # Flet 1.0には存在しないメソッド


ft.run(main, view=ft.AppView.WEB_BROWSER, port=8581)

Flet 1.0のPageクラスを調べると、hasattr(ft.Page, "go")はFalseです。実行すると、ブラウザ画面に赤い「Error running app」バナーでエラー内容がそのまま表示されます。

page.goのエラーバナー
▲ page.goを実行した結果。「’Page’ object has no attribute ‘go’」というエラーバナーが画面に出る

ここは親切な部類です。画面に直接エラー文が出るので、気づくのは難しくありません。後継のメソッドは、同期コールバック用のpage.navigate()と、async用のpage.push_route()です。この記事ではpage.navigate()を使います。


スポンサーリンク

核心②:ft.Viewの引数順序が変わり、エラーなく中身がすり替わる

ここからが今回の核心です。ft.Viewのコンストラクタを実際に調べると、第1引数がcontrols、第2引数がrouteでした。以前のバージョンではrouteが先だったため、多くのチュートリアルやAIの出力に見られるft.View("/", [...])という書き方は、Flet 1.0ではそのまま使えません。

import flet as ft
from nav_common import setup_page


def main(page: ft.Page):
    setup_page(page, "Nav View Arg Swap")

    def route_change():
        page.views.clear()
        page.views.append(
            ft.View(
                "/",  # 1番目の位置引数のつもりが、実際はcontrolsに入ってしまう
                [
                    ft.Text("ホーム画面"),
                    ft.Button("設定へ", on_click=lambda e: page.navigate("/settings")),
                ],
            )
        )
        page.update()

    page.on_route_change = route_change
    route_change()


ft.run(main, view=ft.AppView.WEB_BROWSER, port=8582)

型チェックも例外も発生しません。試しにPythonのインタラクティブシェルで中身を確認すると、routeとcontrolsがそっくり入れ替わっていることが分かります。

>>> v = ft.View("/", [ft.Text("hi")])  # 昔ながらの書き方
>>> v.route
[Text(value='hi', ...)]  # controlsのリストが入っている
>>> v.controls
'/'  # route文字列が入っている

このコードでアプリを起動すると、画面には何も表示されません(グレーの何もないキャンバスのまま止まります)。

ft.Viewの引数入れ替わりで何も表示されない画面
▲ 実行結果。画面には何も描画されない

ターミナルにもブラウザの開発者コンソールにも、エラーや警告は一切出ません。開発者コンソールを”error”・”route”・”view”のキーワードで検索しても、該当するメッセージは0件でした。

route/controls入れ替わりとコンソール検証結果の図解
▲ 検証結果を図解したもの。Pythonでのroute/controls入れ替わりの確認と、開発者コンソールでの検索結果(0件)をまとめている

前回のレイアウト崩壊編で見つけた「エラーが出ないまま崩れる」というFletの特徴が、ルーティングでも同じ形で現れた形です。直し方は、位置引数ではなくキーワード引数で明示することです。

ft.View(
    route="/",
    controls=[...],
)

route=とcontrols=さえ書いておけば、バージョンによる引数順の違いに影響されません。


スポンサーリンク

核心③:初期表示をpage.navigate(page.route)任せにすると何も起きない

route=・controls=を正しく直しても、もう一つ引っかかるポイントがあります。旧APIでは、起動時にpage.go(page.route)を呼んで最初の画面を描画する書き方が定番でした。これを素直にpage.navigate(page.route)に置き換えても、期待通りには動きません。

import flet as ft
from nav_common import setup_page


def main(page: ft.Page):
    setup_page(page, "Nav navigate() no-op")

    def route_change(e=None):
        page.views.clear()
        page.views.append(
            ft.View(
                route="/",
                controls=[
                    ft.Text("ホーム画面"),
                    ft.Button("設定へ", on_click=lambda e: page.navigate("/settings")),
                ],
            )
        )
        page.update()

    page.on_route_change = route_change
    page.navigate(page.route)  # 同じルートへのnavigateはon_route_changeを発火させない


ft.run(main, view=ft.AppView.WEB_BROWSER, port=8583)
navigate no-opで何も描画されない画面
▲ 実行結果。こちらも画面には何も描画されない

route_change関数の中にprintを仕込んで確かめたところ、この関数自体が一度も呼ばれていませんでした。page.routeはすでに"/"になっているため、page.navigate(page.route)は「ルートを変化させない呼び出し」になり、on_route_changeが発火しないというわけです。

Flet公式のpush_routeのdocstring内サンプルを確認すると、正しい書き方は「初期表示はルート変更用の関数を自分で直接1回呼ぶ」というものでした。1つ目のサンプルコード(route_basics)のように、page.on_route_change = route_changeの直後にroute_change()を素直に呼び出すだけで解決します。


スポンサーリンク

AIへの丸投げで画面遷移を直す4ステップ

ここまでの内容を踏まえて、AIが生成した画面遷移のコードを手元で直していく一連の流れを4ステップのアニメーションにまとめました。

AIに丸投げから画面遷移を直すまでの4ステップ
▲ AIに丸投げ→画面が真っ白→原因特定→修正までの流れ

途中経過のうち、崩壊した瞬間と直った後の画面だけを静止画でも載せておきます。

STEP2/4 実行すると画面が真っ白になる
▲ STEP2/4 実行すると、画面が真っ白になる
STEP4/4 画面遷移が正しく動くようになる
▲ STEP4/4 画面遷移が正しく動くようになる

ポイントは、AIが出したft.View("/", [...])のような位置引数のコードを見たら、まずroute=・controls=のキーワード引数に書き換えることです。プロンプトを工夫する場合は、「Flet 1.0の書き方で、ft.Viewはroute=とcontrols=を明示して」のように具体的に指定すると、この手の入れ替わりを避けやすくなります。


スポンサーリンク

実践:顧客一覧→詳細ページをBefore/Afterで直す

最後に、ここまでの内容をまとめて、「顧客一覧から詳細ページへ遷移する」業務アプリ風の画面をBefore/Afterで直してみます。

Beforeは、AIが出しそうな旧来スタイルのコードです。page.go・ft.Viewの位置引数・ft.ElevatedButtonをそのまま使っています。

import flet as ft
from nav_common import setup_page

CUSTOMERS = {
    "1": "株式会社アルファ",
    "2": "ベータ商事",
    "3": "ガンマ工業",
}


def main(page: ft.Page):
    setup_page(page, "顧客管理(Before)")

    def route_change(e):
        page.views.clear()
        page.views.append(
            ft.View(
                "/",
                [
                    ft.Text("顧客一覧", size=20),
                    *[
                        ft.ElevatedButton(name, on_click=lambda _, cid=cid: page.go(f"/customers/{cid}"))
                        for cid, name in CUSTOMERS.items()
                    ],
                ],
            )
        )
        if page.route.startswith("/customers/"):
            cid = page.route.split("/")[-1]
            page.views.append(
                ft.View(
                    page.route,
                    [
                        ft.Text(f"顧客詳細: {CUSTOMERS.get(cid, '不明')}", size=20),
                        ft.ElevatedButton("一覧へ戻る", on_click=lambda _: page.go("/")),
                    ],
                )
            )
        page.update()

    page.on_route_change = route_change
    page.go(page.route)


ft.run(main, view=ft.AppView.WEB_BROWSER, port=8584)
Before実行結果 page.goエラー
▲ Before実行結果。page.goがエラーになり、赤いエラーバナーが表示される

Afterは、route=・controls=を明示し、page.navigateとft.TemplateRouteで動的な顧客詳細ページ(/customers/:id)まで組み直したものです。

import flet as ft
from nav_common import setup_page

CUSTOMERS = {
    "1": "株式会社アルファ",
    "2": "ベータ商事",
    "3": "ガンマ工業",
}


def main(page: ft.Page):
    setup_page(page, "顧客管理(After)")

    def route_change(e=None):
        page.views.clear()
        page.views.append(
            ft.View(
                route="/",
                controls=[
                    ft.Text("顧客一覧", size=20, weight=ft.FontWeight.BOLD),
                    ft.Container(height=8),
                    *[
                        ft.Button(
                            name,
                            icon=ft.Icons.PERSON,
                            on_click=lambda e, cid=cid: page.navigate(f"/customers/{cid}"),
                        )
                        for cid, name in CUSTOMERS.items()
                    ],
                ],
                padding=30,
            )
        )

        troute = ft.TemplateRoute(page.route)
        if troute.match("/customers/:id"):
            name = CUSTOMERS.get(troute.id, "不明な顧客")
            page.views.append(
                ft.View(
                    route=page.route,
                    controls=[
                        ft.Text("顧客詳細", size=20, weight=ft.FontWeight.BOLD),
                        ft.Text(f"ID: {troute.id}", size=13, color=ft.Colors.GREY_600),
                        ft.Text(name, size=17),
                        ft.Container(height=16),
                        ft.Button(
                            "一覧へ戻る",
                            icon=ft.Icons.ARROW_BACK,
                            on_click=lambda e: page.navigate("/"),
                        ),
                    ],
                    padding=30,
                )
            )
        page.update()

    page.on_route_change = route_change
    route_change()


ft.run(main, view=ft.AppView.WEB_BROWSER, port=8585)

ft.TemplateRoute(page.route)に対して.match("/customers/:id")を呼ぶと、URLのパターンが一致するかを判定でき、一致した場合はtroute.idでパスの値(この場合は顧客ID)を取り出せます。

After 顧客一覧画面
▲ After: 顧客一覧画面
After 顧客詳細画面 ベータ商事
▲ After: 「ベータ商事」を選んだ詳細画面。ID: 2、名前が正しく表示されている

page.goをpage.navigateに、位置引数をroute=・controls=に置き換えるだけで、動的なURLを扱う画面遷移まで正しく動くようになります。

スポンサーリンク

サンプルコードのダウンロード

この記事で紹介した7本のサンプルコード(route入門、page.goエラー再現、View引数入れ替わり、navigate no-op、顧客一覧/詳細のBefore/After、共通ヘルパー)は、まとめてダウンロードできます。

📦 サンプルコード一式をダウンロード(zip)

スポンサーリンク

よくある質問(FAQ)

Qpage.goとpage.navigateの違いは何ですか?

Apage.goはFlet 1.0より前の画面遷移メソッドで、1.0では完全に削除されています。後継は同期コールバック用のpage.navigate()と、async用のpage.push_route()です。通常のon_clickハンドラからはpage.navigate()を使います。

Qft.Viewはなぜキーワード引数で書いた方がいいのですか?

Aft.Viewの位置引数の順序はバージョンによって変わっており、Flet 1.0では(controls, route)の順です。route=・controls=を明示しておけば、この順序変更の影響を受けません。

Q画面が真っ白になったとき、何を確認すればいいですか?

Aまずはターミナルとブラウザの開発者コンソールを確認しますが、この記事で紹介したような不具合はエラーも警告も出ません。ft.Viewの引数がキーワード引数になっているか、on_route_changeが実際に呼ばれているか(printを仕込んで確認)を1つずつ疑うと原因にたどり着けます。

Q初期画面が表示されないときは何を疑えばいいですか?

Apage.navigate(page.route)のように、現在のルートと同じ値でnavigateしていないか確認してください。ルートが変化しない呼び出しはon_route_changeを発火させないため、on_route_changeに登録した関数を直接1回呼ぶ書き方に直す必要があります。

Q動的なURL(/customers/1のような)はどう扱えばいいですか?

Aft.TemplateRoute(page.route)を作り、.match("/customers/:id")のようにパターンを渡します。一致すればtroute.idでパスの中の値を取り出せます。


スポンサーリンク

まとめ

この記事では、Fletの画面遷移でFlet 1.0が変えた3つのポイント(page.goの削除、ft.Viewの引数順序、初期表示のnavigateno-op)と、エラーなく画面が真っ白になる不具合の直し方を、動かした画面とあわせて紹介しました。

  • page.go()はFlet 1.0で完全に削除されている。後継はpage.navigate()(同期)とpage.push_route()(async)
  • ft.Viewは位置引数の順序が変わっているため、route=・controls=のキーワード引数で書く
  • page.navigate(page.route)のような「変化しないnavigate」はon_route_changeを発火させない。初期表示はルート変更用の関数を直接呼ぶ
  • ft.TemplateRouteを使うと、/customers/:idのような動的なURLからIDなどの値を取り出せる
  • Fletのレイアウト崩壊(前回)と同じく、画面遷移の不具合もエラーや警告が出ないまま起きる


ニンジン🥕アイコン

Follow me on X

本ブログの中の人「ニンジン🥕」はXやってます。新着記事はいち早くポストでお知らせしているので、見逃したくない方は @ninjin_py_vba をフォローしてみてください🥕

@ninjin_py_vba をフォロー

この記事が役に立ったら、シェアしてもらえると嬉しいです🥕

Xでポスト はてブ LINE Pocket
まとめハブ
Python
スポンサーリンク

コメント