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

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

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

# Fletとは
以下文章はFletの公式Docsから引用した内容です。`mobile`や`Flutter`という単語もありますね。
**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.

https://flet.dev/docs/#what-is-flet

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

https://github.com/flet-dev/flet

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

https://github.com/flet-dev/examples

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/fa7697f0-e255-f281-4271-2a9ce566a8c8.png)
# Fletを触ってみましょうか
PythonとFletのインストールのみですぐに開始できます。
[Python Tutorial](https://flet.dev/docs/guides/python/authentication)の項目のうち、以下内容が特に気になったので、確認を行いました。ある程度はハンズオン通りですが、全く同じでは実装していても‥、と思ったので、自分なりに少し実装をトッピングしています。（以下項目以外も実装している箇所はあると思います）
**確認のために実装したコードについて、記事の最後にリポジトリ情報を載せています。**
- Authentication
- Client storageとSession storage
- Encrypting sensitive data
- Hot reload
- Packaging desktop app
- Publishing a static website

### [Authentication](https://flet.dev/docs/guides/python/authentication)

:::note 

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

**認証前の画面**
ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/693d8b26-4c18-f105-4643-dc67470b85da.png)
**認証許可を求める画面**
みなさんも一度は見たことあるかと思いますが、認証の画面が表示されるので、緑色のボタンをクリックして、認証を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/7bcd2bce-1b80-b401-ac58-6f844de7ced4.png)
**認証後の画面**
認証が成功したので、ログアウト処理のボタンと、コンソールに自分のRepositoryを表示するボタンが表示されるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/b13d6f49-bc8d-c98c-521f-05e00db93a25.png)
logoutをクリックすれば、認証前の画面に戻ります。Show Reposをクリックすれば、リポジトリ一覧を確認できます。

コードは以下の通りです。（環境変数の設定は各自でお願いいたします。）
```python: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クラス)で定義した関数を外から呼び出す場合、自動的に第一引数にレシーバーが渡されます。それを引き取るための引数を定義する必要があります。

https://qiita.com/mizuno_jin/items/7c1f5a90dfb89bd64bff

### [Client storage](https://flet.dev/docs/guides/python/client-storage)と[Session storage](https://flet.dev/docs/guides/python/session-storage)
:::note 

**確認理由**
client StorageやSession Storageを理解することで、アプリケーションでの値管理が柔軟に行えるようになり、より実践的なアプリケーション作成が行えるようになります。
:::
**各ドキュメントをざっくり説明すると、以下の通りです。**
- Client Storageはサーバを止めて再起動しても、影響なし(値が保持される)
- Session Storageはサーバを止めて再起動すると、影響あり(値が保持されない)

:::note alert

**機密情報の扱い**
> 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/fca8df51-668a-56a2-cec6-fcabf1839ca6.png)

起動してそれぞれの`check!`ボタンを押して、値を表示すると、どちらも`None`と表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/5837568c-3257-2292-b247-93d1c283a31e.png)
それぞれの`set!`ボタンをクリック後、再度`check!`ボタンを押します。
それぞれで値(value)が設定できていることを確認しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/d7a18430-bdfc-142a-54f1-69422f5117be.png)
**一旦サーバを停止します。**

再度サーバを起動して、それぞれの`check!`ボタンを押します。
**Client Storage側では値が維持され、Session Storage側では値が維持されず、`None`に戻っていることを確認できます。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/c4ddbec1-b671-627a-81c4-dcabcf672f61.png)

Client Storageの値は、DevToolsでも確認してみました。Local Storageに値が保持されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/87bd7585-19b3-cbf9-c776-73980d06e531.png)

画面での挙動を見ていただきましたが、コードは以下の通りです。
```python: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](https://flet.dev/docs/guides/python/encrypting-sensitive-data)
:::note 

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/3b7b02c4-f347-7d60-cc29-d2b4ee797d13.png)
暗号化するテキストを上のテキストフィールドに入力して、ボタンをクリックすると、暗号化した値をコンソール出力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/dd299cbb-ce0a-91d8-000f-7cb712d981a6.png)
暗号化した値を下のテキストフィールドに入力して、上のテキストフィールドに入力した値がコンソールに出力されます。この値が上のテキストフィールド入力した値と一致します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/38dcd189-8d54-7161-84af-88e9ec7d0190.png)

