LoginSignup
14
12
お題は不問!Qiita Engineer Festa 2023で記事投稿!

Fletで実践的なアプリ作成に向けて!Googleアカウント認証と表示制御から

Last updated at Posted at 2023-07-06

先月あたりからFletというPython × Flutterのライブラリについて学習を始めました。

実際に簡単なハンズオンを行って記事を公開したところ、想像より遥かに多い方々に記事をみていただけて、驚きを隠せぬまま今回の記事作成に至っています!

これまではFletの基本的な文法や、簡単な機能を持つ画面を何個か作ってきました。
今回はもう少し本格的なアプリケーション作成をしたいと思って、始めました。
全部完成してから、公開!という手もあったのですが、多分まとめる作業がかなりボリューミーになりそう。。。だったので、
少しずつ公開して丁寧なアウトプットにしたいと思います。

続編
Fletで実践的なアプリ作成に向けて!セミナー運営サイトの設計・実装を進める7/20に公開済です!

作成してみたもの

今回作成したのが以下画像の通りです。
image.png

デモサイト
今回作成したアプリケーションのデモサイトです。機能数はまだ少ないないため、なんとなくポチポチしていただければ、どんな機能があるか把握いただけると、と思います!

作成した機能

レイアウトのベースは既にあるので、利用しつつ、以下の機能を作成してみました。

  • light/darkモードの切り替え
  • googleアカウントによるログイン/ログアウト機能
  • ログイン/未ログインに応じて、画面の表示内容制御
    • こちらは、土台としていたサンプルを元に自分になりアレンジしました!

今回は、画面上部の青い部分と左側のタブに関する説明が中心です。
画面右側(※)は、これから具体的に作り上げますので、特に説明は記載していません。今回は、今後使うかな~~と考えているコンテンツを仮で配置しています。
今回の説明は特に記載していません。
※作成してみたもの、でお見せした画像の、「一覧画面」といったタイトルやその下のカード表示

また、以下サンプルを土台に作成を開始しました。
同じようなレイアウト、であると確認いただけると思います。この土台に手を加えて、先ほど表示した画面レイアウトになりました。

image.png

実装

まずは、画面要素を作り上げるために必要な実装について記載します。
その後、画面に表示した要素に取り付けた機能を実現するための実装について記載しています。

画面要素の作成

土台作成

画面要素の構築を進めていきます。最初は本当に土台です。(起動して問題ないか確認する程度)

main.py
import flet as ft
from flet import (
    Text
)
if __name__ == "__main__":
    def main(page: Page, title="Basic Responsive Menu"):
        page.title = title
        page.theme_mode = "light"
        page.add(Text(f"My Flet Sample App"))
    ft.app(target=main, port=8550, view=ft.WEB_BROWSER)

画面にテキストを表示するだけですね。
image.png
画面に要素を追加する場合は、以下を記述します。

page.add(追加したい内容)
AppBar追加

Appbarの先頭(左端)の表示内容、タイトルの中央寄せ、AppBarの高さ・背景色などを定義します。
今回はpage.add()は使ってませんね。
AppBarについて、page.appbarというプロパティが、Pageクラスにフィールドが用意されているので、それを使います。page.XXXのXXXは色々用意されています。以下リンクでその数が確認できますが、かなり充実してます!!

if __name__ == "__main__":
    def main(page: Page, title="Basic Responsive Menu"):
        page.title = title
        # 略・・・
        # AppBarを追加
        menu_button = IconButton(icons.MENU, icon_color=ft.colors.WHITE)
        page.appbar = AppBar(
            title=Text(f"My Flet Sample App", size=32, color=ft.colors.WHITE),
            leading=menu_button,
            leading_width=40,
            center_title=True,
            toolbar_height=70,
            bgcolor=ft.colors.BLUE_ACCENT_700
        )
    ft.app(target=main, port=8550, view=ft.WEB_BROWSER)

image.png

サイドバー追加

他のファイルで定義したクラスにpage情報を渡して、表示内容や表示内容に関するアクション・制御に必要な関数を設定します。
またpages配列は各タブの情報(アイコンとテキスト)を定義しています。
※他ファイルのコードはここでは割愛します。この後のGoogle認証時の表示制御で記載します。

from layout import ResponsiveMenuLayout
if __name__ == "__main__":
    def main(page: Page, title="Basic Responsive Menu"):
        page.title = title
        # 略・・・
        # AppBarを追加
        # 略・・・
        pages = [
        # 各タブの表示内容(略)
        ]
        menu_layout = ResponsiveMenuLayout(page, pages)
        page.add(menu_layout)
        menu_button.on_click = lambda e: menu_layout.toggle_navigation()
    ft.app(target=main, port=8550, view=ft.WEB_BROWSER)

