Bundling Python code with Flutter Desktop apps as self-contained binary, interop with Python self-hosted gRPC service
MIT License
The kit is composed of 2 Bash scripts and a number of template source code files (in Python and Dart) that are used for code generation. You can copy the starter-kit
folder to your project folder and follow the below instructions to get started.
It is assumed that there are 2 folders that contain Flutter and Python projects. 2 scripts are provided to:
prepare-sources.sh
).bundle-python.sh
).On mac/Linux, make them runnable: chmod -x prepare-sources.sh & chmod -x bundle-python.sh prepare-sources.sh
.
There are 4 steps:
prepare-sources.sh
script.Put Flutter app and Python code side by side. Let's assume we have Flutter in app
folder and service
folder for Python.
This is the service definition to be implemented by Python and used by Flutter client.
Run ./prepare-sources.sh --proto ./service.proto --flutterDir ./app --pythonDir ./server
in terminal.
On Windows:
On Linux:
bash ./prepare-sources.sh --proto ./service.proto --flutterDir ./app --pythonDir ./server
.What it does is:
grpcio
, grpcio-tools
.tinyaes
, pyinstaller
.protobuf
compiler.
pip
.protoc_plugin
for Dart..starterDependenciesInstalled
file next to proto file just to keep tract of the fact that dependencies have been installed and do not repeat the heavy process again.lib/grpc_generated
for Flutter and Python root).grpc
, protobuf
, path
, path_provider
packages to pubspec.yaml
.com.apple.security.app-sandbox
set to false
.client.dart
, client_native.dart
and client_web.dart
files that wrap gRPC client instantiation for different platforms.
client.dart
needs to be updated manually after creation.init_py.dart
, init_py_native.dart
and init_py_web.dart
.py_file_info.dart
defining Python executable name and it's version that is currently stored in the assets.
server.py
file that runs self-hosted gRPC server, it needs to be updated to host the actual service.Script parameters:
--proto
- points to gRPC PROTO definition of the service.--flutterDir
- location of Flutter app, .dart stubs will be created at $flutterDir/lib/grpc_generated
.--pythonDir
- location of Python project.--exeName
(optional) - name of the executable to build Python app into using PyInstaller, defaults to 'server_py_flutter'. Must be unique enough since server lifecycle management logic will be using the name to kill any processes with the name on app close (or start - to garbage collect). When building '_win', '_osx' and '_lnx' postfixes are added automatically based on platform.Upon successful completion you'd get Dart/Flutter and Python bindings have been generated for 'service.proto' definition
message
from grpc import service_pb2_grpc
from grpc import service_pb2
server.py
to host the implemented serviceFuture<void> pyInitResult = Future(() => null);
void main() {
WidgetsFlutterBinding.ensureInitialized();
pyInitResult = initPy();
runApp(const MainApp());
}
Please note that it is suggested not to await 'init' in the main function, but rather to have the app start and let Python run in parallel. Besides, you can handle 'future' errors later. E.g. you can use FutureBuilder somewhere in the widget tree to display loading spinner and error message.
class MainAppState extends State<MainApp> with WidgetsBindingObserver {
@override
Future<AppExitResponse> didRequestAppExit() {
shutdownPyIfAny();
return super.didRequestAppExit();
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
getClientChannel()
method from client.dart
as constructor parameter when creating client:NumberSortingServiceClient(getClientChannel())
ios/Runner/Info.plist
add this: <key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
Run ./bundle-python.sh --flutterDir ./app --pythonDir ./server
in terminal. You can pass --nuitka
flag to use Nuitka compiler instead of PyInstaller. It can provide better performance at a cost of lower stability.
On Windows:
On Linux:
./bundle-python.sh --flutterDir ./app --pythonDir ./server
.protoc
compiler will be installed via apt
.What it does:
server.py
via PyInstaller into a single executable file using the name defined in --exeName
parameter (defaults to 'server_py_flutter')--exeName
based on the platform.$flutterDir/assets/
folder.pubspec.yaml
.py_file_info.dart
with the name and version of the bundled Python executable.When building Flutter app you can override host and port via --dart-define, e.g.:
flutter build macos --dart-define port=8080 --dart-define host=ajax.com
This is needed when building Web and mobile clients to allow the use of a remote server.
true
as an argument to Future<void> initPy([bool doNoStartPy = false])
OR create a separate launch.json
config and use --dart-define to set this flag (see example). E.g. the following config is automatically recognized via initPy(): "toolArgs": [ "--dart-define", "useRemote=true", ]
python3 server.py
) and start Flutter app, they will both use localhost:50055
by default.grpcurl
or evans
command line client, e.g.evans service.proto --port 50055
call SortNumbers
1, 2, 3 -> Ctrl+D
To test gRPC Web locally you can use gRPC-Web-Proxy binary like that:
./grpcwebproxy-v0.15.0-osx-x86_64 --backend_addr=localhost:50055 --backend_tls_noverify --allow_all_origins
Binaries can be downloaded from here: https://github.com/improbable-eng/grpc-web/releases - note that they are not signed and on Mac you will need to check Security settings and allow it to run
When playing with servers (built binary or started via python interpreter) watch out for running the server on one port multiple times. You might get errors. You can use the following command to kill processes used by default by this starter kit: kill -9 $(lsof -ti:50055,8080)
.
Manually start the generated Python binary from Flutter's ./assets
folder to test it for any issues (what if it crashes).
You can target any client to use remote server, though it is specifically useful with mobile and Web as they can't bundle standalone Python server.
To do so you have 2 options:
defaultPort
and defaultHost
variables in the generated cleint.dart
- these define the defaults for server address.flutter build macos --dart-define port=50055 --dart-define host=ajax.com
.Web clients can't work over HTTP2 and require a proxy in front of gRPC server. As of 2023, there is no work in progress on a Python proxy (Sonora doesn't work with Dart client). The 2 options are Envoy suggested by Google and gRPC Web.
numpy
import was not found. Only pip3 install --upgrade numpy
helped solve the issues.
Click Python version at the bottom (), in the popup window, check that you got the right Python (recomended in my case was not chosen).