131
141

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Flet使うとPythonでWeb/Desktop/Mobileアプリ作れちゃうんだ

Last updated at Posted at 2023-06-19

Fletというライブラリはご存じでしょうか。。私はつい1週間前に知ったばかりです・・・
Pythonを利用した技術について、情報収集している中で見つました。元々、reactPy(Pythonでreactの機能を実現できるライブラリ)の学習を進めており、こちらのyoutubeチャンネルを参考にしていました。
reactPyの記事は別途公開しているので、気になる方は見てみてください!!

チャンネルの他動画を眺めていたところ、Fletと書かれた動画がありました。なんだろう?と思って、少しやってみたら面白そう、ということにでもう少し触ってみることにしました。
ハンズオンを進める中で、デプロイ先にcloudflareが出てきたので、この機会にそちらの理解も深めることをもう一つの目標として、作業を進めることにしました!

Fletとは

以下文章はFletの公式Docsから引用した内容です。mobileFlutterという単語もありますね。
Web,Desktop,MobileのアプリケーションがすべてPythonで作れちゃう、というライブラリのようです。

 Flet is a framework that allows building interactive multi-user web, desktop and mobile applications in your favorite language without prior experience in frontend development.
You build a UI for your program with Flet controls which are based on Flutter by Google. Flet does not just "wrap" Flutter widgets, but adds its own "opinion" by combining smaller widgets, hiding complexities, implementing UI best-practices, applying reasonable defaults - all to ensure your apps look cool and professional without extra efforts.

githubのStarが5000を超えていますので、全然マイナーではないですね。
githubのStarが10800まで伸びていました!(2024年9月9日現在)

サンプルが充実している印象
Fletを用いた実装のサンプルが以下リポジトリにあるのですが、数が多く、何かアプリケーション作成をしたいとき、
以下のサンプルを参考にすると、開発を始めやすくなる印象を受けました。

今回と以降の流れについて

今回はまだ、Fletとcloudflareの基礎理解の段階です。
ですが、この話だけではとどめずに深堀りします。以下の流れで進めようと思っています。

  1. Flet Docs (Python Guide編) + cloudflareのpagesデプロイ ← 今回ここです!
  2. Fletのアプリをfly.ioへデプロイしてcloudflare workers&D1と連携6/22に公開済です!
  3. 1と2の学習を経て、詳細は未定ですが、DBやStorageを利用したサイトを作成
    1. Fletで実践的なアプリ作成に向けて!Googleアカウント認証と表示制御から7/7に公開済です!
    2. Fletで実践的なアプリ作成に向けて!セミナー運営サイトの設計・実装を進める7/20に公開済です!

image.png

Fletを触ってみましょうか

PythonとFletのインストールのみですぐに開始できます。
Python Tutorialの項目のうち、以下内容が特に気になったので、確認を行いました。ある程度はハンズオン通りですが、全く同じでは実装していても‥、と思ったので、自分なりに少し実装をトッピングしています。(以下項目以外も実装している箇所はあると思います)
確認のために実装したコードについて、記事の最後にリポジトリ情報を載せています。

  • Authentication
  • Client storageとSession storage
  • Encrypting sensitive data
  • Hot reload
  • Packaging desktop app
  • Publishing a static website

Authentication

確認理由
OAuthを利用した認証方法を理解することで、アプリケーションで必要となる認証による表示制御ができるようになるので、確認しました。

GitHubOAuthProviderを用いて、githubアカウントによる認証を行います。

認証前の画面
ボタンをクリックします。
image.png
認証許可を求める画面
みなさんも一度は見たことあるかと思いますが、認証の画面が表示されるので、緑色のボタンをクリックして、認証を行います。
image.png
認証後の画面
認証が成功したので、ログアウト処理のボタンと、コンソールに自分のRepositoryを表示するボタンが表示されるようになりました。
image.png
logoutをクリックすれば、認証前の画面に戻ります。Show Reposをクリックすれば、リポジトリ一覧を確認できます。

コードは以下の通りです。(環境変数の設定は各自でお願いいたします。)

auth.py
import os

