Fletの画面遷移におけるTIPS
はじめに
FletはPythonでFlutterアプリを構築できるフレームワークです。
ボタンやテキストボックスといったGUIとしての部品だけでなく、異なる画面間を遷移するための部品も揃ってます。
ただ、その取り扱いにはけっこう苦戦したので、その時に得た知見をを共有します。
読者の想定
Fletの公式ドキュメントの「Navigation and routing」を読んで、以下を理解している
-
page.views
の最後の要素が画面に表示される-
page.views
:ft.View
オブジェクトのリスト
-
-
page.go()
などでRouteが変更される時にpage.on_route_change
で指定した関数が実行される -
AppBar
の戻るボタンをクリックするとpage.on_view_pop
で指定した関数が実行される
環境構成
筆者は下記の構成で動作を確認しています。
- OS:Windows10
- Pythonバージョン:3.11.2
- Fletバージョン:0.4.2
この記事を読んで実現できる画面遷移
ある目的を達成するためには複数のフェーズがあり、各フェーズでのタスクが完了したら次の画面へ遷移していくといった動作を実現します。
例えば、ショッピングサイトで買い物する時に、商品検索画面→カート画面→購入情報入力画面→購入完了画面→商品検索画面に戻るといった感じで画面遷移するイメージです。
この記事では簡単のために、下図のような画面遷移を行う処理を実装することを前提とします。
- 画面遷移用のボタンを押すと次の画面に遷移する
- フォームに入力したデータを次の画面に渡す
-
AppBar
の戻るボタンを押すと前の画面に遷移する- View2画面からView1画面に戻った時、View1画面で入力したデータはそのまま残った状態
- View2画面からの画面遷移ではTop画面に遷移する
- Top画面からView2画面には戻れないようにする
実装したプログラムのソースコード
前述した画面遷移を実装したプログラムのソースコードです。
クリックすると展開されます
import flet as ft
class Top(ft.View):
def __init__(self):
data = "Top data"
controls = [
ft.AppBar(title=ft.Text("Top view"), bgcolor=ft.colors.SURFACE_VARIANT),
ft.TextField(value=data, on_change=self.changed),
ft.ElevatedButton("Go to View1", on_click=self.clicked)
]
super().__init__("/", controls=controls)
self.data = data
def clicked(self, e):
e.page.go("/view1")
def changed(self, e):
self.data = e.control.value
self.update()
class View1(ft.View):
def __init__(self, _data):
data = 'View1 data'
controls = [
ft.AppBar(title=ft.Text("View1"), bgcolor=ft.colors.SURFACE_VARIANT),
ft.Text(f'Top\'s data: {_data}'),
ft.TextField(value=data, on_change=self.changed),
ft.ElevatedButton("Go to View2", on_click=self.clicked)
]
super().__init__("/view1", controls=controls)
self.data = data
def clicked(self, e):
e.page.go("/view2")
def changed(self, e):
self.data = e.control.value
self.update()
class View2(ft.View):
def __init__(self, _data):
controls = [
ft.AppBar(title=ft.Text("View2"), bgcolor=ft.colors.SURFACE_VARIANT),
ft.Text(f'View1\'s data: {_data}'),
ft.ElevatedButton("Go to Top", on_click=self.clicked)
]
super().__init__("/view2", controls)
def clicked(self, e):
e.page.go("/")
def main(page: ft.Page):
page.title = "test app"
pop_flag = False
def route_change(e):
nonlocal pop_flag
if pop_flag:
pop_flag = False
else:
if page.route == "/":
page.views.clear()
page.views.append(
Top()
)
elif page.route == "/view1":
page.views.append(
View1(page.views[-1].data)
)
elif page.route == "/view2":
page.views.append(
View2(page.views[-1].data)
)
def view_pop(e):
nonlocal pop_flag
pop_flag = True
page.views.pop()
top_view = page.views[-1]
page.go(top_view.route)
page.on_route_change = route_change
page.on_view_pop = view_pop
page.views.clear()
page.go("/")
if __name__ == '__main__':
ft.app(target=main, view=ft.WEB_BROWSER)
各画面を開いている時のpage.routeとpage.viewsの要素数は下表のようになります。
画面 | page.route | len(page.views) |
---|---|---|
Top画面 | / | 1 |
View1画面 | /view1 | 2 |
View2画面 | /view2 | 3 |
画面遷移を実装する際のポイント
画面遷移を実装する際のポイントは以下の3点です。
- 画面毎にft.Viewを継承したクラスを作成する
- 画面が持つデータを保持する際は
page.views.clear()
を使わない -
view_pop
時にもroute_change
が呼ばれることに気をつける
画面毎にft.Viewを継承したクラスを作成する
画面毎に独自の処理を実行させたり、画面遷移先にデータを渡したりしたいことがあります。そういう時は画面毎にft.View
を継承したクラスを作成すると良いでしょう。
ft.View
オブジェクトはdata
という任意のデータを保持するための変数を持っているので、画面遷移先にデータを渡すためにこの変数を使うこともできます。しかし、data
に値を格納したり、値を更新するための処理は定義されていません。
そのため、ft.View
を継承したクラスを作成して、そこに定義してあげるわけです。
もちろん、クラス内のメソッドではなく独自の関数として定義することもできますが、画面数が増えてくると似て非なる関数を別名で作成する必要があったり、どの関数をどの画面で利用するのかといった管理が煩雑になったりしてきます。
実装したプログラムでは、各画面で類似する関数を必要としますが、それぞれのクラス内のメソッドとして定義しているので、名前の重複を気にする必要がありません。
class Top(ft.View):
def __init__(self):
# 省略
def clicked(self, e):
e.page.go("/view1")
def changed(self, e):
self.data = e.control.value
self.update()
class View1(ft.View):
def __init__(self, _data):
# 省略
def clicked(self, e):
e.page.go("/view2")
def changed(self, e):
self.data = e.control.value
self.update()
class View2(ft.View):
def __init__(self, _data):
# 省略
def clicked(self, e):
e.page.go("/")
page.views.clear()
を使わない
画面が持つデータを保持する際は公式ドキュメントのRoute Exampleでは、Routeが変わる度にpage.views.clear()
を実行し、そのRouteまでのView
を追加し直すことで画面遷移を実現しています。
def route_change(route):
page.views.clear()
page.views.append(
ft.View(
"/",
[
ft.AppBar(title=ft.Text("Flet app"), bgcolor=ft.colors.SURFACE_VARIANT),
ft.ElevatedButton("Visit Store", on_click=lambda _: page.go("/store")),
],
)
)
if page.route == "/store":
page.views.append(
ft.View(
"/store",
[
ft.AppBar(title=ft.Text("Store"), bgcolor=ft.colors.SURFACE_VARIANT),
ft.ElevatedButton("Go Home", on_click=lambda _: page.go("/")),
],
)
)
page.update()
しかし、新しい画面遷移先のView
だけでなく、元々いた画面のView
も追加し直すのは無駄がある上に、AppBar
の戻るボタンで元のページに戻っても表示されるView
は元のものとは別物であり、元々いた画面での入力情報は破棄されてしまいます。
画面が持つデータを保持する際はpage.views.clear()
は使わない方が良いでしょう。
実装したプログラムでは、Top画面に遷移した時だけpage.views.clear()
が実行されるようになっています。
def route_change(e):
nonlocal pop_flag
if pop_flag:
pop_flag = False
else:
if page.route == "/":
page.views.clear() # <- page.routeが"/"の時だけ実行
page.views.append(
Top()
)
elif page.route == "/view1":
page.views.append(
View1(page.views[-1].data)
)
elif page.route == "/view2":
page.views.append(
View2(page.views[-1].data)
)
view_pop
時にもroute_change
が呼ばれることに気をつける
これには一番ハマりました。
前述したように、画面が持つデータを保持したかったのでTop画面に遷移した時だけpage.views.clear()
が実行されるように変更したのですが、AppBar
の戻るボタンを押した時に実行されるview_pop
関数内のpage.go()
で一つ前の画面に遷移する際にもroute_change
が呼ばれるため、余分に前の画面を追加してしまうようになりました。
(Route Exampleでは毎度一からView
を作成し直してるためこの事象は発生しない)
操作 | 処理 | page.route | len(page.views) |
---|---|---|---|
-(初期状態) | - | / | 1 |
Top画面の画面遷移ボタン | page.go('/view1') | /view1 | 1→2 |
View1画面のAppBarの戻るボタン | page.views.pop() top_view=page.views[-1] page.go(top_view.route) |
/ | 2→1→2 |
これを回避するために、page.go()
ではなくpage.update()
を実行するように変更してみました。
def view_pop(e):
page.views.pop()
page.update()
AppBar
の戻るボタンで戻る際には一見問題ないのですが、page.route
はAppBar
の戻るボタンを押す前の画面の状態です。そのため、戻った画面で再び次の画面に遷移するボタンを押したとしても、Routeが変わっていないためroute_change
が実行されません。
操作 | 処理 | page.route | on_route_change で指定した関数 |
---|---|---|---|
-(初期状態) | - | / | - |
Top画面の画面遷移ボタン | page.go('/view1') | /view1 | 実行される |
View1画面のAppBarの戻るボタン | page.views.pop() page.update() |
/view1 | 実行されない |
Top画面の画面遷移ボタン | page.go('/view1') | /view1 | 実行されない |
上記の結果から、view_pop
関数内のpage.go()
で画面表示とRouteを前の画面に変更しつつ、route_change
関数で余分な画面が追加されないよう配慮する必要があることが分かりました。
実装したプログラムでは、view_pop
が呼ばれた時のフラグをpop_flag
で管理し、True
の時は画面の追加処理は実行しないように分岐するようにしました。
def route_change(e):
nonlocal pop_flag # <- 上位のmain関数スコープの変数
if pop_flag:
pop_flag = False # <- フラグがTrueの時はFalseにするだけ
else:
# 省略
def view_pop(e):
nonlocal pop_flag # <- 上位のmain関数スコープの変数
pop_flag = True # <- フラグをTrueにする
page.views.pop()
top_view = page.views[-1]
page.go(top_view.route)
おわりに
これらのポイントを押さえておけば、Fletアプリ内での画面遷移についてはそれなりに動くと思います。
しかし、ブラウザの進む/戻るボタンやアドレスバーへの直接入力による画面遷移には対応していないため、本来であればFletアプリ外からRoute変更された場合の挙動も考慮する必要があります。
現時点で思いつくのは、Fletアプリ内のRoute変更にフラグを立て、それ以外のRoute変更は許容しないといったところでしょうか。
Discussion