👣

Flet でアイデアボードアプリ作ってみた

2024/11/24に公開

はじめに

今回はFletというマルチプラットフォーム対応のアプリケーションを作成できるpythonのフレームワークを使って簡単なアプリケーションを作りました!以下が完成したアプリケーションになります!
 本ブログでは、私がFletをはじめて使って作成したアプリケーションについて開発環境構築からアプリケーション公開までの過程をざっくり説明しております。

https://github.com/takeru-a/flet_idea_board/

FletはWebアプリ、デスクトップアプリ(Windows, Mac, Linux)、スマホアプリ(ios, android)などマルチプラットフォームで動作するアプリケーションを作ることができ、python版のFlutterみたいなフレームワークです。
https://flet.dev/
 実際Flutterとの親和性は高くFlutterのUIを使えたり、Flutterのサードパーティパッケージを使えたりします。
マルチプラットフォーム対応ではあるのですが、WindowsからはMac/ios用のパッケージはビルドできないなど、ビルドする環境によって特定のプラットフォーム用のパッケージをビルドできるかどうかの制限があります。
詳細は以下の公式ドキュメントを参照してください
https://flet.dev/docs/publish

環境構築

はじめに、このアプリケーションを開発する専用の仮想環境を構築します。
仮想環境を構築することでpythonのバージョンやパッケージのバージョンをアプリごとに独立させることができ、そのアプリケーションに合わせてバージョンを変更、管理することができます。
独立しているので、他の環境に影響を及ぼすことがないです。
今回はpyenv(pyenv-win)+venvを使ってこのアプリケーションを開発する専用の仮想環境を構築していきます。

pythonのバージョン管理

pythonのバージョン管理ツールとしてpyenvを使いたいと思います。
ただ標準のpyenvはUNIX/MacOSのみにしか対応しておらず、Windowsでは使用できません。
代わりにpyenvからforkされたpyenv-winというツールを使います。

pyenv-winインストール

それではpyenv-winをインストールします。PowerShellを起動し、以下のコマンドを実行します。

PowerShell
Invoke-WebRequest -UseBasicParsing -Uri "https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1" -OutFile "./install-pyenv-win.ps1"; &"./install-pyenv-win.ps1"

以下の表示されたら、PowerShellを開き直します。

以下のコマンドを実行し、インストールできたか確認します。バージョンが表示されればインストール成功しています!

PowerShell
pyenv --version

pythonインストール

それでは今回使用するpythonをインストールしていきます。
今回はバージョン3.12.0のpythonをインストールし、それをこのプロジェクトで使用するように指定します。

PowerShell
# インストールできるバージョンを確認
pyenv install -l
# インストール
pyenv install 3.12.0
# 開発プロジェクトのフォルダまで移動後
# このプロジェクトで使用するpythonのバージョンを指定
pyenv local 3.12.0

以下のようなファイルが開発プロジェクトフォルダ配下に生成されていれば、pythonバージョンの固定化ができています。

実際に特定のバージョンのpythonを実行できるか確認します。Hello World!という文字と使用したいバージョンのpythonのパスが表示されればOKです!

PowerShell
pyenv exec python -c "import sys; print('Hello World!\n' + sys.executable)"

全体で使用するpythonのバージョンを指定したい時ときは以下のコマンドで設定します。localで特定のバージョンを指定していない場合はこのバージョンのpythonが使用されます。デフォルトで使用するpythonバージョンがあればこちらで指定すると良いです。

PowerShell
# 全体で使用するpythonのバージョンを指定
pyenv global 3.12.0

上記の方法が最も簡単ですが、別のインストール方法もあるので状況に合わせてインストール方法を選択してください。githubに他のインストール方法は記載されてます。
https://github.com/pyenv-win/pyenv-win?tab=readme-ov-file#installation

パッケージバージョン管理

パッケージバージョン管理ツールとしてvenvを使用します。
python -m venv [仮想環境名]というコマンドで新しく仮想環境を作成することができます。
プロジェクトディレクトリ直下移動して、以下のコマンドを実行し、仮想環境を作成します。
今回は仮想環境名はvenvで作成します。

