Pythonだけでクロスプラットフォームなアプリを作れるFletについて

# はじめに

Python だけでクロスプラットフォームなアプリを作ることが出来る、[Flet](https://flet.dev/) というフレームワークについての記事です。

Pythonだけで次のようなWeb・デスクトップに両対応したアプリを作ることが出来ます。

![trolli-app.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1224564/c1d7acb3-6c17-4523-155d-f402fe2dd9f9.gif)
![todo-complete-demo-web.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1224564/e4b2fd54-ece1-372c-d21a-899082557564.gif)

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

## Flet の概要

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1224564/f35ea378-8e4f-1952-729a-295b8c453f1a.png)

Flet は Flutter をベースにしています。主に以下のような特徴があります。

- **From idea to app in minutes**
	- 「素早くGUIアプリを作成出来る」ことが、Fletの主なセールスポイントのようです。
- **Simple Architecture**
	- JSフロントエンドやRestAPIを書くこと無く、PythonだけでSPAを作る事が出来ます。
- **Batteries included**
	- [Batteries Included](https://en.wikipedia.org/wiki/Batteries_Included) は、Pythonの設計思想のようです。電池が付属している、つまりそのままでもすぐに動かせることを指します。
- **Powered by Flutter**
	- Flet の UI は [Flutter](https://flutter.dev/) で構成されているので、綺麗なUIを提供でき、マルチプラットフォームに対応しています。
- **Speaks your language**
	- Flet は言語に依存しないことを目指しています。現在サポートされているのはPythonのみですが、今後 Go や TypeScript、 C# にも対応していく予定のようです。
- **Deliver to any device**
	- 複数のプラットフォーム(iOS、Android、Web、Windows、Mac、Linux)に対応しています。

なお、モバイルサポートについては現在開発中のようで、[ドキュメント](https://flet.dev/docs/guides/python/mobile-support)には
ロードマップとビジョンが掲載されています。

そのため、この記事ではデスクトップアプリへのビルド、Webアプリとしてのデプロイのみを扱います。

### Flet のユースケース

ざっくりまとめると、「使い慣れている単一の言語で」「クロスプラットフォームな」アプリを開発することが出来るので、スピードを重視する時に特に威力を発揮すると思われます。

社内向けのツール開発などに適していると言えそうです。

:::note info
https://flet.dev/docs/tutorials/trello-clone#why-flet の要約

多くの開発者は、次のような場面に遭遇することが多いでしょう。

- 社内の非開発者向けに、比較的小規模のアプリを開発する必要がある
- CUI アプリを開発したら当初の予想よりも多くの人に使ってもらえることが分かったため、GUI化したい

このような状況で求められるのは主にスピードなので、electronのように巨大なツールやflutterのような機能豊富なフレームワーク、.NET MAUI などのクロスプラットフォームなフレームワークは適さないことが多いです。

Flet は、上のようなユースケースにおいて、見栄えが良く、ある程度パフォーマンスも良好で、手早くGUI アプリを提供することが出来ます。
:::

:::note info
Flutter の補足

https://flutter.dev/

Flutter は、 Google 社が開発しているオープンソースのフレームワークです。
主な特徴には以下のようなものがあります。

- クロスプラットフォームに対応しており、ワンソースで複数のプラットフォーム（iOS、Android、Web、Windows、Mac、Linux）に対応したアプリを開発することが出来る
- Dart言語により開発する
- 高速な動作
- ホットリロードのサポート
:::

## Flet の基本

ほぼ [公式チュートリアル](https://flet.dev/docs/guides/python/getting-started) の日本語訳です。
絶賛開発中のライブラリなので、極力公式ドキュメントを参照するようにしてください。

### インストール

```sh
pip install flet
```

Linux, WSL の場合は追加でパッケージのインストールや設定が必要です。
https://flet.dev/docs/guides/python/getting-started#linux を参照してください。

### Fletアプリの基本形

```python
import flet as ft

def main(page: ft.Page):
    # add/update controls on Page
    pass

ft.app(target=main)
```

- `main` 関数は Flet アプリケーションのエンドポイントです。この関数には `flet.Page` インスタンスが渡されます。
- `flet.app` の呼び出しにより、アプリケーションとして実行されます。
	- デフォルトではデスクトップアプリとして起動されます。
	- `flet.app(view=ft.WEB_BROWSER)` のように変更することで、ブラウザで起動することが出来ます。

### 内部的にはWebアプリ

全ての Flet アプリは内部的には Web アプリで、ネイティブ OS のウィンドウで開かれたとしても、ビルトインのWebサーバーがバックグラウンドで起動しています。

Flet の Webサーバーは **Fletd** と呼ばれ、デフォルトではランダムな TCPポートでリッスンしています。

`flet.app(port=8550, target=main)` のようにポートを指定して起動、ブラウザで `http://localhost:8550` を開くと、FletアプリのWebバージョンが表示されます。

![Pasted image 20230103235053.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1224564/422e09bc-7eb6-c530-d416-ee4cc9f100aa.png)

## Control

Flet の UI は Flutter のモデルを簡略化した [Control](https://flet.dev/docs/controls/) により構成されます。

※ 以下「Control」は「コントロール」とします

コントロールは、以下のいずれかにより表示されます。
- ページに追加する
- 他のコントロールの中に追加する

**ページ**は最上位のコントロールです。
コントロールの入れ子は、**ページをルートとするツリー**として表現されます。

### コントロールのインスタンスを生成

コントロールはクラスとして実装されています。
例えば、 `ft.Text` のインスタンスを生成するには次のようにします。

```python
t = ft.Text(value="Hello, world!", color="green")
```

### ページにコントロールを追加

ページにコントロールを追加するには、`page.controls`にコントロールのインスタンスを追加し、`page.update` を呼び出します。

```python
import flet as ft


def main(page: ft.Page):
    # Text Control 生成
    t = ft.Text(value="Hello, world!", color="green")
    # ページのコントロールリストに Control を追加
    page.controls.append(t)
    # ページを更新
    page.update()


ft.app(target=main)
```

上のコードにより、次のようなUIが生成されます。

![Pasted image 20230104000342.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1224564/168f359b-08a8-6f7c-4f8e-651b517234c6.png)

`page.add(control)` という `page.controls.append(control)` , `page.update()` をまとめた関数も用意されています。

### プロパティの更新

コントロールのプロパティを更新した後、 `page.update` を呼び出すことでそれがページに反映されます。

次のコードでは、0 ~ 9 まで、 1秒ごとにテキストが更新されるテキストが生成されます。

```python
import time
import flet as ft

def main(page: ft.Page):
    t = ft.Text()
    page.add(t)

    for i in range(10):
        t.value = f"Step {i}"
        page.update()
        time.sleep(1)

ft.app(target=main)
```

### コンテナ

`Page` や `Row` 、 `Column` のように、他のコントロールを入れ子に出来るコントロールもあります。

```python
import flet as ft

def main(page):
    row = ft.Row(controls=[ft.Text("A"), ft.Text("B"), ft.Text("C")])
    column = ft.Column(controls=[row, ft.Text("D")])
    page.add(column)

ft.app(target=main)
```

![Pasted image 20230104154411.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1224564/985a9a56-4119-e519-e24d-5189ea2129a1.png)

### イベントハンドラ

ボタンなどのコントロールは、 `ElevatedButton.on_click` のように、ユーザの入力で発火するイベントハンドラを持つことが出来ます。

```python
def button_clicked(e):
    page.add(ft.Text("Clicked!"))

page.add(ft.ElevatedButton(text="Click me", on_click=button_clicked))
```

### `visible` プロパティ

表示/非表示を切り替えるプロパティです。全てのコントロールにデフォルトで `True` が設定されています。

`visible` を `False` にすると、表示されなくなることは勿論、キーボードやマウスにより選択できず、イベントも発生しなくなります。

### `disabled` プロパティ

`TextField`, `Dropdown`, ボタンなど、ユーザから入力を受け取るコントロールで主に使用されます。

`disabled` は親コントロールに設定することができ、その値は再帰的に全ての子コントロールに伝搬されます。

### 参照

コントロールはオブジェクトなので、そのプロパティにアクセスするには、それらのオブジェクトへの参照(変数)を保持する必要があります。

次のような例を考えてみましょう。

```python
import flet as ft

def main(page):

    first_name = ft.TextField(label="First name", autofocus=True)
    last_name = ft.TextField(label="Last name")
    greetings = ft.Column()

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

    page.add(
        first_name,
        last_name,
        ft.ElevatedButton("Say hello!", on_click=btn_click),
        greetings,
    )

ft.app(target=main)
```

このコードは次のようなページを生成します。
![RefExample](https://user-images.githubusercontent.com/85564407/210390595-5a82d8fb-b5d0-48ab-9303-31d7eae8f0b2.gif)

`main` 関数の冒頭で、 `on_click` で使用する3つのコントロールを作成しています。

この調子でコントロールやイベントハンドラをどんどん追加していくと、コントロールの定義を一箇所にまとめることが難しくなってしまいそうです。
その結果、 `pages.add()` のパラメータを見ただけでは、どのようなページが出来るのか想像するのが難しくなってしまうかもしれません。 

このような問題を解消するために、 Flet にはコントロールへの参照を定義し、イベントハンドラでその参照を使用、後でページを構築する際に実際のコントロールへの参照を設定することが出来る `Ref` というユーティリティクラスが用意されています。(お察しの通り、Reactから着想を得ているようです。)

`Ref` を定義するには、次のようにします。

```python
first_name = ft.Ref[ft.TextField]()
```

参照をコントロールに割り当てるには、以下のようにします。

```python
page.add(  
	ft.TextField(ref=first_name, label="First name", autofocus=True)  
)
```

`Ref` を使って先程の例を書き換えると、次のようになります。

```python
import flet as ft


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

これで、 `page.add` の呼び出しを見るだけでページの構造がわかるようになりました。

### 命令形 UI

ここまでサンプルを見てきて、フロントエンド開発経験のある方は「Flutterベースなのに宣言型じゃないの？」という疑問を持たれた方も居るのではないでしょうか。

Flutterを始めとして、現在のフロントエンド開発で使われるフレームワークでは、データが変更されると自動的にUIが再構築される宣言的なモデルが採用されることが多いです。

一方で、Flet は手動でプロパティを更新することで UI をコントロールする命令形のモデルを実装しています。

あえてこの「古い」アプローチを採用することで、フロントエンドの開発経験がないプログラマーの開発ハードルを下げる狙いがあるようです。

> Flet implements imperative UI model where you "manually" build application UI with stateful controls and then mutate it by updating control properties. Flutter implements declarative model where UI is automatically re-built on application data changes. Managing application state in modern frontend applications is inherently complex task and Flet's "old-school" approach could be more attractive to programmers without frontend experience.
> 
> https://flet.dev/docs/guides/python/getting-started


## デスクトップアプリにビルド

[公式ドキュメント](https://flet.dev/docs/guides/python/packaging-desktop-app)では、ビルド方法として Pyinstaller によるビルドが紹介されています。

ここではpyinstallerのGUIラッパーである[auto-py-to-exe](https://github.com/brentvollebregt/auto-py-to-exe) を使ってビルドしてみます。

### auto-py-to-exeでビルド

インストールします。

```sh
pip install auto-py-to-exe
```

コマンドを叩いて起動します。

```sh
auto-py-to-exe
```

起動すると下のようなウィンドウが表示されると思います。

![Pasted image 20230104092247.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1224564/3cceb432-f908-33d5-1e81-7d813046d0ba.png)

必要事項を入力します。

- Script Location : 配布したいPythonファイルを指定します。 ( ここでは `main.py` ) 
- Onefile : One file を選択すると、文字通り１つのファイルとして出力されます。
- Console Window : コンソールを表示するかどうか選べます。GUIアプリの場合は表示しない方が良いでしょう。

![Pasted image 20230104092336.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1224564/1759b609-b8d8-2ec3-1e44-1ea5c808d77a.png)

入力出来たら「CONVERT...」 をクリックします。

しばらくして、下のような表示が出ればOKです。
「OPEN OUTPUT FOLDER」 をクリックすると、出力先のフォルダに飛びます。
![Pasted image 20230104093133.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1224564/9e6cd6a1-5f8c-350f-4862-0f3e1da4d795.png)

### ビルド結果

無事にファイル単体で実行出来るようになりました。

![auto-py-to-exe-result](https://user-images.githubusercontent.com/85564407/210463174-34634593-d914-4bee-ab80-af7be86c2ca3.gif)

手元のWindows環境でも試してみましたが、同じように動作しました。

## Webアプリとしてデプロイ

Web アプリとしてデプロイすることも出来ます。

### Web socket 

Flet は UIのリアルタイム部分更新と、プログラムへのイベント送信にWebSocketを使用します。
そのため、ホスティングプロバイダを選択するときには WebSocket に対応しているか確認する必要があります。

デプロイ先として [公式ドキュメント](https://flet.dev/docs/guides/python/deploying-web-app) で紹介されていたのは [fly.io](https://flet.dev/docs/guides/python/deploying-web-app/fly-io) と [Replit](https://flet.dev/docs/guides/python/deploying-web-app/replit) です。
この記事では fly.io について扱います。

### fly.io

fly.io は以下のような特徴があります。

> - `Dockerfile` が使えるので特定の言語やランタイムに依存しない
> -   デプロイしたアプリは各地のEdge Serverで実行され、高速なレスポンスが期待できる
> -   東京Regionがある
> -   特別な設定なしで自動的にHTTPSなURLを提供してくれる
> -   マネージドなPostgreSQLがある（[無料枠付き！](https://fly.io/blog/free-postgres/)）
> -   同じ組織からデプロイしたアプリはWireGuardのVPNを共有し、`<app-name>.internal` アドレスでお互いアクセスできる
> 
> [fly.io に Pleroma を設置する - castaneaiのブログ](https://castaneai.hatenablog.com/entry/pleroma-on-fly-io) より

### fly にログイン

[Install flyctl · Fly Docs](https://fly.io/docs/hands-on/install-flyctl/) を参考に、`flyctl`  をインストールします。

```sh
brew install flyctl
```

次のコマンドで fly にログインして( Github認証が使えます )、支払情報を登録します。

```sh
flyctl auth login
```

### デプロイ準備

ここでは以下のファイルを用意します。

- `requiments.txt`
- `fly.toml`
- `Dockerfile`

```txt:requiments.txt
flet>=0.1.33
```

`flet-getting-started` のところは好きな名前に変更してください。

```fly.toml
app = "flet-getting-started"

kill_signal = "SIGINT"
kill_timeout = 5
processes = []

[env]
  FLET_SERVER_PORT = "8080"

[experimental]
  allowed_public_ports = []
  auto_rollback = true

[[services]]
  http_checks = []
  internal_port = 8080
  processes = ["app"]
  protocol = "tcp"
  script_checks = []

  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = "connections"

  [[services.ports]]
    force_https = true
    handlers = ["http"]
    port = 80

  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

  [[services.tcp_checks]]
    grace_period = "1s"
    interval = "15s"
    restart_limit = 0
    timeout = "2s"

```

```dockerfile
FROM python:3-alpine

WORKDIR /app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8080

CMD ["python", "./main.py"]
```

### デプロイ

次のコマンドで Fly アプリをビルド、デプロイ出来ます。

```sh
flyctl apps create --name flet-getting-started

flyctl deploy
```

確認してみます。

```sh
flyctl apps open
```

![Pasted image 20230104151822.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1224564/704b03a0-3817-aefd-f516-58f92af4cd77.png)


無事デプロイ出来ました。

### 削除

不要になったアプリは削除しておきましょう。

```sh
flyctl apps destroy flet-getting-started
```

## まとめ

以上、Fletの紹介でした。

私は非開発者向けの簡単なツールを作ることがよくあるので、Pythonで少しコードを書けば、Web・デスクトップに対応したアプリを作ることができるという点はとても魅力的です。モバイル対応も楽しみです。

余談ですが、この記事を書く際に今話題の [ChatGPT](https://chat.openai.com/chat) を使ってみました。
「箇条書きに要約して」「添削して」などの言葉の後に[DeepL](https://www.deepl.com/translator)で翻訳したドキュメントを突っ込むことで、ざっくりした概要を把握することが出来ます。

原文を読まないと意味が分からなかったり、「要約して」の時に重要なのに捨てられてしまう箇所が多少あったりしますが、初めて扱うライブラリのドキュメントを読む時にはかなり重宝しそうです。

## 参考

- 公式ドキュメント : https://flet.dev/docs/
    - コントロールのリファレンス : https://flet.dev/docs/controls
    - Todoアプリ、Trelloクローンなどの簡単なアプリ作成 : https://flet.dev/docs/tutorials
- サンプルアプリのリポジトリ : https://github.com/flet-dev/examples/tree/main/python