image.png
アイコンクリック時
image.png

AppBarにアイコンとボタン追加

light & darkモードの切り替えアイコンとGoogle認証のボタンについて要素を追加します。こちらも別ファイルで必要な要素と関数を定義して、そのクラスを呼び出します。

from layout import ResponsiveMenuLayout
from google_auth import GoogleOAuth
from switch_right_dark import ToggleDarkLight
if __name__ == "__main__":
    def main(page: Page, title="Basic Responsive Menu"):
        page.title = title
        # 略・・・
        menu_button.on_click = lambda e: menu_layout.toggle_navigation()
        # AppBarの表示要素追加
        page.appbar.actions = []
        # light dark 切り替え要素
        ToggleDarkLight(page, page.appbar.actions)
        # Google認証要素追加
        GoogleOAuth(page, page.appbar.actions)
    ft.app(target=main, port=8550, view=ft.WEB_BROWSER)

image.png

機能面の実装

これまでは、画面に対して要素を追加する方法を記載してきました。画面に対して要素を追加するだけであれば、難易度はそこまで上がらないと思います。実際に各要素に対してアクションを加える始めると、色々考えるようになると感じました。

light darkモード切替以外にも機能を追加しよう

Fletの公式ドキュメントに近い形にしようと思って進めました。
画面の背景色が変わるので、機能しているかどうかは、ボタンを押したときに判断しやすいです。
ですが切り替え以外にも、アイコンの切り替えやToolTip(マウスを載せたときの表示文字)をつけたいです。以下画像が作成してみたものです。
image.png
image.png

switch_right_dark.py
import flet as ft
from flet import (
    Container,
    IconButton,
    Page,
    ButtonStyle,
    ProgressBar,
)
from time import sleep

class ToggleDarkLight():
    def __init__(
        self,
        page: Page,
        contents: list,
        * args,
        **kwargs,
    ):
        super().__init__(*args, **kwargs)
        self.page = page

        # 切り替え時にプログレスバー表示
        page.splash = ProgressBar(visible=False)

        def change_theme(e):
            # プログレスバー表示
            page.splash.visible = True
            page.theme_mode = "light" if page.theme_mode == "dark" else "dark"
            page.update()

            # アニメーション適用のための時間確保(1s)
            sleep(1)

            # アイコンの選択状態
            toggle_dark_light.selected = not toggle_dark_light.selected
            # ToolTipの表示文言更新
            toggle_dark_light.tooltip = f"switch light and dark mode (currently {'dark' if toggle_dark_light.selected else 'light'} mode)"
            # プログレスバー非表示
            page.splash.visible = False
            page.update()

        # 切り替え用のボタン
        toggle_dark_light = IconButton(
            # ToolTipの表示文言
            tooltip=f"switch light and dark mode (currently light mode)",
            # アイコンクリック時に実行する処理
            on_click=change_theme,
            icon="light_mode",
            selected_icon="dark_mode",
            style=ButtonStyle(
                # change color if light and dark
                color={"": ft.colors.ORANGE_300,
                       "selected": ft.colors.YELLOW},
            )
        )

        contents.append(
            Container(
                content=toggle_dark_light,
                margin=ft.margin.only(right=10)
            )
        )

page.add(x)page.update() の違いについて

ここまでの実装紹介で、page.add(x)page.update()が登場しています。
何が異なるのかという話ですが、実はpage.add(x)page.update()も実施しています。
チュートリアルのコードのコメントで以下のように記載されています。

page.add(t) # it's a shortcut for page.controls.append(t) and then page.update()

page.controls.append(t)は画面のある要素に対して、何か(t)を追加することを指します。
追加した上で、画面の状態を更新(page.update())します。
page.update()はlight darkモードでもそうですが、何か要素を追加する操作はなく、
既存の要素を使って、画面の状態を更新しています。

Google認証機能を実装してみよう

Google Cloudの設定
OAuthのクライアントIDを作成します。ポート番号8550はfletアプリケーション起動時に指定している番号と同じです。作成した際の、client IDclient Secretが発行されるので、その値は実装時に使用します。
image.png
画面操作
認証前は以下のように、タブが2つされた状態です。
image.png
ログインボタンをクリックすると、アカウント選択のウインドウが立ち上がるので、アカウントを選択して、ログイン処理を実行します。
image.png
image.png
Sign in からSign outにボタンが変わり、表示されるタブが4つに増えました!!
image.png
image.png