PowerShell
# 仮想環境(venv)作成
pyenv exec python -m venv venv
# 確認(venvが作成されている)
ls
# 有効化
.\venv\Scripts\activate
# pythonのバージョンを確認
python -V

仮想環境が有効化されていると以下のような表記になります。

以下が仮想環境を操作するコマンドになります。

PowerShell
# 仮想環境作成
python -m venv [仮想環境名]

# 仮想環境有効化
.\[仮想環境名]\Scripts\activate

# 仮想環境無効化
deactivate

これで開発環境構築を構築することができたので、開発を進めていきましょう!

実装

それではfletを使ってアプリケーションを作っていきます。今回は簡単なアイデアボードアプリを作成します。
 頭の中にあるアイデアや考えごとを書き出し整理するために使用することを想定してプロダクトを作成します。
成果物イメージとしてはmiroexcalidrawに近いものになります。

以下の機能を実装します。

  • アイディアボード作成
  • お描き機能
  • 消しゴム機能
  • オブジェクト追加機能
  • オブジェクトドラッグ移動機能
  • オブジェクト削除機能
  • オブジェクト拡大機能

以下のコマンドでfletをインストールします。仮想環境を有効化してから実行してください。

# インストール
pip install flet

# 確認
flet --version

fletのダウンロード完了できれば機能を追加していきましょう。アプリケーションバー、サイドバーエリアなどレイアウト部分に関してはfletの公式で出されているtrelloのクローンアプリのレイアウトを参考にしました。
https://github.com/flet-dev/examples/tree/main/python/apps/trolli
本ブログではすべての機能について説明しておりません。本アプリケーションの全コードはgithub上にありますので、詳細はコードをご確認ください。
https://github.com/takeru-a/flet_idea_board/

READMEに本アプリケーションの簡単な操作マニュアルを記載しております。機能の概要や操作イメージはそちらをご覧ください。

お描き機能、消しゴム機能

お描き機能はアイデアボードにマウスドラッグによって線を描くことができる機能で、消しゴム機能は同じくマウスドラッグによってお描き機能で追加した線を削除することができる機能です。
お描き機能は画面上部のコントロールバーのペンのアイコンを、消しゴム機能はゴミ箱のアイコンを選択することで使うことができます。

 Fletはコントロール(ウィジェット)という要素で画面を構成しています。
お描き機能と消しゴム機能はFletのCanvasというコントロールを使用して実装しています。
https://flet.dev/docs/controls/canvas
Canvasというカスタムコントロールを作成してお描き機能、消しゴム機能のロジックを記載していきます。

canvas.py
from flet import (
    UserControl, 
    GestureDetector, 
    DragStartEvent, 
    DragUpdateEvent, 
    Paint,
    PaintingStyle,
    colors
)
import flet.canvas as cv
from state import State

class Canvas(UserControl):
    def __init__(self, page, state: "State"):
        super().__init__()
        self.page = page
        self.state = state
        self.cp = cv.Canvas(
            [],
            content=GestureDetector(
                on_pan_start=self.pan_start,
                on_pan_update=self.pan_update,
                drag_interval=10,
            ),
            expand=True,
            width=self.page.width,
            height=self.page.height,
            on_resize=self.on_resize
        )
    def build(self):
        return self.cp

cv.Canvasのcontentには子コントロールを指定でき、ここではGestureDetectorというユーザの操作を検知するコントロールを指定しています。
GestureDetectorの引数にイベント発生時に発火する関数を設定できます。お描き機能、消しゴム機能を実装するためにマウスポインターのドラッグ開始時のイベントの処理を設定するon_pan_startとドラッグ時の処理を設定するon_pan_updateを定義します。

以下がon_pan_startに設定する関数で、ドラッグ開始時のx,y座標を保存しています。
Stateクラスにはマウスポインタのx,y座標やお描きモード、削除モードなどボードの現在のモードを保存しています。

canvas.py
def pan_start(self, e: DragStartEvent):
    if self.state.mode == "drawer" or self.state.mode == "delete":
        self.state.x = e.local_x
        self.state.y = e.local_y

