Flutter是一个以跨平台应用程序开发而闻名的UI框架。尽管Python是一种多功能的编程语言,以其可读性和庞大的图书馆生态系统而闻名。
本指南将涵盖使用Flutter-Python Starter Kit.
集成颤音和python进行应用程序开发的过程要旨
启用在所有flutter支持的六个平台上使用Python代码,包括MacOS,Windows,Linux,Android,iOS和Web。 Python代码和运行时被包装为用于桌面平台的独立可执行文件,而远程托管版本则用于移动和Web平台。该系统依赖于GRPC原始定义,用于Flutter Client和Python服务器之间的一致API,并使用代码生成工具来处理样板任务,从而使开发人员可以专注于业务逻辑。
先决条件
- Flutter SDK
- Python 3.9+
- 巧克力包装经理和git bash(适用于Windows)
- 决定使用Nuitka(而不是Pyinstaller)
- 最近发布的Python(不是OS提供的)的官方发布
- 确保将Python添加到路径系统环境变量中。
- 建议将VSCODE作为IDE
概述
Flutter-Python Starter Kit
是一个开源项目。这是一堆脚本和源文件,它们可以自动执行许多操作,否则这些操作将要求开发人员手动执行它们。所做的事情是将建立和维护良好的技术汇总在一起(见上文),使它们共同努力。
入门套件由3个主要组件组成:
-
prepare-sources.sh
:一个安装依赖项,从.proto
生成grpc存根的脚本,创建飞镖/python脚手架,并将文件复制到flutter and Python项目目录。 -
bundle-python.sh
:一个创建一个独立的python可执行文件并将其捆绑为Flutter项目中的资产的脚本,更新资产版本。 -
templates
:一个带有现成飞镖和python文件的文件夹可以解决许多问题,例如在Python端启动GRPC服务器,提取和启动独立的可执行文件,启动GRPC客户端频道,等等。 p>
现在,让我们深入研究整合整合颤音和python的步骤。
样本项目
我们将构建一个非常简单的应用程序,该应用程序生成一系列随机数,将其发送到Python,通过Numpy对它们进行分类并返回UI。
指南展示了从头开始创建解决方案 相同的原理/步骤可以轻松地应用于现有代码库中。
Complete sources of the example are here.
步骤0:获取入门套件
下载the repo并将starter-kit
文件夹放在项目的根部。
步骤1:准备颤音和python项目
转到项目目录,为flutter零件创建app
,server
为python部分。结构看起来像:
my_project/
|-- app/ (Flutter app)
|-- server/ (Python module)
|-- starter-kit
然后切换到app
目录创建示例Flutter Counter app
(我们将通过终端命令修改后者):
flutter create . --empty
暂时将server/
留为
步骤2:在.proto
文件中定义GRPC服务
在项目的根部,创建一个service.proto
文件以指定编号排序GRPC服务。该文件将定义API,Python服务器和Flutter客户端都将使用该API。
syntax = "proto3";
service NumberSortingService {
rpc SortNumbers (NumberArray) returns (NumberArray) {}
}
message NumberArray {
repeated int32 numbers = 1;
}
步骤3:生成GRPC绑定和助手
从项目文件夹的根部运行prepare-sources.sh
脚本。它将从service.proto
文件中生成必要的飞镖/颤动(客户端)和Python(服务器)GRPC绑定。您可能需要先授予其执行权限:
chmod 755 ./starter-kit/prepare-sources.sh; chmod 755 ./starter-kit/bundle-python.sh
./starter-kit/prepare-sources.sh --proto ./service.proto --flutterDir ./app --pythonDir ./server
第一次运行一两分钟。此命令安装所需的依赖项,例如GRPC工具和Pyinstaller,为DART和PYTHON生成GRPC存根,并创建其他助手文件。
完成后,您应该在flutter App中查看app/lib/grpc_generated
中的新文件,server/grpc_generated
for python模块。
步骤4:在Python中实施GRPC服务
如果我们检查/server
目录,它将不再为空:
my_project/
|-- server/ (Python module)
|-- grpc_generated/
|-- requirements.txt
|-- server.py
- 在上一步中,Protoc编译器在
grpc_generated/
中创建了Python Stubs,添加了requirements.txt
,添加了GRPC依赖项,复制了server.py
模板代码,该模板代码启用了新的GRPC服务器。
让我们添加number_sorting.py
并实现grpc_generated/service_pb2_grpc.py
和grpc_generated/service_pb2.py
中定义的服务:
from concurrent import futures
import numpy as np
from grpc_generated import service_pb2_grpc
from grpc_generated import service_pb2
class NumberSortingService(service_pb2_grpc.NumberSortingService):
def SortNumbers(self, request, context):
arr = np.array(request.numbers)
result = np.sort(arr)
print(f"Sorted {len(result)} numbers")
return service_pb2.NumberArray(numbers=result)
更新server.py
文件以包括NumberSortingService
实现。
...
# TODO, import generated gRPC stubs
from grpc_generated import service_pb2_grpc
# TODO, import yor service implementation
from number_sorting import NumberSortingService
...
# TODO, add your gRPC service to self-hosted server, e.g.
service_pb2_grpc.add_NumberSortingServiceServicer_to_server(NumberSortingService(), server)
...
碰巧的是,模板文件已经在其中具有NumberSortingService
(它是硬编码的,而不是用于.proto)。在一个真实的应用程序中,必须将其更改为已实施服务的名称。
您可以尝试在终端中运行server.py
。如果一切顺利,您将收到一条消息,是在Localhost聆听:
user@users-mbp my_project % python3 server/server.py
gRPC server started and listening on localhost:50055
注意:您可能需要更改server.py
中GRPC服务器的方式,即将localhost
更改为[::]
进行远程部署或设置TLS。
步骤5:更新Flutter应用程序以使用GRPC客户端
prepare-sources.sh
to /app
文件夹的更改
- 添加了
pubspec.yaml
的依赖项(grpc,path,path_provider,protobuf) - 在
lib/grpc_generated/
中制作文件- 数字排序服务的DART客户端实现
- GRPC健康检查服务的客户端实现(用于检查服务器是否启动并在启动时运行)
- 本机和Web客户端频道助手类(无论您是运行Web还是本机应用程序,摘要都可以连接到GRPC)
- python服务器初始助手类(提取资产,检查其版本,启动和杀死过程)
要将我们的Flutter应用程序连接到Python,我们只需要更改main.dart
文件。
UI没有GRPC
让我们从实现无需任何GRPC绑定的数组的UI开始。
将MainApp
转换为已有的小部件(为此有方便的重构选项),将随机列表添加到凝视,一些UI ...或仅复制并粘贴以下文件:)
main.dart ,通过DART进行数字排序
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatefulWidget {
const MainApp({Key? key}) : super(key: key);
@override
MainAppState createState() => MainAppState();
}
class MainAppState extends State<MainApp> {
List<int> randomIntegers =
List.generate(40, (index) => Random().nextInt(100));
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Container(
padding: const EdgeInsets.all(20),
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
randomIntegers.join(', '),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
setState(() {
randomIntegers =
List.generate(40, (index) => Random().nextInt(100));
});
},
style: ElevatedButton.styleFrom(
minimumSize:
const Size(140, 36), // Set minimum width to 120px
),
child: const Text('Regenerate List'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
setState(() => randomIntegers.sort());
},
style: ElevatedButton.styleFrom(
minimumSize:
const Size(140, 36), // Set minimum width to 120px
),
child: const Text('Sort'),
),
],
),
),
),
);
}
}
您应该得到这样的东西:
连接到Python
现在,让我们采取这些生成的文件并将排序操作移交给Python。
a)在main.dart
开始时导入必要的GRPC绑定和辅助文件:
import 'package:flutter/material.dart';
import 'package:app/grpc_generated/client.dart';
import 'package:app/grpc_generated/init_py.dart';
import 'package:app/grpc_generated/init_py_native.dart';
import 'package:app/grpc_generated/service.pbgrpc.dart';
b)通过更改main()
函数初始化python:
void main() {
WidgetsFlutterBinding.ensureInitialized();
pyInitResult = initPy();
runApp(const MainApp());
}
initPy()
是辅助方法,它负责旋转服务器并设置客户端频道。它还提取可以通过build/run命令传递的--dart-define
参数(如果必须从资产中提取服务器)。
请注意,该方法返回了一个尚未等待但保存到全局var的koude42。这是故意完成的,因为Pyhton服务器的启动可能很耗时,我们不希望UI悬挂。在旁边,可能会有错误。后者或我们将使用FutureBuilder
来帮助Python Init Progress的UI更新。
c)添加WidgetsBindingObserver
以响应应用程序关闭事件
关闭Python服务器:
class MainAppState extends State<MainApp> with WidgetsBindingObserver {
@override
Future<AppExitResponse> didRequestAppExit() {
shutdownPyIfAny();
return super.didRequestAppExit();
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
...
请注意,shutdownPyIfAny()
是提供的,但是助手并发出OS命令以关闭服务器进程。它使用MACOS上的流程名称server_py_flutter_osx
来搜索流程名称。运行prepare-sources.sh
时,默认名称可以通过--exeName
参数覆盖。 _osx
,_lin
和_win.exe
后缀在构建过程中自动添加,并用于辨别不同扑波平台上的资产。
d)使用FutureBuilder
显示python初始化的状态:
...
SizedBox(
height: 50,
child:
// Add FutureBuilder that awaits pyInitResult
FutureBuilder<void>(
future: pyInitResult,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Stack(
children: [
SizedBox(height: 4, child: LinearProgressIndicator()),
Positioned.fill(
child: Center(
child: Text(
'Loading Python...',
),
),
),
],
);
} else if (snapshot.hasError) {
// If error is returned by the future, display an error message
return Text('Error: ${snapshot.error}');
} else {
// When future completes, display a message saying that Python has been loaded
// Set the text color of the Text widget to green
return const Text(
'Python has been loaded',
style: TextStyle(
color: Colors.green,
),
);
}
},
),
),
const SizedBox(height: 16)
...
e),最后切换到进行分类的GRPC客户端:
ElevatedButton(
onPressed: () {
//setState(() => randomIntegers.sort());
NumberSortingServiceClient(getClientChannel())
.sortNumbers(NumberArray(numbers: randomIntegers))
.then(
(p0) => setState(() => randomIntegers = p0.numbers));
},
这是完整的 main.dart ,通过python进行数字排序
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:app/grpc_generated/client.dart';
import 'package:app/grpc_generated/init_py.dart';
import 'package:app/grpc_generated/init_py_native.dart';
import 'package:app/grpc_generated/service.pbgrpc.dart';
Future<void> pyInitResult = Future(() => null);
void main() {
WidgetsFlutterBinding.ensureInitialized();
pyInitResult = initPy();
runApp(const MainApp());
}
class MainApp extends StatefulWidget {
const MainApp({Key? key}) : super(key: key);
@override
MainAppState createState() => MainAppState();
}
class MainAppState extends State<MainApp> with WidgetsBindingObserver {
@override
Future<AppExitResponse> didRequestAppExit() {
shutdownPyIfAny();
return super.didRequestAppExit();
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
List<int> randomIntegers =
List.generate(40, (index) => Random().nextInt(100));
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Container(
padding: const EdgeInsets.all(20),
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text.rich(
TextSpan(
children: [
const TextSpan(
text: 'Using ',
),
TextSpan(
text: '$defaultHost:$defaultPort',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
TextSpan(
text:
', ${localPyStartSkipped ? 'skipped launching local server' : 'launched local server'}',
),
],
),
),
const SizedBox(height: 16),
SizedBox(
height: 50,
child:
// Add FutureBuilder that awaits pyInitResult
FutureBuilder<void>(
future: pyInitResult,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Stack(
children: [
SizedBox(height: 4, child: LinearProgressIndicator()),
Positioned.fill(
child: Center(
child: Text(
'Loading Python...',
),
),
),
],
);
} else if (snapshot.hasError) {
// If error is returned by the future, display an error message
return Text('Error: ${snapshot.error}');
} else {
// When future completes, display a message saying that Python has been loaded
// Set the text color of the Text widget to green
return const Text(
'Python has been loaded',
style: TextStyle(
color: Colors.green,
),
);
}
},
),
),
const SizedBox(height: 16),
Text(
randomIntegers.join(', '),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
setState(() {
randomIntegers =
List.generate(40, (index) => Random().nextInt(100));
});
},
style: ElevatedButton.styleFrom(
minimumSize:
const Size(140, 36), // Set minimum width to 120px
),
child: const Text('Regenerate List'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
//setState(() => randomIntegers.sort());
NumberSortingServiceClient(getClientChannel())
.sortNumbers(NumberArray(numbers: randomIntegers))
.then(
(p0) => setState(() => randomIntegers = p0.numbers));
},
style: ElevatedButton.styleFrom(
minimumSize:
const Size(140, 36), // Set minimum width to 120px
),
child: const Text('Sort'),
),
],
),
),
),
);
}
}
步骤6:将Python捆绑
运行bundle-python.sh
脚本以创建一个独立的python可执行文件(使用pyinstaller),并将其捆绑为flutter项目中的资产:
./starter-kit/bundle-python.sh --flutterDir ./app --pythonDir ./server
该脚本将针对/server/server.py
运行Pyinstaller,并将构建文件复制到/app/assets/server_py_flutter_{platform_postfix}
,它还将将assets
段添加到pubspec.yaml
中引用assets/
文件夹。
步骤7:跑步和调试
如果您使用VSCODE,则可以通过F5作为桌面应用程序运行该应用程序,然后获取以下UI(左 - 加载,右 - 加载和排序):
注意:根据您的异常处理设置在调试器中的设置,您可能会在探测Python服务器时吞咽的例外,可能会碰到断点。
根据特定方案,您可能希望让服务器从资产中启动,而要使用您在调试器中启动的服务器。或者,如果您正在运行移动客户端,则没有自托管服务器。为了帮助您可以:
- 将端口号传递给
server.py
聆听,例如python3 server.py 8080
- 使用
--dart-define
和port
,host
,useRemote
参数,构建/运行命令flutter
沿着启动器套件提供的示例,app/.vscode
下的launch.json文件仪具有一些针对不同情况的启动配置。
还要注意,在调试Web客户端时,您需要设置一个将处理客户端的入站连接并将其转发到GRPC的Web代理。套件附带的示例还具有(对此)[https://github.com/improbable-eng/grpc-web/]命令行代理,可以使您免于进入特使/docker路线。
结论
在指南中,您可以看到flutter和python集成的端到端情况,可以将其推送到任何其他代码库中。建议的解决方案几乎没有独特的功能,例如python部分在单独的过程中完全隔离在颤动中(因此不堵塞UI或不崩溃),管理子服务器过程的生命周期,可用性可以集成到构建管道中的预先烹饪的shell脚本文件等等。请参阅(满足要求)中的完整列表[https://github.com/maxim-saplin/flutter_python_starter/blob/main/README.md#requirements-fulfilled]节。
建议的方法并不是唯一的方法,我将在下一篇文章中介绍的其他解决方案。然而,我最终创建了入门套件的关键原因是,建议的方法都没有完整(大多数教程都以许多重要的问题尚未解决)或在平台支持中受到限制。