集成颤音{所有6个平台}和Python:综合指南
#教程 #python #flutter #crossplatform

Flutter是一个以跨平台应用程序开发而闻名的UI框架。尽管Python是一种多功能的编程语言,以其可读性和庞大的图书馆生态系统而闻名。

本指南将涵盖使用Flutter-Python Starter Kit.

集成颤音和python进行应用程序开发的过程

Flutter-Python starter kit deployment options

要旨


颤抖

启用在所有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个主要组件组成:

  1. prepare-sources.sh:一个安装依赖项,从.proto生成grpc存根的脚本,创建飞镖/python脚手架,并将文件复制到flutter and Python项目目录。

  2. bundle-python.sh:一个创建一个独立的python可执行文件并将其捆绑为Flutter项目中的资产的脚本,更新资产版本。

  3. 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零件创建appserver为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.pygrpc_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.shto /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'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

您应该得到这样的东西:

Image description

连接到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的kou​​de42。这是故意完成的,因为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(左 - 加载,右 - 加载和排序):

Flutter and Python integrated

注意:根据您的异常处理设置在调试器中的设置,您可能会在探测Python服务器时吞咽的例外,可能会碰到断点。

根据特定方案,您可能希望让服务器从资产中启动,而要使用您在调试器中启动的服务器。或者,如果您正在运行移动客户端,则没有自托管服务器。为了帮助您可以:

  • 将端口号传递给server.py聆听,例如python3 server.py 8080
  • 使用--dart-defineporthostuseRemote参数,构建/运行命令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]节。

建议的方法并不是唯一的方法,我将在下一篇文章中介绍的其他解决方案。然而,我最终创建了入门套件的关键原因是,建议的方法都没有完整(大多数教程都以许多重要的问题尚未解决)或在平台支持中受到限制。