以下がon_pan_updateに設定する関数で、ドラッグ開始時に保存したx,y座標と現在のマウスポインターのx,y座標を結ぶ線をcv.Line関数を使って描く処理を行い、self.cp.update()で線をキャンバスに反映させています。
最後に線を書いた時点でのポインタの座標をStateに保存し、次のドラッグイベント発生時にその座標を使って直線を描きます。こうすることでマウスドラッグで自由に線を描くことができます。

canvas.py
def pan_update(self, e: DragUpdateEvent):
    if self.state.mode == "drawer":
        self.cp.shapes.append(
            cv.Line(
                self.state.x, self.state.y, e.local_x, e.local_y, paint=Paint(color=colors.BLACK87 ,stroke_width=3)
            )
        )
        self.cp.update()
        self.state.x = e.local_x
        self.state.y = e.local_y
    elif self.state.mode == "delete":
        self.cp.shapes.append(
            cv.Line(
                self.state.x, self.state.y, e.local_x, e.local_y, paint=Paint(color=colors.WHITE ,stroke_width=20, style=PaintingStyle.FILL)
            )
        )
        self.cp.update()
        self.state.x = e.local_x
        self.state.y = e.local_y

お描き機能と消しゴム機能はコード的にはほとんど処理は変わらず、消しゴムの場合はキャンバスと同じ色の太い線を描いているだけです。ボードのモードによって処理を分岐させています。

オブジェクト追加、削除機能

四角、丸形、矢印の3つのオブジェクトを追加、削除するする機能を実装しました。
追加機能は画面上部にあるコントロールバーから追加したいオブジェクトのアイコンを選択し、ボードの任意の場所をクリックすることで追加できます。
削除機能はゴミ箱のアイコンを選択し、削除したいオブジェクトをクリックすることでオブジェクトの削除を行えます。

ボード

追加する先のボードはStackコントロールを使用して実装します。引数のcontrolsに追加するオブジェクトをリスト形式で指定しています。Stackコントロールは子コントロール(追加するオブジェクト)を重ねることができ、リストの要素が大きいほど上に表示されます。
ボードにはデフォルトで追加されている要素があり、お描き用のキャンバス、オブジェクト追加検知用のGestureDetector、コントロールバーの3つは最初から配置されています。
コントロールバーは常に一番上に表示したいので追加するオブジェクトはコントロールバーの前の要素に格納しています。

board.py
self.board = Stack(
    controls = [
        Row(
            controls = [self.cp],
            vertical_alignment="start",
            scroll="auto",
            expand=True,
        ),
        self.bg,
        self.controlbar,
    ],
    expand=True,
    width=(self.page.width),
    height=(self.page.height),
)

オブジェクト追加検知用のGestureDetectorについて、コントロールバーで四角、丸形、矢印のどれかが選択されている状態で画面がクリックされたことを検知します。画面がクリックされた場合はon_tap_downに指定されている関数が実行されます。

board.py
self.bg = GestureDetector(
            on_tap_down=self.add_obj,
            on_tap_up=self.add_obj_after,
            width=(self.page.width),
            height=(self.page.height),
            top=70,
            )

四角、丸形

四角、丸形はContainerコントロールを使用して実装しており、違いは引数のshapeに指定している値がことなるだけになります。また追加したオブジェクトにはテキストを入力することができます。
add_obj関数によってオブジェクトを追加します。ボードのモードによって追加するオブジェクトの処理を分岐させています。topとleftにイベント発生時のマウスカーソルのx,y座標を設定することで、クリックした場所にオブジェクトを追加しています。

board.py
def add_obj(self, e: TapEvent):
    # 四角のオブジェクトを追加
    if self.state.mode == "squares":
        id = len(self.board.controls) - 1
        self.board.controls.insert(id, GestureDetector(
            mouse_cursor=MouseCursor.MOVE,
            drag_interval=10,
            on_pan_update=self.move_obj,
            on_scale_update=self.resize_obj,
            on_tap=self.delete_obj,
            content=Container(
                content=TextField("", color=colors.BLACK, multiline=True, border="None"),
                bgcolor=colors.GREY_50,
                border_radius=2,
                border=border.all(1, colors.BLACK),
                shadow=BoxShadow(color=colors.GREY_400, spread_radius=0.2, blur_radius=0.2, offset=(1, 1)),
                alignment=alignment.center,
            ),
            width=100,
            height=100,
            top=e.local_y,
            left=e.local_x,
        ))
    # 円形のオブジェクトを追加
    elif self.state.mode == "circle":
        id = len(self.board.controls) - 1

