Skip to main content

Navigation and Routing

· 5 min read
Feodor Fitsner

Flet 0.1.42 has been released with navigation and routing!

Navigation and routing is an essential feature of Single Page Applications (SPA) which allows organizing application user interface into virtual pages (views) and "navigate" between them while application URL reflects the current state of the app.

For mobile apps navigation and routing serves as a deep linking to specific application parts.

Well, it took more efforts than expected to add navigation and routing into Flet as the implementation is based on Navigator 2.0 Flutter API and required to replace Flet's "Page" abstraction with "Page and Views". Flutter's newer navigation and routing API has substantial improvements such as:

  1. Programmatic control over history stack.
  2. An easy way to intercept a call to "Back" button in AppBar.
  3. Robust synchronization with browser history.

Explore source code of the example above.

Page route

Page route is a portion of application URL after # symbol:

Default application route, if not set in application URL by the user, is /. All routes start with /, for example /store, /authors/1/books/2.

Application route can be obtained by reading page.route property, for example:

import flet as ft

def main(page: ft.Page):
page.add(ft.Text(f"Initial route: {page.route}"))

ft.app(target=main, view=ft.WEB_BROWSER)

Grab application URL, open a new browser tab, paste the URL, modify its part after # to /test and hit enter. You should see "Initial route: /test".

Every time the route in the URL is changed (by editing the URL or navigating browser history with Back/Forward buttons) Flet calls page.on_route_change event handler:

import flet as ft

def main(page: ft.Page):
page.add(ft.Text(f"Initial route: {page.route}"))

def route_change(route):
page.add(ft.Text(f"New route: {route}"))

page.on_route_change = route_change
page.update()

ft.app(target=main, view=ft.WEB_BROWSER)

Now try updating URL hash a few times and then use Back/Forward buttons! You should see a new message added to a page each time the route changes:

Route can be changed programmatically, by updating page.route property:

import flet as ft

def main(page: ft.Page):
page.add(ft.Text(f"Initial route: {page.route}"))

def route_change(route):
page.add(ft.Text(f"New route: {route}"))

def go_store(e):
page.route = "/store"
page.update()

page.on_route_change = route_change
page.add(ft.ElevatedButton("Go to Store", on_click=go_store))

ft.app(target=main, view=ft.WEB_BROWSER)

Click "Go to Store" button and you'll see application URL is changed and a new item is pushed in a browser history. You can use browser "Back" button to navigate to a previous route.

Page views

Flet's Page now is not just a single page, but a container for View layered on top of each other like a sandwich:

A collection of views represents navigator history. Page has page.views property to access views collection.

The last view in the list is the one currently displayed on a page. Views list must have at least one element (root view).

To simulate a transition between pages change page.route and add a new View in the end of page.view list.

Pop the last view from the collection and change route to a "previous" one in page.on_view_pop event handler to go back.

Building views on route change

To build a reliable navigation there must be a single place in the program which builds a list of views depending on the current route. Other words, navigation history stack (represented by the list of views) must be a function of a route.

This place is page.on_route_change event handler.

Let's put everything together into a complete example which allows navigating between two pages:

import flet as ft

def main(page: ft.Page):
page.title = "Routes Example"

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()

def view_pop(view):
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.go(page.route)


ft.app(target=main, view=ft.WEB_BROWSER)

Try navigating between pages using "Visit Store" and "Go Home" buttons, Back/Forward browser buttons, manually changing route in the URL - it works no matter what! :)

note

To "navigate" between pages we used page.go(route) - a helper method that updates page.route, calls page.on_route_change event handler to update views and finally calls page.update().

Notice the usage of page.on_view_pop event handler. It fires when the user clicks automatic "Back" button in AppBar control. In the handler we remove the last element from views collection and navigate to view's root "under" it.

Route templates

Flet offers TemplateRoute - an utility class based on repath library which allows matching ExpressJS-like routes and parsing their parameters, for example /account/:account_id/orders/:order_id.

TemplateRoute plays great with route change event:

troute = TemplateRoute(page.route)

if troute.match("/books/:id"):
print("Book view ID:", troute.id)
elif troute.match("/account/:account_id/orders/:order_id"):
print("Account:", troute.account_id, "Order:", troute.order_id)
else:
print("Unknown route")

You can read more about template syntax supported by repath library here.

That's all for today!

Give Flet a try and let us know what you think!