990
1077

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-01-08

はじめに

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

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

trolli-app.gif
todo-complete-demo-web.gif

Flet の概要

image.png

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

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

なお、モバイルサポートについては現在開発中のようで、ドキュメントには
ロードマップとビジョンが掲載されています。

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

Flet のユースケース

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

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

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

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

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

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

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

Flutter の補足

https://flutter.dev/

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

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

Flet の基本

ほぼ 公式チュートリアル の日本語訳です。
絶賛開発中のライブラリなので、極力公式ドキュメントを参照するようにしてください。

インストール

pip install flet

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

Fletアプリの基本形

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

Control

Flet の UI は Flutter のモデルを簡略化した Control により構成されます。

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

コントロールは、以下のいずれかにより表示されます。

  • ページに追加する
  • 他のコントロールの中に追加する

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

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

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

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

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

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

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

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

プロパティの更新

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

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

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)

コンテナ

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

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

イベントハンドラ

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

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

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

visible プロパティ

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

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

disabled プロパティ

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

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

参照

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

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

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

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

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

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

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

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

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

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

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

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

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

公式ドキュメントでは、ビルド方法として Pyinstaller によるビルドが紹介されています。

ここではpyinstallerのGUIラッパーであるauto-py-to-exe を使ってビルドしてみます。

auto-py-to-exeでビルド

インストールします。

pip install auto-py-to-exe

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

auto-py-to-exe

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

Pasted image 20230104092247.png

必要事項を入力します。

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

Pasted image 20230104092336.png

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

しばらくして、下のような表示が出ればOKです。
「OPEN OUTPUT FOLDER」 をクリックすると、出力先のフォルダに飛びます。
Pasted image 20230104093133.png

ビルド結果

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

auto-py-to-exe-result

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

Webアプリとしてデプロイ

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

Web socket

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

デプロイ先として 公式ドキュメント で紹介されていたのは fly.ioReplit です。
この記事では fly.io について扱います。

fly.io

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

  • Dockerfile が使えるので特定の言語やランタイムに依存しない
  • デプロイしたアプリは各地のEdge Serverで実行され、高速なレスポンスが期待できる
  • 東京Regionがある
  • 特別な設定なしで自動的にHTTPSなURLを提供してくれる
  • マネージドなPostgreSQLがある(無料枠付き!
  • 同じ組織からデプロイしたアプリはWireGuardのVPNを共有し、<app-name>.internal アドレスでお互いアクセスできる

fly.io に Pleroma を設置する - castaneaiのブログ より

fly にログイン

Install flyctl · Fly Docs を参考に、flyctl をインストールします。

brew install flyctl

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

flyctl auth login

デプロイ準備

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

  • requiments.txt
  • fly.toml
  • Dockerfile
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"

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 アプリをビルド、デプロイ出来ます。

flyctl apps create --name flet-getting-started

flyctl deploy

確認してみます。

flyctl apps open

Pasted image 20230104151822.png

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

削除

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

flyctl apps destroy flet-getting-started

まとめ

以上、Fletの紹介でした。

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

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

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

参考

990
1077
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
990
1077

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?