from flet.auth.providers.github_oauth_provider import GitHubOAuthProvider
import flet as ft
import requests
import json

from dotenv import load_dotenv
load_dotenv()

def main(page: ft.Page):

    # Define Provider
    provider = GitHubOAuthProvider(
        client_id=os.getenv("GITHUB_CLIENT_ID"),
        client_secret=os.getenv("GITHUB_CLIENT_SECRET"),
        redirect_url="http://localhost:8550/api/oauth/redirect",
    )

    def login_button_click(e):
        page.login(provider, scope=["public_repo"])

    def on_login(e: ft.LoginEvent):
        if not e.error:
            toggle_login_buttons()

    def logout_button_click(e):
        page.logout()

    def on_logout(e):
        toggle_login_buttons()

    def toggle_login_buttons():
        login_button.visible = page.auth is None
        logout_button.visible = page.auth is not None
        repos_button.visible = page.auth is not None
        page.update()

    def display_repos(e):
        headers = {"Authorization": "Bearer {}".format(page.auth.token.access_token)}
        repos_resp = requests.get("https://api.github.com/user/repos", headers=headers)
        user_repos = json.loads(repos_resp.text)
        for repo in user_repos:
            print(repo["full_name"])


    login_button = ft.ElevatedButton("Login with GitHub", on_click=login_button_click)
    logout_button = ft.ElevatedButton("Logout", on_click=logout_button_click)
    repos_button = ft.ElevatedButton("Show Repos", on_click=display_repos)
    toggle_login_buttons()
    page.on_login = on_login
    page.on_logout = on_logout
    # page.
    page.add(login_button, logout_button,repos_button)

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

display_repos(e)引数:eについて
今回は特にon_clickしたときのイベント情報は設定していませんが、Pythonのクラス内(今回の場合は、ElevatedButtonクラス)で定義した関数を外から呼び出す場合、自動的に第一引数にレシーバーが渡されます。それを引き取るための引数を定義する必要があります。

Client storageSession storage

確認理由
client StorageやSession Storageを理解することで、アプリケーションでの値管理が柔軟に行えるようになり、より実践的なアプリケーション作成が行えるようになります。

各ドキュメントをざっくり説明すると、以下の通りです。

  • Client Storageはサーバを止めて再起動しても、影響なし(値が保持される)
  • Session Storageはサーバを止めて再起動すると、影響あり(値が保持されない)

機密情報の扱い

It is responsibility of Flet app developer to encrypt sensitive data before sending it to a client storage, so it's not read/tampered by another app or an app user.

記載されていた通りですが、client Storageについて、機密情報の扱いには注意してください。

以下のような画面を作成して、それぞれの挙動を確かめてみることにしました。
image.png

起動してそれぞれのcheck!ボタンを押して、値を表示すると、どちらもNoneと表示されます。
image.png
それぞれのset!ボタンをクリック後、再度check!ボタンを押します。
それぞれで値(value)が設定できていることを確認しました。
image.png
一旦サーバを停止します。

再度サーバを起動して、それぞれのcheck!ボタンを押します。
Client Storage側では値が維持され、Session Storage側では値が維持されず、Noneに戻っていることを確認できます。
image.png

Client Storageの値は、DevToolsでも確認してみました。Local Storageに値が保持されています。
image.png

画面での挙動を見ていただきましたが、コードは以下の通りです。

storage.py
import flet as ft