コードは以下の通りです。
```python: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](https://flet.dev/docs/guides/python/hot-reload)

:::note 

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

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

### [Packaging desktop app](https://flet.dev/docs/guides/python/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.

:::note 

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

ハンズオンの中で作成した以下のアプリで、試してみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/0595270a-4869-f1ef-6f67-c46a0414037c.png)
```python: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)
```
:::note warn
**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
```
<details><summary>コンソールの出力内容</summary>

```text
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
```
</details>

distフォルダの中に、`packaging_desktop_app.exe`が作成されました。このファイルをダブルクリックすれば、作成したアプリケーションが起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/4a5478cc-e11c-9d80-3e1d-76eb3743fe6a.png)

:::note warn

**`# ft.app(target=main, port=8550, view=ft.WEB_BROWSER)`のコメントアウト**
Desktopアプリなので、Chromeなどのブラウザで起動する設定(`view=ft.WEB_BROWSER`)で実装していた場合、実行できません！
私は最初、ブラウザで起動する設定でパッケージ化してしまいました。
実際に、exeファイルを実行すると、以下のウインドウが表示されてしまいます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/2ad0188b-f2f4-8a76-8d19-b327f7ab4ffc.png)
:::

### [Publishing a static website with Pyodide](https://flet.dev/docs/guides/python/publishing-static-website)
:::note 

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

まず、デプロイする資材を用意します。以下コマンドを実行すると、main.pyとどう階層に`dist`ディレクトリが生成されます。
```
flet publish main.py
```
`dist`ディレクトリ利用した動作確認を行います。問題なければデプロイ準備が完了します。
```
python -m http.server --directory dist
```
以降は、デプロイのための作業です。cloudflareにデプロイが可能なので、そこまで試してみました。**ほんとに手順通りで完了しますので、サラッと書きます**

1. githubのリポジトリをテキトーに作成
2. `main.py`と同じ階層に、`requirements.txt`と`runtime.txt`(※1)を作成
3. `requirements.txt`と`runtime.txt`に必要事項を記載
    ```text:requirements.txt
    flet==0.4.0
    ```
    ```text:runtime.txt
    3.10.5
    ```
4. gitコマンド実行（add→commit→pushと実行）
5. cloudflareで、Pagesを作成(Gitに接続をクリックし、手順1で作成したリポジトリを選択)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/1d729b80-9727-d3f0-bda2-37422279d323.png)
6. ビルド時の設定
`ビルドの構成`は、参考サイトの手順通りです。
ビルドシステムのバージョンは、`2(ベータ版)`を選択しました。

https://developers.cloudflare.com/pages/platform/language-support-and-tools/

ビルドのデプロイの設定（設定後の画面）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/b5fd2c3f-1dae-446c-6e05-fc91d8a6507f.png)

:::note warn

※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)](https://flet.dev/docs/guides/python/deploying-web-app/progressive-web-apps)

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

実装ではなく、理論に対する理解を深める部分となりました。以下の内容について記載された記事でした。
- PWAとは
- PWAのメリット・デメリット
- PWAを使うべきケースとは

https://www.pcmag.com/how-to/how-to-use-progressive-web-apps

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

https://github.com/eno-conan/Flet-tutorial_cloudflare-pages-sample

# 今後
冒頭にも今後の予定を記載しましたが、次はレイアウト部分を掘り下げる作業とcloudflareでデータベースを用いる作業を進めてみたいと思います。
> Flet Docs (controls編) + cloudflareのWorkdersデプロイ(d1を添えて)

https://flet.dev/docs/controls

https://developers.cloudflare.com/d1/