Flutter is a UI framework known for cross-platform application development. While Python is a versatile programming language famed for its readability and vast library ecosystem.
This guide will cover the process of integrating Flutter and Python for app development using the Flutter-Python Starter Kit.
The Gist
Enable the use of Python code across all six platforms supported by Flutter, including macOS, Windows, Linux, Android, iOS, and Web. The Python code and runtime are packaged as self-contained executables for desktop platforms, while a remotely hosted version is utilized for mobile and web platforms. The system relies on gRPC proto definition for a consistent API between the Flutter client and Python server, with code generation tools handling the boilerplate tasks, allowing developers to focus on the business logic.
Prerequisites
- Flutter SDK
- Python 3.9+
- Chocolately package manager and Git Bash (for Windows)
- When deciding to use Nuitka (instead of PyInstaller)
- Recent release official release of Python (not the one provided by OS)
- Ensure Python is added to the PATH system environment variable.
- VSCode is recommended as IDE
Overview
The Flutter-Python Starter Kit
is an open-source project. It is a bundle of scripts and source files that automate a number of actions that would otherwise require developers to do them manually. What is does is putting together established and well-maintained technologies (see above) making them work together.
The starter kit consists of 3 main components:
prepare-sources.sh
: A script that installs dependencies, generates gRPC stubs from a.proto
, creates Dart/Python scaffolding and copies files to the Flutter and Python project directories.bundle-python.sh
: A script that creates a self-contained Python executable and bundles it as an asset in the Flutter project, updates asset version.templates
: A folder with ready-made Dart and Python files that solve many problems, such as starting a gRPC server on the Python side, extracting and launching standalone executable, firing up gRPC client channels, etc.
Now, let’s dive into the step-by-step process of integrating Flutter and Python.
Sample Project
We’re going to build a very simple app that generates an array of random numbers, sends it to Python, sorts them via NumPy and returns back to the UI.
The guide showcases creating the solution from the scratch yet the same principles/steps can be easily applied to existing code base.
Complete sources of the example are here.
Step 0: Get the starter kit
Download the repo and put the starter-kit
folder to the root of your project.
Step 1: Preparing Flutter and Python projects
Go to the project directory and create app
for the Flutter part and server
for the Python part. The structure will look like that:
my_project/
|-- app/ (Flutter app)
|-- server/ (Python module)
|-- starter-kit
Then switch to app
directory create sample Flutter Counter app
(which we’ll modify latter) via the terminal command:
flutter create . --empty
Leave server/
empty for the time being
Step 2: Define gRPC service in .proto
file
At the root of the project, create a service.proto
file to specify the number sorting gRPC service. This file will define the API, which both the Python server and the Flutter client will use.
syntax = "proto3";
service NumberSortingService {
rpc SortNumbers (NumberArray) returns (NumberArray) {}
}
message NumberArray {
repeated int32 numbers = 1;
}
Step 3: Generating gRPC bindings and helpers
From the root of the project folder, run the prepare-sources.sh
script. It will generate the necessary Dart/Flutter (client) and Python (server) gRPC bindings from the service.proto
file. You might need to give it execute permission first:
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
Give it a minute or two for the first run. This command installs the required dependencies, such as gRPC tools and PyInstaller, generates gRPC stubs for both Dart and Python, and creates additional helper files.
Upon completion, you should see new files in app/lib/grpc_generated
for the Flutter app and server/grpc_generated
for the Python module.
Step 4: Implementing the gRPC service in Python
If we check /server
directory, it is no longer empty:
my_project/
|-- server/ (Python module)
|-- grpc_generated/
|-- requirements.txt
|-- server.py
- during previous step, protoc compiler created Python stubs in
grpc_generated/
, addedrequirements.txt
with gRPC dependencies, copiedserver.py
template code that spins up a new gRPC server.
Let’s add number_sorting.py
and implement the service defined in grpc_generated/service_pb2_grpc.py
and 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)
Update the server.py
file to include the NumberSortingService
implementation.
...
# 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)
...
It happens that the template file already has NumberSortingService
in it (it is hard-coded and not taken for .proto). In a real app this must be changed to name of the implemented service.
You can try running the server.py
in the terminal. If all went fine you’ll get a message that is it listening at localhost:
user@users-mbp my_project % python3 server/server.py
gRPC server started and listening on localhost:50055
Note: you might want to change the way gRPC server is started in server.py
, i.e. change localhost
to [::]
for remote deployment OR set up TLS.
Step 5: Updating the Flutter app to use the gRPC client
Changes made by prepare-sources.sh
to /app
folder
- Added dependencies to
pubspec.yaml
(grpc, path, path_provider, protobuf) - Made files in
lib/grpc_generated/
- Dart client implementation for number sorting service
- Client implementation for gRPC health check service (used to check if the server is up and running upon launch)
- Native and Web client channel helper classes (abstracts away connecting to gRPC no matter if you’re running web or native app)
- Python server init helper classes (extracting assets, checking its version, launching and killing processes)
To connect our Flutter app to Python, we will only have to make changes main.dart
file.
UI without gRPC
Let’s start by implementing the UI for sorting an array without any gRPC bindings.
Convert MainApp
to stateful widget (there’s a handy Refactor option for that), add the random list to stare, some UI... Or just copy and paste the below file:)
main.dart, number sorting via 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'),
),
],
),
),
),
);
}
}
You should get something like this:
Connecting to Python
Now let’s put to action those generated files and handover the sorting operation to Python.
a) Import the necessary gRPC bindings and helper files at the beginning of main.dart
:
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) Initialize Python by changing the main()
function:
void main() {
WidgetsFlutterBinding.ensureInitialized();
pyInitResult = initPy();
runApp(const MainApp());
}
initPy()
is the helper method that takes care of spinning the server and setting up client channels. It also extracts --dart-define
params that can be passed with build/run commands (the define host, port to connect and flag if server must be extracted from the assets).
Note that the method returns a Future
which is not awaited but rather saved to a global var. This is done on purpose since Pyhton server launch can be time consuming and we do not want the UI to hang. Beside, there can be errors. Latter or we’ll use FutureBuilder
to help with UI updates for Python init progress.
c) Add WidgetsBindingObserver
to respond to app close event
And shut down the Python server:
class MainAppState extends State<MainApp> with WidgetsBindingObserver {
@override
Future<AppExitResponse> didRequestAppExit() {
shutdownPyIfAny();
return super.didRequestAppExit();
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
...
Note the shutdownPyIfAny()
is the one provided but the helper and issues an OS command to close the server process. It uses process name server_py_flutter_osx
on macOS to search for process by name. The default name can be overriden via --exeName
parameter when running prepare-sources.sh
. The _osx
, _lin
and _win.exe
postfixes are added automatically during build process and used to discern assets on different Flutter platforms.
d) Use FutureBuilder
to display the status of Python initialisation:
...
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) And finally switch to gRPC client doing the sorting:
ElevatedButton(
onPressed: () {
//setState(() => randomIntegers.sort());
NumberSortingServiceClient(getClientChannel())
.sortNumbers(NumberArray(numbers: randomIntegers))
.then(
(p0) => setState(() => randomIntegers = p0.numbers));
},
Here’s the complete main.dart with number sorting via 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'),
),
],
),
),
),
);
}
}
Note: for iOS, to let the app connect to remote gRPC server, in ios/Runner/Info.plist
add this:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
Step 6: Bundling the Python executable
Run the bundle-python.sh
script to create a self-contained Python executable (using PyInstaller) and bundle it as an asset in the Flutter project:
./starter-kit/bundle-python.sh --flutterDir ./app --pythonDir ./server
The script will run PyInstaller against /server/server.py
and copy the built file to /app/assets/server_py_flutter_{platform_postfix}
, it will also add assets
section to pubspec.yaml
referencing assets/
folder.
Step 7: Running and Debugging
If you’re using VSCode, you can run the app via F5 as a desktop app and get the following UI (left - loading, right - loaded and sorted):
Note: depending on your exception handling settings in Debugger you might hit breakpoints due to exceptions that are swallowed by helper classes while probing Python server.
Depending on specific scenario you might want to have the server not being started from the asset but use the one you’ve started in the debugger. Or if you are running mobile client, you don’t have self hosted server. To help with different kinds of of setups you can:
- Pass in port number to
server.py
to listen to, e.g.python3 server.py 8080
- Use
--dart-define
andport
,host
,useRemote
arguments with build/run commands for Flutter
With the example provided along the starter kit, there’s a launch.json filer under app/.vscode
that has a few launch configurations for different cases.
Also note that when debugging a web client you need to set-up a web proxy that will handle inbound connections from the client and forwards them to gRPC. The example coming with the kits also has this covered relying (on this)[ https://github.com/improbable-eng/grpc-web/] command line proxy that can save you from going the Envoy/docker route.
Conclusion
In the guide you’ve seen end-to-end case with Flutter and Python integration, which can be extrapolated to any other code base. There’re few distinctive features to the suggested solution, such as Python part being completely isolated from Flutter in a separate process (and hence non-blocking the UI or not crashing it), managing the lifecycle of the child server process, availability of pre-cooked shell script files that can be integrated into build pipelines and many more. See full list in Requirements fulfilled section.
The suggested approach is not the only one and there’re alternative solutions which I will cover in the next post. Yet the key reason I’ve ended up creating the starter kit is that none of the suggested ways were complete (most tutorials are open ended with many important questions left unanswered) or were limited in platforms support.