def main(page: ft.Page):
    page.title = "Check Storage"
    client_values = ft.Ref[ft.Column]()
    session_values = ft.Ref[ft.Column]()

    def set_client_storage_values(e):
        # strings
        page.client_storage.set("key", "value")
        # numbers, booleans
        page.client_storage.set("number.setting", 12345)
        page.client_storage.set("bool_setting", True)
        # lists
        page.client_storage.set("favorite_colors", ["read", "green", "blue"])

    def check_client_storage_values(e):
        client_values.current.controls.clear()
        client_values.current.controls.append(
            ft.Text(
                f"client storage values : {page.client_storage.get('key')}")
        )
        page.update()

    def set_session_storage_values(e):
        # strings
        page.session.set("key", "value")

        # numbers, booleans
        page.session.set("number.setting", 12345)
        page.session.set("bool_setting", True)

        # lists
        page.session.set("favorite_colors", ["read", "green", "blue"])

    def check_session_storage_values(e):
        session_values.current.controls.clear()
        session_values.current.controls.append(
            ft.Text(
                f"session storage values : {page.session.get('key')}")
        )
        page.update()

    page.add(
        # client
        ft.Text(value="Client Storage Part", size=28, color="red"),
        ft.ElevatedButton("Set!",
                          on_click=set_client_storage_values
                          ),
        ft.ElevatedButton("check!",
                          on_click=check_client_storage_values),
        ft.Column(ref=client_values),
        # session
        ft.Text(value="Session Storage Part", size=28, color="blue"),
        ft.ElevatedButton("Set!",
                          on_click=set_session_storage_values),
        ft.ElevatedButton("check!",
                          on_click=check_session_storage_values),
        ft.Column(ref=session_values),
    )


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

Encrypting sensitive data

確認理由
機密情報の管理に欠かせない、encryptとdecryptの方法を理解するためにやってみました。

あくまでも動作を知るためですが、以下のような画面を作成してみました。

image.png
暗号化するテキストを上のテキストフィールドに入力して、ボタンをクリックすると、暗号化した値をコンソール出力します。
image.png
暗号化した値を下のテキストフィールドに入力して、上のテキストフィールドに入力した値がコンソールに出力されます。この値が上のテキストフィールド入力した値と一致します。
image.png

コードは以下の通りです。

encrypt_decrypt.py
import flet as ft
from flet.security import encrypt, decrypt
import os
from dotenv import load_dotenv
load_dotenv()


def main(page):

    secret_key = os.getenv("GITHUB_CLIENT_ID")
    to_encrypt_text = ft.Ref[ft.TextField]()
    to_decrypt_text = ft.Ref[ft.TextField]()

    def run_encrypt(e):
        encrypted_data = encrypt(to_encrypt_text.current.value, secret_key)
        to_encrypt_text.current.value = ""
        print(encrypted_data)
        page.update()

    def run_decrypt(e):
        plain_text_data = decrypt(to_decrypt_text.current.value, secret_key)
        to_decrypt_text.current.value = ""
        print(plain_text_data)
        page.update()

    page.add(
        ft.Text(value="Encrypt", size=28, color="blue"),
        ft.TextField(ref=to_encrypt_text,
                     label="To Encrypt Text", autofocus=True),
        ft.ElevatedButton("encrypt!", on_click=run_encrypt),
        ft.Text(value="Decrypt", size=28, color="red"),
        ft.TextField(ref=to_decrypt_text, label="To Decrypt Text"),
        ft.ElevatedButton("decrypt", on_click=run_decrypt),

    )


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

Hot reload

確認理由
開発中の手間を省く方法を知るために確認しました。

開発中の話ですが、コード修正して逐一サーバを再起動するのは手間なので、ホットリロードするサーバ起動のコマンドを確認しました。

flet run main.py -d

Packaging desktop app

Flet Python app and all its dependencies can be packaged into an executable and user can run it on their computer without installing a Python interpreter or any modules.

確認理由
パッケージ方法を理解することで、実装したアプリケーションについて、OSさえあれば、Pythonや他の他のモジュールがないPC上でも、実行可能な状態を実現できる方法を知ることができます。

ハンズオンの中で作成した以下のアプリで、試してみました。
image.png

packaging_desktop_app.py
import flet as ft
import logging
logging.basicConfig(level=logging.WARN)
logging.getLogger("flet_core").setLevel(logging.WARN)


def main(page):

    first_name = ft.Ref[ft.TextField]()
    last_name = ft.Ref[ft.TextField]()
    greetings = ft.Ref[ft.Column]()

    def btn_click(e):
        greetings.current.controls.append(
            ft.Text(
                f"Hello, {first_name.current.value} {last_name.current.value}!")
        )
        first_name.current.value = ""
        last_name.current.value = ""
        page.update()
        first_name.current.focus()

    page.add(
        ft.TextField(ref=first_name, label="First name", autofocus=True),
        ft.TextField(ref=last_name, label="Last name"),
        ft.ElevatedButton("Say hello!", on_click=btn_click),
        ft.Column(ref=greetings),
    )


