主题
使用 flet-geolocator 扩展在你的 Flet 应用中访问设备定位服务。 该控件封装了 Flutter 的 geolocator 包,并提供了用于权限检查和位置流的异步辅助方法。
平台支持
| 平台 | Windows | macOS | Linux | iOS | Android | Web |
|---|---|---|---|---|---|---|
| 支持 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
用法
将 flet-geolocator 添加到项目依赖中:
uv
bash
uv add flet-geolocatorpip
bash
pip install flet-geolocator [^1][^1]: 完成上述操作后,你还需要手动将此包添加到你的 requirements.txt 或 pyproject.toml 中。
环境要求
以下各节展示了各平台所需的相关配置。
Android
要访问设备定位,需要进行以下配置:
ACCESS_FINE_LOCATION:允许访问精确位置。如果两项都设置了,将优先使用该权限而非ACCESS_COARSE_LOCATION。ACCESS_COARSE_LOCATION:允许访问粗略位置。ACCESS_BACKGROUND_LOCATION(可选):即使应用在后台也允许访问位置。从 Android 10(API 级别 29)起生效。FOREGROUND_SERVICE_LOCATION(可选):即使应用在前台也允许访问位置。从 Android 14(API 级别 34)起生效。
注意
- 要获取位置更新,
ACCESS_FINE_LOCATION或ACCESS_COARSE_LOCATION权限中至少有一项是 必需的,如果两项都设置,则优先使用前者。 - 指定
ACCESS_COARSE_LOCATION权限会导致位置更新的精度大约相当于一个城市街区。由于ACCESS_COARSE_LOCATION只使用网络服务来计算设备位置, 在你获得第一个定位结果之前可能需要很长时间(数分钟)。更多信息见 此处。
bash
flet build apk \
--android-permissions android.permission.ACCESS_FINE_LOCATION=true \
--android-permissions android.permission.ACCESS_COARSE_LOCATION=true \
--android-permissions android.permission.ACCESS_BACKGROUND_LOCATION=true \
--android-permissions android.permission.FOREGROUND_SERVICE_LOCATION=truetoml
[tool.flet.android.permission]
"android.permission.ACCESS_FINE_LOCATION" = true
"android.permission.ACCESS_COARSE_LOCATION" = true
"android.permission.ACCESS_BACKGROUND_LOCATION" = true
"android.permission.FOREGROUND_SERVICE_LOCATION" = true另请参阅:
iOS
要访问设备定位,需要进行以下配置:
bash
flet build ipa \
--info-plist NSLocationWhenInUseUsageDescription="Some message to describe why you need this permission..."toml
[tool.flet.ios.info]
NSLocationWhenInUseUsageDescription = "Some message to describe why you need this permission..."另请参阅:
macOS
要访问设备定位,需要进行以下配置:
bash
flet build macos \
--info-plist NSLocationUsageDescription="Some message to describe why you need this permission..." \
--macos-entitlements com.apple.security.personal-information.location=truetoml
[tool.flet.macos.info]
NSLocationUsageDescription = "Some message to describe why you need this permission..."
[tool.flet.macos.entitlement]
"com.apple.security.personal-information.location" = true另请参阅:
跨平台
此外(或者作为替代方案),你还可以使用我们预定义的跨平台 location权限包:
bash
flet build <target_platform> --permissions locationtoml
[tool.flet]
permissions = ["location"]Web:缓存位置与实时位置
在 Web 上,Geolocator.get_current_position 默认会接受浏览器缓存中最多 5 分钟前的位置, 否则它会向浏览器请求一次实时定位,如果 30 秒内没有结果就会超时。 这个默认行为的存在,是因为桌面浏览器的按需定位流程较慢,而且在静止设备上经常失败。
如果你需要更严格的时效性(或更长的缓存窗口),请传入一个带有显式 maximum_age 和/或 time_limit 的 GeolocatorWebConfiguration:
python
geo = ftg.Geolocator(
configuration=ftg.GeolocatorWebConfiguration(
accuracy=ftg.GeolocatorPositionAccuracy.HIGH,
maximum_age=ft.Duration(seconds=10),
time_limit=ft.Duration(seconds=15),
),
)疑难解答
macOS:Web 浏览器返回 POSITION_UNAVAILABLE 或永远无法解析
在 macOS 上以 --web 模式运行 Flet 应用时,即使「定位服务」已开启、且浏览器已在 系统设置 → 隐私与安全性 → 定位服务 中被允许,浏览器仍可能无法获取位置。症状包括:
getCurrentPosition挂起,或返回Location request timed out.错误。- 位置流发出
Failed to obtain position: Position update is unavailable(这是浏览器返回的 W3CPOSITION_UNAVAILABLE/ 错误码 2)。
这是 macOS 的 locationd 守护进程陷入卡死状态的已知问题:它会报告浏览器已被授权, 却永远不返回定位结果。重启它即可:
bash
sudo killall locationd该守护进程会自动重新启动。你可以通过访问 browserleaks.com/geo 来独立验证浏览器一侧 —— 如果那个页面也无法获取位置,那么问题出在操作系统,而不是你的 Flet 应用。
原生(非 Web)的 macOS Flet 应用不受影响,因为它通过平台插件直接使用 CoreLocation,而不是走浏览器的 Geolocation API。
示例
python
from typing import Callable
import flet as ft
import flet_geolocator as ftg
async def main(page: ft.Page):
page.scroll = ft.ScrollMode.ADAPTIVE
page.appbar = ft.AppBar(title=ft.Text("Geolocator Tests"))
def handle_position_change(e: ftg.GeolocatorPositionChangeEvent):
page.add(ft.Text(f"New position: {e.position.latitude} {e.position.longitude}"))
def get_dialog(handler: Callable):
return ft.AlertDialog(
adaptive=True,
title="Opening Location Settings...",
content=ft.Text(
"You are about to be redirected to the location/app settings. "
"Please locate this app and grant it location permissions."
),
actions=[ft.TextButton("Take me there", on_click=handler)],
actions_alignment=ft.MainAxisAlignment.CENTER,
)
def show_snackbar(message):
page.show_dialog(ft.SnackBar(ft.Text(message)))
async def handle_permission_request(e: ft.Event[ft.OutlinedButton]):
try:
p = await geo.request_permission()
page.add(ft.Text(f"request_permission: {p}"))
show_snackbar(f"Permission request sent: {p}")
except RuntimeError as ex:
show_snackbar(f"Could not request permission: {ex}")
async def handle_get_permission_status(e: ft.Event[ft.OutlinedButton]):
try:
p = await geo.get_permission_status()
show_snackbar(f"Permission status: {p}")
except RuntimeError as ex:
show_snackbar(f"Could not read permission status: {ex}")
async def handle_get_current_position(e: ft.Event[ft.OutlinedButton]):
try:
p = await geo.get_current_position()
show_snackbar(f"Current position: ({p.latitude}, {p.longitude})")
print(f"Current position: ({p.latitude}, {p.longitude})")
except RuntimeError as ex:
show_snackbar(f"Could not get current position: {ex}")
print(f"Could not get current position: {ex}")
async def handle_get_last_known_position(e):
try:
p = await geo.get_last_known_position()
if p is None:
show_snackbar("No last known position available.")
else:
show_snackbar(f"Last known position: ({p.latitude}, {p.longitude})")
print(f"Last known position: ({p.latitude}, {p.longitude})")
except RuntimeError as ex:
show_snackbar(f"Could not get last known position: {ex}")
async def handle_location_service_enabled(e):
try:
p = await geo.is_location_service_enabled()
show_snackbar(f"Location service enabled: {p}")
except RuntimeError as ex:
show_snackbar(f"Could not check location service: {ex}")
async def handle_open_location_settings(e: ft.Event[ft.OutlinedButton]):
try:
p = await geo.open_location_settings()
page.pop_dialog()
if p is True:
show_snackbar("Location settings opened successfully.")
else:
show_snackbar("Location settings could not be opened.")
except RuntimeError as ex:
page.pop_dialog()
show_snackbar(f"Could not open location settings: {ex}")
async def handle_open_app_settings(e: ft.Event[ft.OutlinedButton]):
try:
p = await geo.open_app_settings()
page.pop_dialog()
if p:
show_snackbar("App settings opened successfully.")
else:
show_snackbar("App settings could not be opened.")
except RuntimeError as ex:
page.pop_dialog()
show_snackbar(f"Could not open app settings: {ex}")
location_settings_dlg = get_dialog(handle_open_location_settings)
app_settings_dlg = get_dialog(handle_open_app_settings)
geo = ftg.Geolocator(
configuration=ftg.GeolocatorConfiguration(
accuracy=ftg.GeolocatorPositionAccuracy.LOW
),
on_position_change=handle_position_change,
on_error=lambda e: page.add(ft.Text(f"Error: {e.data}")),
)
page.add(
ft.SafeArea(
content=ft.Column(
controls=[
ft.Row(
wrap=True,
controls=[
ft.OutlinedButton(
content="Request Permission",
on_click=handle_permission_request,
),
ft.OutlinedButton(
content="Get Permission Status",
on_click=handle_get_permission_status,
),
ft.OutlinedButton(
content="Get Current Position",
on_click=handle_get_current_position,
),
ft.OutlinedButton(
content="Get Last Known Position",
visible=not page.web,
on_click=handle_get_last_known_position,
),
ft.OutlinedButton(
content="Is Location Service Enabled",
on_click=handle_location_service_enabled,
),
ft.OutlinedButton(
content="Open Location Settings",
visible=not page.web, [^1]
on_click=lambda e: page.show_dialog(
location_settings_dlg
),
),
ft.OutlinedButton(
content="Open App Settings",
visible=not page.web, [^1]
on_click=lambda e: page.show_dialog(app_settings_dlg),
),
],
)
],
),
)
)
if __name__ == "__main__":
ft.run(main)描述
Geolocator 继承自:ft.Service, BaseControl
一个允许你从设备获取 GPS 数据的控件。
属性
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
configuration | Optional[GeolocatorConfiguration] | None | 一些额外的配置。 |
position | Optional[GeolocatorPosition] | None | 设备的当前位置。(只读) 初始为 None,当位置变化时会被更新。 |
data | Any | skip_ | 任意类型的自定义数据。 |
key | Optional['KeyValue'] | None | 用于在更新之间保持控件标识稳定的键。 |
只读属性
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
parent | Optional['BaseControl'] | — | 此控件的直接祖先(父级)。 默认为 None,只有在此控件被挂载(加入页面树)后才有值。 Page 控件(页面树的根)是个例外 —— 它的 parent 始终为 None。 |
page | 'Page' | — | 此控件所属的页面。 |
事件
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
on_position_change | Optional[ft.EventHandler[GeolocatorPositionChangeEvent]] | None | 当设备位置发生变化时触发。 |
on_error | Optional[ft.ControlEventHandler['Geolocator']] | None | 发生错误时触发。 事件处理器参数的 data 属性包含错误的相关信息。 |
方法
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
get_current_position | GeolocatorPosition | — | 使用所需的精度和设置获取设备的当前位置。 注意: 根据各种定位服务的可用情况,这可能需要数秒。建议先调用 get_last_known_position 方法获取一个已知/缓存的位置,再用 get_current_position 方法的结果更新它。 参数: configuration:位置请求的额外配置。如果未指定,则使用 configuration 属性。 返回: 设备的当前位置,类型为 GeolocatorPosition。 |
get_last_known_position | Optional[GeolocatorPosition] | — | 获取存储在用户设备上的最后已知位置。精度可通过 configuration 属性定义。 返回: 设备的最后已知位置,类型为 GeolocatorPosition;如果没有可用的最后已知位置则返回 None。 抛出异常: FletUnsupportedPlatformException:如果在 Web 平台上调用。 |
get_permission_status | GeolocatorPermissionStatus | — | 获取应用已被授予访问设备位置的权限。 返回: 权限的状态。 |
request_permission | GeolocatorPermissionStatus | — | 向设备请求访问设备位置的权限。 返回: 权限请求的状态。 |
is_location_service_enabled | bool | — | 检查定位服务是否已启用。 返回: 如果定位服务已启用则返回 True,否则返回 False。 |
open_app_settings | bool | — | 尝试打开应用的设置。 返回: 如果应用设置成功打开则返回 True,否则返回 False。 抛出异常: FletUnsupportedPlatformException:如果在 Web 平台上调用。 |
open_location_settings | bool | — | 尝试打开设备的定位设置。 返回: 如果设备设置成功打开则返回 True,否则返回 False。 抛出异常: FletUnsupportedPlatformException:如果在 Web 平台上调用。 |
distance_between | ft.Number | — | 计算所给坐标之间的距离,单位为米。 坐标之间的距离使用 Haversine 公式计算(参见 https://en.wikipedia.org/wiki/Haversine_formula)。 参数: start_latitude:起点的纬度,单位为度。 start_longitude:起点的经度,单位为度。 end_latitude:终点的纬度,单位为度。 end_longitude:终点的经度,单位为度。 返回: 坐标之间的距离,单位为米。 |
init | — | — | — |
is_isolated | — | — | 返回此控件是否被标记为隔离。 隔离控件会被排除在父级驱动的更新遍历之外,并需要自行管理更新边界。 |
build | — | — | 在控件初始化期间调用一次,用于定义其子控件。在此方法中 page 属性可用。 |
before_update | — | — | 每当此控件被更新时都会调用此方法。 注意: 务必不要在此处调用或请求 update()。 |
before_event | — | — | 在事件处理器执行之前拦截事件。 返回 False 可取消分发。返回 True 或 None 则继续正常的事件处理。 |
did_mount | — | — | 在控件挂载到页面树之后调用。 重写此方法以启动需要已挂载页面的资源,例如订阅、定时器或服务监听器。 |
will_unmount | — | — | 在控件从页面树移除之前调用。 重写此方法以释放 did_mount() 中创建的资源,例如订阅、定时器或外部句柄。 |
get_data_channel | — | — | 解析为此 widget 在 Dart 侧分配的 [DataChannel]。模式: on_data_channel_open: Optional[ft.EventHandler[DataChannelOpenEvent]] = None def init(self): self.on_data_channel_open = self._on_open def _on_open(self, e): self._channel = self.get_data_channel(e.channel_id) 幂等 —— 底层的 Connection 会按 id 缓存 DataChannel,因此重复调用会返回同一实例。没有错误路径:该 id 始终来自框架触发的事件,所以当此方法运行时,通道在两端都已存在。 |
update | None | — | 为此控件请求一次 UI 更新。 在更改控件状态或属性后调用。控件必须已挂载到页面且未被标记为冻结。 |