画像を見ていただいた上で、以下の機能を実装しています。

  • ボタン(ログイン/ログアウト)の差し替え
  • 認証前後で表示タブの切り替え
    client IDclient SecretRedirect Urlは環境変数のファイルに設定して、読み込む形にしています。
google_oauth.py
import flet as ft
from flet import (
    Container,
    ElevatedButton,
    Page,
)
from flet.auth.providers.google_oauth_provider import GoogleOAuthProvider
import os
from dotenv import load_dotenv
load_dotenv()

ClientID = os.getenv('ClientID')
ClientSecret = os.getenv('ClientSecret')
RedirectUrl= os.getenv('RedirectUrl')

class GoogleOAuth():
    def __init__(
        self,
        page: Page,
        contents: list,
        * args,
        **kwargs,
    ):
        super().__init__(*args, **kwargs)

        self.page = page

        # GoogleOAuthのProvider定義
        provider = GoogleOAuthProvider(
            client_id=ClientID,
            client_secret=ClientSecret,
            redirect_url=RedirectUrl
        )

        # ログイン処理
        def login_google(e):
            self.page.login(provider)

        # ログアウト処理
        def logout_google(e):
            self.page.logout()

        # ログインボタン
        login_button = Container(
            content=ElevatedButton(
                "Sign in Google", bgcolor=ft.colors.LIGHT_BLUE_500, color=ft.colors.WHITE, on_click=login_google,
            ),
            margin=ft.margin.only(right=10)
        )

        # ログアウトボタン
        logout_button = Container(
            content=ElevatedButton(
                "Sign out Google", bgcolor=ft.colors.RED_300, color=ft.colors.WHITE, on_click=logout_google),
            margin=ft.margin.only(right=10)
        )

        def on_login(e):
            print(page.auth.user['name'], page.auth.user['email'])
            contents.pop()
            # 画面に表示するボタンを「ログアウト」ボタンに
            log_inout_button = logout_button
            contents.append(log_inout_button)
            page.update()
            page.go('/')

        def on_logout(e):
            contents.pop()
            log_inout_button = login_button
            contents.append(log_inout_button)
            page.update()
            page.go('/logout')

        page.on_login = on_login
        page.on_logout = on_logout
        # 画面に表示するボタンを「ログイン」ボタンに
        log_inout_button = login_button
        contents.append(log_inout_button)

# reference
# https://www.youtube.com/watch?v=t9ca2jC4YTo

Google認証前後で表示内容を制御してみよう

こちらは、実装を割愛すると中盤で話した、ResponsiveMenuLayoutの話になります。
ResponsiveMenuLayoutクラス内部に、関数_on_route_changeを定義しています。画面上で、タブを切り替えるたびに実行する処理です。
また関数でupdate_destinationsで表示タブの制御をしています。肝心なのはself.page.authです。
この値は、Google認証前はNoneですが、認証後はユーザ名やメールアドレス情報が格納されます。(None以外になる)
この値の違いを利用して、画面の表示タブを制御する機能を加えました。
image.png
アイコン検索
各タブで表示しているアイコンは以下で検索しました。Flet公式ページのGalleryからアクセスできます。

layout.py
class ResponsiveMenuLayout(Row):
    def __init__(
        self,
        page: Page,
        pages,
        support_routes=True,
        *args,
        **kwargs,
    ):
        super().__init__(*args, **kwargs)
        self.page = page
        
        # 略・・・・・
        if support_routes:
            self._route_change(page.route)
            # パスが更新される度に実行する処理を設定
            self.page.on_route_change = self._on_route_change
        self._change_displayed_page()

        # パスが更新される度に実行する処理
        def _on_route_change(self, event):
            # タブの表示項目を更新する処理を呼び出し
            self.update_destinations()
            self._route_change(event.route)
            self.page.update()

        # タブの表示項目を更新する処理
        def update_destinations(self):
            navigation_items = self.navigation_items
    
            if self.page.auth is None:
                _navigation_items = []
                _navigation_items.append(navigation_items[0])
                _navigation_items.append(navigation_items[1])
                navigation_items = _navigation_items
    
            self.navigation_rail.destinations = [
                NavigationRailDestination(**nav_specs) for nav_specs in navigation_items
            ]

以上が機能に関する実装の紹介でした!

感想と今後

今回の取り組みを通じて、レイアウト構築やログイン状態に応じた画面制御に関する実装方法を知ることができました。
特に、表示タブの制御を自力で実装できたのは、既存の実装(土台にしていたコード)
を理解できている
といえる部分があるので、良かったと思います。

次のステップとして、
各画面の機能を検討し、画面の実装やデータベースのテーブル定義などを進めていきたいと思います。

14
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
12