先述した通り、コントロールバーの前の要素に追加しています。
ここで追加しているのは、Containerコントロールを子要素としてもつGestureDetectorコントロールで、GestureDetectorコントロールによってマウス操作可能なオブジェクトを追加することができます。Containerコントロールで追加するオブジェクトの形を決めており、子要素として入力可能なテキストボックスを持っています。
追加するオブジェクトは以下のような入れ子構造になっています。

board.py
id = len(self.board.controls) - 1
self.board.controls.insert(id, GestureDetector(
    mouse_cursor=MouseCursor.MOVE,
    drag_interval=10,
    on_pan_update=self.move_obj,
    on_scale_update=self.resize_obj,
    on_tap=self.delete_obj,
    content=Container(
        content=TextField("", color=colors.BLACK, multiline=True, border="None"),

削除処理に関して、追加している各オブジェクトのContainerの引数であるon_tapに削除処理を行う関数delete_objを指定しており、オブジェクトが削除モードの際にクリックされたらリストからオブジェクトを削除します。

board.py
def delete_obj(self, e: TapEvent):
    if self.state.mode == "delete":
        self.board.controls.remove(e.control)
        self.update()

矢印

矢印のオブジェクトはadd_objで新しいArrowインスタンスを生成し、それを追加させています。
Arrowは独自に実装したカスタムコントロールでCanvasコントロールを使用して、矢印を描きそれをドラッグ操作可能にコントロールとして実装しております。Arrowコントロールはarrow.pyに記載しております。

board.py
# 矢印のオブジェクトを追加
elif self.state.mode == "arrow":
    id = len(self.board.controls) - 1
    arrow = Arrow(self.page, self.state, self.board, id, top=e.local_y, left=e.local_x)
    self.board.controls.insert(id, arrow)

削除機能には他のオブジェクトとは異なりArrowコントロール独自で実装されており、Arrowクラスで定義されています。
矢印オブジェクトを削除モードの際にクリックすると以下の関数の処理が行われます。

arrow.py
def delete_arrow(self, e):
    if self.state.mode == "delete":
        self.board.controls = [control for control in self.board.controls if not (isinstance(control, Arrow) and control.arrow_id == self.arrow_id)]
        self.board.update()

オブジェクト操作

追加されたオブジェクトを移動させたり、拡大縮小させたりする機能を実装しました。
矢印オブジェクトに関しては、移動とリサイズ機能に加えて矢印の向きを変更させる機能も実装しております。四角、円形オブジェクトと矢印オブジェクトでは処理方法が異なります。

四角、円形の操作について

四角、円形のオブジェクトは右クリックをしながらドラッグ操作をすると移動ができ、右クリックをしながらドラッグ操作をするとリサイズを行うことができます。
追加したオブジェクトのGestureDetectorの引数であるon_pan_updateに左クリックドラッグイベント発生時の処理関数を指定し、on_scale_updateに右クリックドラッグイベント発生時の処理関数を指定します。

board.py
GestureDetector(
    mouse_cursor=MouseCursor.MOVE,
    drag_interval=10,
    on_pan_update=self.move_obj,
    on_scale_update=self.resize_obj,

移動の処理は以下の関数で定義しており、ドラッグ時のマウスポインターの移動量に合わせてオブジェクトの位置を更新しています。

board.py
def move_obj(self, e: DragUpdateEvent):
    # 対象のコントロールの位置をドラッグイベントの移動量に応じて更新
    e.control.top = min(self.page.height - 100, max(0, e.control.top + e.delta_y))
    e.control.left = min(self.page.width, max(0, e.control.left + e.delta_x))
    e.control.update()

リサイズの処理は以下の関数で定義しており、ドラッグ時のマウスポインターの移動量に合わせてオブジェクトの縦横の大きさを更新しています。

board.py
def resize_obj(self, e: ScaleUpdateEvent):
    # 対象のコントロールのサイズをドラッグイベントの移動量に応じて更新
    e.control.width = max(0, e.control.width + e.focal_point_delta_x)
    e.control.height = max(0, e.control.height + e.focal_point_delta_y)
    e.control.update()

矢印の操作について

矢印オブジェクトは少し操作が特殊で、ダブルクリックによって操作が分岐されます。
ダブルクリックなしで左クリックドラッグ操作で、矢印の移動ができ、ダブルクリック後に左クリックドラッグ操作でリサイズ処理を行います。ダブルクリックされたかどうかの状態を保持しており、ダブルクリックすることで、移動⇔リサイズの処理を変更できます。
また、右クリックでドラッグ操作することで、矢印の向きを変更することができます。
Arrowコントロールは子要素として、GestureDetectorコントロールをもつCanvasで構成されています。GestureDetectorコントロールのon_pan_updateで、左クリックドラッグ時の処理関数を指定し、
on_scale_start&on_scale_updateで右クリックドラッグ時の処理関数を指定しています。on_double_tapで矢印をダブルクリックしたときに実行するchange_control関数を指定しており、ダブルクリックの状態をスイッチしています。

arrow.py
class Arrow(UserControl):
    def __init__(self, page, state: "State", board, id: int, top: float, left: float):
        super().__init__()
        self.page = page
        self.board = board
        self.state = state
        self.double_tap = False
        self.arrow_id = id
        self.arrow: "Arrow" = cv.Canvas(
                        [
                        # 矢印の軸を描画
                        cv.Path(
                            [
                                cv.Path.MoveTo(50, 50),  # 矢印の元
                                cv.Path.LineTo(150, 50),  # 矢印の軸
                                cv.Path.LineTo(140, 40),  # 矢印の頭の上部分
                                cv.Path.MoveTo(150, 50),  # 矢印の元に戻る
                                cv.Path.LineTo(140, 60),  # 矢印の頭の下部分
                            ],
                            paint=Paint(
                                stroke_width=3,
                                color=colors.BLACK,
                                style=PaintingStyle.STROKE,
                            ),
                        ),
                        ],
                        content=GestureDetector(
                            mouse_cursor=MouseCursor.CLICK,
                            on_pan_update=self.update_arrow,
                            on_double_tap=self.change_control,
                            on_tap=self.delete_arrow,
                            on_scale_start=self.rotate_arrow_before,
                            on_scale_update=self.rotate_arrow,
                            drag_interval=10,
                            width=150,
                            height=100,
                        ),
                        top=top,
                        left=left,
                        rotate=0, # 2πで360度
        )

移動とリサイズの処理はupdate_arrowで定義しており、ダブルクリックの状態によって分岐させています。
ダブルクリックされた状態で、ドラッグされた場合はドラッグ時のマウスポインターの移動量に合わせて、Canvasコントロールの横の大きさと描画している矢印の大きさの2つを更新し、矢印の大きさを変更させています。
ダブルクリックがされていない状態でドラッグされた場合はドラッグ時のマウスポインターの移動量に合わせてオブジェクトの位置を更新しています。回転行列を用いることで、矢印の向きが変わっても同じドラッグ操作で移動を行うことができるようにしてます。

arrow.py
def update_arrow(self, e: DragUpdateEvent):

    # 矢印の長さを変更
    if self.state.mode == "pointer" and self.double_tap == True:
        
        height = e.control.height
        width = max(0, e.control.width + e.delta_x)
        e.control.width = width
        self.arrow.shapes.clear()
        self.arrow.shapes.append(
            cv.Path(
                [
                    cv.Path.MoveTo(50, height/2),  # 矢印の元
                    cv.Path.LineTo(width, height/2),  # 矢印の軸
                    cv.Path.LineTo(width-10, height/2-10),  # 矢印の頭の上部分
                    cv.Path.MoveTo(width, height/2),  # 矢印の元に戻る
                    cv.Path.LineTo(width-10, height/2+10),  # 矢印の頭の下部分
                ],
                paint=Paint(
                    stroke_width=3,
                    color=colors.BLACK,
                    style=PaintingStyle.STROKE,
                ),
            )
        )
        self.arrow.update()
    # 矢印の移動
    elif self.state.mode == "pointer" and self.double_tap == False:
        # sinθ, cosθを算出
        cos_theta = math.cos(self.arrow.rotate)
        sin_theta = math.sin(self.arrow.rotate)
        
        # 回転に対応したdelta_x, delta_y
        delta_x_rotated = e.delta_x * cos_theta - e.delta_y * sin_theta
        delta_y_rotated = e.delta_x * sin_theta + e.delta_y * cos_theta
        
        self.arrow.top = min(self.page.height - 100, max(0, self.arrow.top + delta_y_rotated))
        self.arrow.left = min(self.page.width, max(0, self.arrow.left + delta_x_rotated))
        self.arrow.update()

矢印の回転はrotate_arrow_beforeとrotate_arrowで行っており、rotate_arrow_beforeで右クリックがされたときのマウスポインタの座標を回転前の座標として保存し、rotate_arrowで回転前のマウスポインタの座標(B)と回転後のマウスポインタの座標(C)、現在の矢印の位置(A)の3点から回転する角度を算出し、それに合わせて矢印オブジェクトを回転させています。
3点からベクトルを定義し、外積を求めることで回転方向を判定し、内積を求めることでどれだけ回転させるかの角度を算出しています。

arrow.py
# 矢印の回転の準備
def rotate_arrow_before(self, e: ScaleStartEvent):
    if self.state.mode == "pointer":
        self.state.x = e.focal_point_x
        self.state.y = e.focal_point_y

# 矢印の回転
def rotate_arrow(self, e: ScaleUpdateEvent):
    if self.state.mode == "pointer":
        end_x = e.focal_point_x
        end_y = e.focal_point_y
        # ベクトルを算出
        vec_ab = [self.state.x - self.arrow.left, self.state.y - self.arrow.top]
        vec_ac = [end_x - self.arrow.left, end_y - self.arrow.top]
        
        # 外積を計算して回転方向を判定
        cross_product = vec_ab[0] * vec_ac[1] - vec_ab[1] * vec_ac[0]
        
        # cosθを算出
        cos_theta = (vec_ab[0]*vec_ac[0] + vec_ab[1]*vec_ac[1]) / (math.sqrt(vec_ab[0]**2 + vec_ab[1]**2) * math.sqrt(vec_ac[0]**2 + vec_ac[1]**2))
        
        if (cross_product > 0 and ((-math.pi/2.0 <= self.arrow.rotate <= math.pi/2.0) or (3*math.pi/2.0 <= self.arrow.rotate <= 2*math.pi/2.0) or (-2*math.pi <= self.arrow.rotate <= -3*math.pi/2.0)))\
            or (cross_product < 0 and ((math.pi/2.0 <= self.arrow.rotate <= 3*math.pi/2.0) or (-3*math.pi/2.0 <= self.arrow.rotate <= -math.pi/2.0))):
            self.arrow.rotate += math.acos(cos_theta) * 2*math.pi
            self.arrow.rotate = (self.arrow.rotate + 2*math.pi) % (4*math.pi) - 2*math.pi
        else:
            self.arrow.rotate -= math.acos(cos_theta) * 2*math.pi
            self.arrow.rotate = (self.arrow.rotate + 2*math.pi) % (4*math.pi) - 2*math.pi
        self.arrow.update()
        self.state.x = end_x
        self.state.y = end_y

実行ファイル化

折角、ディスクトップアプリを作成したので、それを他の人にも使ってもらいと思うことがあると思います。作成したアプリケーションを配布するために実行ファイルを生成します。
実行ファイル化することでpythonの実行環境がなくてもアプリ動かすことができます。
実行ファイル化するためにpyinstallerを使用します。今回は実行ファイルのアイコンを設定したいので、pillowをインストールしますが、アイコンが不要であれば要らないです。

# 必須
pip install pyinstaller
# アプリアイコンを設定する場合のみ必要
pip install pillow

以下のコマンドで実行ファイル化します。<>の部分は適宜自身のものに置き換えてください。
オプションについては公式ドキュメントを参照してください。
https://flet.dev/docs/cookbook/packaging-desktop-app/

flet pack <your_program.py> --icon <your-image.png>

# 例
flet pack .\src\main.py --icon ./icon.png --name idea-board --product-name idea-board --product-version 1.0.0

dist配下に実行ファイルが生成されていれば成功です!

実行ファイルをダブルクリックして起動してみます。
問題なく起動できていることを確認できました!アイコンも反映されてます。

上記の実行ファイルは以下のサイトからダウンロードできます。Download raw fileをクリックしてファイルをダウンロードします。ダウンロードしたファイルを起動する際にWindows Defenderにより確認のポップ画面が表示されると思いますが許可してください。

https://github.com/takeru-a/flet_idea_board/blob/main/dist/idea-board.exe

Webアプリ

fletで作成したアプリはwebアプリとして動作させることも可能です。
作成したアプリケーションを以下のコマンドでローカル環境にてwebアプリとして起動させます。
ブラウザでhttp://localhost:8000/create/board にアクセスし無事に起動されていることを確認します。(ポート番号を8000で指定した場合)
ポート番号を指定して起動することができますが、コマンド実行時に自動でブラウザでアプリケーションにアクセスしてくれるので、動作確認で動かす目的であればポートを指定する必要はないです。

# webアプリとして起動
flet run --web [ファイルを指定]

# portを指定して実行
flet run --web --port 8000 [ファイルを指定]

# 例(ポートを指定しない場合はランダムに割り当てられる)
flet run --web .\src\main.py

デプロイ

作成したアプリケーションをrenderにデプロイします。renderには無料枠があり、簡単なアプリをテスト的に動かす目的であれば適しています。
https://render.com/docs/free
デプロイの準備として、本アプリケーションに必要なライブラリを以下のコマンドでファイルに出力します。pip installでインストールしたライブラリ一覧を出力します。

pip freeze > requirements.txt

出力できたらアプリケーションのgithubリポジトリにpushします。

Render

アカウントがない場合は以下のページからアカウントを作成します。メールアドレス&パスワードで作成する方法とgoogleアカウントやgithubアカウントからアカウントを作成する方法がありますので、自身にあった方法で作成します。のちにgithubアカウントと連携する必要があるので、私はgithubアカウントからユーザを作成することをお勧めします。

https://dashboard.render.com/register
します。

DashBoardページからCreate your first projectボタンをクリックし、デプロイするプロジェクト名を入力し、作成します。


Create new serviceボタンをクリックしてプロジェクトに追加するサービスを選択します。

今回はWeb Servicesを選択します。

Source Codeにはアプリをpushしたリポジトリを選択します。

Languageはpython3でBranchにはデプロイしたいブランチを選択します。

Build Commandには先ほど生成したrequirements.txtのライブラリをインストールするコマンドを指定します。

pip install -r requirements.txt

Start Commandには以下のfletアプリを起動させるコマンドを指定します。(画像とは一部異なります)

flet run --web --port 8000 ./src/main.py

Instance Typeには無料枠にインスタンスを選択します。

環境変数を設定する場合はEnvironment Variablesの項目に設定しますが、
今回は不要なのでDeploy Web Serviceボタンをクリックしてデプロイします。

ステータスがLiveになっていればデプロイ完了しています。

デプロイ先にアクセスして無事にデプロイできているか確認します。デプロイできていることを確認できました!
https://flet-idea-board.onrender.com

おわりに

このアプリを作成するにあたって、画像追加機能やデータの永続化機能など削った機能があるので気が向けばアプリの機能追加を行いたいと思います。

参考

https://zenn.dev/ryotajin/articles/19-build-python-by-pyenv-venv
https://qiita.com/fiftystorm36/items/b2fd47cf32c7694adc2e
https://github.com/pyenv-win/pyenv-win
https://flet.dev/
https://qiita.com/narikakun/items/1711883d0f5d0b1ff1ba
https://github.com/flet-dev/examples/tree/main/python/apps/trolli

Discussion