ft.app(target=main)
# ft.app(target=main, port=8550, view=ft.WEB_BROWSER)

pyinstallerのインストール
未インストールで実行すると、以下のメッセージが表示されるので、pipコマンドでインストール

Please install PyInstaller module to use flet pack command: No module named 'PyInstaller'

インストールを確認後、以下コマンドを実行(packaging_desktop_app.pyのファイル名は任意です。)

 flet pack packaging_desktop_app.py
コンソールの出力内容
Updating Flet View version info C:\Users\ADMINI~1\AppData\Local\Temp\3eff7e1f-a446-438d-a32b-e63bbe0bbcce\flet\flet.exe
Running PyInstaller: ['packaging_desktop_app.py', '--noconsole', '--noconfirm', '--onefile', '--version-file', 'C:\\Users\\ADMINI~1\\AppData\\Local\\Temp\\e81d2313-8552-4118-883c-0af064ebc4c0']
691 INFO: PyInstaller: 5.12.0
691 INFO: Python: 3.11.3
721 INFO: Platform: Windows-10-10.0.22621-SP0
722 INFO: wrote C:\Users\Administrator\Documents\python\Flet\first\package\packaging_desktop_app.spec
724 INFO: UPX is not available.
725 INFO: Extending PYTHONPATH with paths
['C:\\Users\\Administrator\\Documents\\python\\Flet\\first\\package']
1487 INFO: checking Analysis
1487 INFO: Building Analysis because Analysis-00.toc is non existent
1487 INFO: Initializing module dependency graph...
1492 INFO: Caching module graph hooks...
1503 INFO: Analyzing base_library.zip ...
3271 INFO: Loading module hook 'hook-encodings.py' from 'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python311\\Lib\\site-packages\\PyInstaller\\hooks'...

(Loading module hookなどが続くので、中略!)

29332 INFO: checking PYZ
29333 INFO: Building PYZ because PYZ-00.toc is non existent
29333 INFO: Building PYZ (ZlibArchive) C:\Users\Administrator\Documents\python\Flet\first\package\build\packaging_desktop_app\PYZ-00.pyz
30639 INFO: Building PYZ (ZlibArchive) C:\Users\Administrator\Documents\python\Flet\first\package\build\packaging_desktop_app\PYZ-00.pyz completed successfully.
30671 INFO: checking PKG
30671 INFO: Building PKG because PKG-00.toc is non existent
30672 INFO: Building PKG (CArchive) packaging_desktop_app.pkg
39847 INFO: Building PKG (CArchive) packaging_desktop_app.pkg completed successfully.
39854 INFO: Bootloader C:\Users\Administrator\AppData\Local\Programs\Python\Python311\Lib\site-packages\PyInstaller\bootloader\Windows-64bit-intel\runw.exe
39855 INFO: checking EXE
39856 INFO: Building EXE because EXE-00.toc is non existent
39856 INFO: Building EXE from EXE-00.toc
39857 INFO: Copying bootloader EXE to C:\Users\Administrator\Documents\python\Flet\first\package\dist\packaging_desktop_app.exe.notanexecutable
39867 INFO: Copying icon to EXE
39868 INFO: Copying icons from ['C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python311\\Lib\\site-packages\\PyInstaller\\bootloader\\images\\icon-windowed.ico']
39870 INFO: Writing RT_GROUP_ICON 0 resource with 104 bytes
39870 INFO: Writing RT_ICON 1 resource with 3752 bytes
39870 INFO: Writing RT_ICON 2 resource with 2216 bytes
39871 INFO: Writing RT_ICON 3 resource with 1384 bytes
39871 INFO: Writing RT_ICON 4 resource with 38188 bytes
39872 INFO: Writing RT_ICON 5 resource with 9640 bytes
39872 INFO: Writing RT_ICON 6 resource with 4264 bytes
39873 INFO: Writing RT_ICON 7 resource with 1128 bytes
39878 INFO: Copying version information to EXE
39885 INFO: Copying 0 resources to EXE
39886 INFO: Embedding manifest in EXE
39887 INFO: Updating manifest in C:\Users\Administrator\Documents\python\Flet\first\package\dist\packaging_desktop_app.exe.notanexecutable
39890 INFO: Updating resource type 24 name 1 language 0
39899 INFO: Appending PKG archive to EXE
39945 INFO: Fixing EXE headers
40245 INFO: Building EXE from EXE-00.toc completed successfully.
Deleting temp directory: C:\Users\ADMINI~1\AppData\Local\Temp\2503baab-24b5-4c2a-94d7-de342dfbeaca

