本教程详细讲解如何在 Flet 应用中使用 page.client_storage 实现数据持久化,以确保用户数据在应用重启后依然存在。我们将深入探讨存储 Flet UI 控件时常见的“循环引用”错误,并提供正确的解决方案:即仅存储可序列化的数据类型(如字符串、数字或由它们组成的列表/字典),而非 Flet 控件对象本身,并通过示例代码演示如何有效管理和加载持久化数据。
1. 理解 Flet 的 page.client_storage
Flet 框架为开发者提供了一个名为 page.client_storage 的机制,用于在客户端(通常是浏览器或桌面应用的本地存储)持久化数据。这类似于 Web 开发中的 localStorage,它允许应用在会话结束后依然保留少量数据,以便在下次启动时恢复。
page.client_storage 是一个简单的键值对存储,其基本操作包括:
- page.client_storage.set(key, value): 存储一个键值对。value 必须是可序列化的数据类型(字符串、数字、布尔值,或由它们组成的列表/字典)。
- page.client_storage.get(key): 根据键获取存储的值。
- page.client_storage.remove(key): 移除指定键的数据。
- page.client_storage.clear(): 清除所有存储的数据。
- page.client_storage.get_keys(prefix): 获取所有以指定前缀开头的键。
2. 为什么不能直接存储 Flet 控件?
在尝试使用 page.client_storage 存储 Flet UI 控件(如 ft.Row、ft.Text、ft.Checkbox 等)时,开发者经常会遇到 ValueError: Circular reference detected 或 ‘dict’ object has no attribute ‘_build_add_commands’ 等错误。
这是因为 Flet 控件是复杂的 Python 对象,它们内部包含对其他控件、父控件、事件处理器等的引用。这些复杂的内部结构导致它们无法直接被 JSON 序列化(page.client_storage 在底层通常依赖于 JSON 序列化来存储数据)。当你尝试存储一个包含循环引用的对象时,序列化器会陷入无限循环,从而抛出 Circular reference detected 错误。即使没有循环引用,Flet 控件对象本身也包含许多非数据属性和方法,这些都不是 client_storage 期望存储的简单数据。
核心原则:page.client_storage 应该存储数据,而不是 UI 控件。 你应该存储用户输入的数据(例如,待办事项的文本内容),然后在应用启动时,根据这些存储的数据动态地重建 UI 控件。
3. 实现一个持久化的 To-Do List 应用
我们将基于一个待办事项应用来演示如何正确使用 page.client_storage。
3.1 应用结构概览
我们的 To-Do List 应用将包含:
- 一个文本输入框用于添加新任务。
- 一个“添加”按钮。
- 一个显示所有任务的区域。
- 一个“清除所有任务”按钮。
3.2 关键实现步骤
-
数据存储策略:
- 不存储 ft.Row 或 ft.Text 对象。
- 只存储任务的文本内容(字符串)。
- 为每个任务生成一个唯一的键,例如 t1, t2, t3 等,以便独立存储和检索。我们可以利用一个计数器来实现这一点。
-
添加任务与存储:
- 当用户输入任务并点击“添加”按钮时,获取文本内容。
- 更新一个计数器(例如,可以绑定在添加按钮的 data 属性上)。
- 使用计数器作为键的一部分,将任务文本存储到 page.client_storage。
-
加载已存储任务:
- 应用启动时,遍历 page.client_storage 中所有以特定前缀(例如“t”)开头的键。
- 对于每个键,获取其对应的值(任务文本)。
- 根据获取到的任务文本,动态创建 ft.Text 控件,并添加到任务列表中。
-
清除所有任务:
- 当用户点击“清除所有任务”按钮时,调用 page.client_storage.clear() 清除所有数据。
- 同时清空 UI 中的任务列表。
3.3 示例代码
import flet as ft def main(page: ft.Page): # 页面基本设置 page.window_center() page.window_width = 400 page.window_height = 600 page.title = "Flet 持久化待办事项" page.bgcolor = "#141414" # 1. 定义清除所有任务的函数 def all_clear(e): # 清除 client_storage 中的所有数据 page.client_storage.clear() # 清除 UI 中的所有任务控件 task_column.controls.clear() # 更新页面 page.update() # 设置浮动操作按钮用于清除所有任务 page.floating_action_button = ft.FloatingActionButton( icon=ft.icons.CLEANING_SERVICES_ROUNDED, on_click=all_clear, bgcolor="#4B90BE", scale=0.95 ) # 2. 定义添加任务的函数 def add_task(e): # 确保输入框不为空 if tf.value: # 增加添加按钮的data属性作为任务的唯一ID计数器 # add_button.data 初始值为 0,每次点击增加 add_button.data += 1 # 获取任务文本 task_text = tf.value # 创建一个 ft.Text 控件来显示任务 # 注意:这里我们创建了控件,但不会直接存储它 task_control = ft.Text(task_text, color=ft.colors.WHITE) # 将任务控件添加到显示任务的列中 task_column.controls.append(task_control) # 将任务文本存储到 client_storage # 使用 f"task_{add_button.data}" 作为唯一的键 page.client_storage.set(f"task_{add_button.data}", task_text) # 清空输入框 tf.value = "" # 更新页面以显示新任务和清空的输入框 page.update() # 打印当前所有存储的键,用于调试 print("当前存储的键:", page.client_storage.get_keys('')) # 如果输入框为空,则不执行任何操作 else: tf.focus() # 重新聚焦输入框 page.update() # 任务输入框 tf = ft.TextField( label='输入新任务', expand=True, # 允许文本框扩展以填充可用空间 color=ft.colors.WHITE, hint_text_style=ft.TextStyle(color=ft.colors.GREY_500) ) # 添加任务按钮 # data=0 用于作为任务ID的计数器 add_button = ft.IconButton( icon=ft.icons.ADD, on_click=add_task, data=0, icon_color="#4B90BE", tooltip="添加任务" ) # 用于显示所有任务的列 task_column = ft.Column( controls=[], spacing=10 # 任务之间的间距 ) # 将输入框和添加按钮添加到页面顶部 page.add( ft.Row( controls=[ tf, add_button ], alignment=ft.MainAxisAlignment.CENTER ) ) # 将任务显示列添加到页面 page.add( ft.Container( content=task_column, expand=True, # 允许任务列表扩展 padding=ft.padding.all(10), alignment=ft.alignment.top_left ) ) # 3. 应用启动时加载已存储的任务 # 检查 client_storage 中是否存在以 'task_' 开头的键 stored_keys = page.client_storage.get_keys('task_') if stored_keys: # 对键进行排序,以确保任务按添加顺序显示 # 提取数字部分进行排序,例如 'task_10' -> 10 sorted_keys = sorted(stored_keys, key=lambda k: int(k.split('_')[1])) for key in sorted_keys: # 获取存储的任务文本 task_text = page.client_storage.get(key) if task_text: # 确保获取到的值不为空 # 根据任务文本创建 ft.Text 控件 task_control = ft.Text(task_text, color=ft.colors.WHITE) # 将控件添加到任务显示列中 task_column.controls.append(task_control) # 更新 add_button.data,确保新的任务ID不会与现有ID冲突 # 找到最大的ID,并将其设置为 add_button.data 的起始值 current_max_id = int(key.split('_')[1]) if current_max_id > add_button.data: add_button.data = current_max_id # 加载完成后,将 add_button.data 增加1,为下一个新任务做准备 add_button.data += 1 # 更新页面以显示加载的任务 page.update() # 运行 Flet 应用 ft.app(target=main)
3.4 注意事项与改进
-
唯一键管理: 在上述示例中,我们使用 add_button.data 作为计数器来生成唯一的任务键(例如 task_1, task_2)。这是一种简单有效的方法。在应用启动时,需要遍历所有已存储的键,找出最大的数字,并更新 add_button.data,以确保新添加的任务不会覆盖现有任务。
-
单个任务的删除: 示例中只实现了“清除所有任务”。如果需要删除单个任务,则在创建每个任务的 UI 控件时,需要将其对应的 client_storage 键关联起来(例如,通过 control.data 属性)。当删除按钮被点击时,可以根据这个键从 client_storage 中移除对应的数据,并从 UI 中移除该控件。
-
更复杂的数据结构: 如果需要存储更复杂的数据(例如,任务的文本、完成状态、截止日期等),可以考虑将每个任务存储为一个字典,然后将所有任务的字典列表进行 JSON 序列化,作为一个单一的值存储到 client_storage 中。
import json # 存储 tasks_data = [{"text": "Buy milk", "completed": False}, {"text": "Learn Flet", "completed": True}] page.client_storage.set("all_tasks", json.dumps(tasks_data)) # 读取 stored_json = page.client_storage.get("all_tasks") if stored_json: loaded_tasks = json.loads(stored_json) # 根据 loaded_tasks 重新构建 UI
这种方式更易于管理和更新,因为它将所有相关数据存储在一个条目下。
4. 总结
page.client_storage 是 Flet 应用实现数据持久化的强大工具,但正确使用它至关重要。核心在于理解它是一个键值对存储,只能存储可序列化的数据,而非复杂的 Flet UI 控件对象。通过将数据与 UI 逻辑分离,并在应用启动时动态重建 UI,我们可以有效地利用 client_storage 来提升用户体验,确保应用数据的持久性。
评论(已关闭)
评论已关闭