distフォルダの中に、packaging_desktop_app.exeが作成されました。このファイルをダブルクリックすれば、作成したアプリケーションが起動します。
image.png

# ft.app(target=main, port=8550, view=ft.WEB_BROWSER)のコメントアウト
Desktopアプリなので、Chromeなどのブラウザで起動する設定(view=ft.WEB_BROWSER)で実装していた場合、実行できません!
私は最初、ブラウザで起動する設定でパッケージ化してしまいました。
実際に、exeファイルを実行すると、以下のウインドウが表示されてしまいます。
image.png

Publishing a static website with Pyodide

確認理由
作成したアプリケーションのデプロイ方法を理解するために、確認しました。
デプロイ先がclouflare Pagesということで、cloudflareを触るきっかけにもなります。

まず、デプロイする資材を用意します。以下コマンドを実行すると、main.pyとどう階層にdistディレクトリが生成されます。

flet publish main.py

distディレクトリ利用した動作確認を行います。問題なければデプロイ準備が完了します。

python -m http.server --directory dist

以降は、デプロイのための作業です。cloudflareにデプロイが可能なので、そこまで試してみました。ほんとに手順通りで完了しますので、サラッと書きます

  1. githubのリポジトリをテキトーに作成
  2. main.pyと同じ階層に、requirements.txtruntime.txt(※1)を作成
  3. requirements.txtruntime.txtに必要事項を記載
    requirements.txt
    flet==0.4.0
    
    runtime.txt
    3.10.5
    
  4. gitコマンド実行(add→commit→pushと実行)
  5. cloudflareで、Pagesを作成(Gitに接続をクリックし、手順1で作成したリポジトリを選択)
    image.png
  6. ビルド時の設定
    ビルドの構成は、参考サイトの手順通りです。
    ビルドシステムのバージョンは、2(ベータ版)を選択しました。

ビルドのデプロイの設定(設定後の画面)
image.png

※1 runtime.txtの作成について
記事作成時は、FletのDocsにPythonの最新が3.7と記載されてましたが、
ビルドシステムのバージョンをv2(ベータ版)に変更し、Python3.10.5でも動くか確認してみたところ、特に問題なかったので、そのまま進めました。
(個人で作成しているアプリケーションなので、ベータ版でもOK)

Before moving on, add a runtime.txt file in your repo. It should contain the python version to be used. In the file enter 3.7 which is the latest python version Cloudflare uses at time of writing.

Progressive web apps (PWA)

Progressive Web Apps, or PWAs, offer a way to turn app-like websites into website-like apps.
PWAという単語は聞いたことありましたが、これといってわかってなかったので、ちょうどいい機会でした。

実装ではなく、理論に対する理解を深める部分となりました。以下の内容について記載された記事でした。

  • PWAとは
  • PWAのメリット・デメリット
  • PWAを使うべきケースとは

実装詳細

今回ハンズオンで使用したコードは以下リポジトリで公開しています。

今後

冒頭にも今後の予定を記載しましたが、次はレイアウト部分を掘り下げる作業とcloudflareでデータベースを用いる作業を進めてみたいと思います。

Flet Docs (controls編) + cloudflareのWorkdersデプロイ(d1を添えて)

131
141
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
131
141

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?