Hybrid Deployments: Running the same codebase as a standalone desktop application or as a traditional micro-service deployment
Flutter has captivated the development community with its unique ability to facilitate Multiplatform deployments, offering developers unmatched flexibility and efficiency. But how can we truly harness the full potential of this powerful tool?
This article delves into the exceptional versatility of Flutter, focusing on its application across various domains such as standalone desktop applications, web-based solutions, and mobile apps. Flutter’s capability to create robust applications and deploy them seamlessly across different environments ensures that these areas benefit from a dynamic and flexible approach. Our goal is to design a modern micro-service ecosystem that can be dynamically bundled into a self-contained application or deployed as a traditional micro-service, all while maintaining a unified codebase. This approach aims to revolutionize development and deployment processes, ensuring consistency, efficiency, and scalability.
This modular design raises a few implementation questions:
How will our services Communicate?
How can we bundle the backend in the frontend?
How can we run it in a browser?
Is it possible to get this running on older browsers? (gRPC over web sockets)
Lets get started!
Choosing a protocol for interprocess communication
This section tackles the first question. This is essential to build a stable and maintainable codebase. In this article, we will be using gRPC for inter-micro service communication. gRPC is a modern, open-source remote procedure call (RPC) framework that can run in any environment. It leverages HTTP/2 or unix domain sockets for transport, Protocol Buffers as the interface description language, and provides features such as authentication, load balancing, and more out of the box. gRPC offers several advantages that make it suitable for inter-process communication in a micro-service architecture:
1. Efficient and High-Performance: gRPC uses HTTP/2, which allows for multiplexing multiple requests on a single TCP connection, reducing latency and improving performance. It is considered to be up to 5 times more preferment than JSON over HTTP.
2. Strongly Typed: Using Protocol Buffers for message serialization ensures type safety, making communication between services more robust and less error-prone.
3. Language Agnostic: gRPC supports a wide range of programming languages, making it easier to integrate services written in different languages.
4. Bi-directional Streaming: gRPC supports both client-side and server-side streaming, allowing for real-time data exchange and more interactive applications.
5. Built-in Authentication: gRPC provides built-in support for authentication, including token-based and OAuth mechanisms, ensuring secure communication between services.
Key Components of gRPC
Protocol Buffers (Protobuf)
Are a language-neutral, platform-neutral, extensible mechanism for serializing structured and deserializing data. RPC defines tow components Messages and Services.
syntax = "proto3";
package fibonacci;
service FibonacciService {
// Unary RPC to calculate a Fibonacci number
rpc CalcFib (FibonacciRequest) returns (FibonacciResponse) {}
// Server-streaming RPC to stream ongoing Fibonacci events
rpc StreamOngoingEvents (Empty) returns (stream Event) {}
}
// Message for Fibonacci calculation request
message FibonacciRequest {
int32 number = 1; // The number for which the Fibonacci is to be calculated
}
// Message for Fibonacci calculation response
message FibonacciResponse {
int64 result = 1; // The calculated Fibonacci number
}
// Message representing an event in the Fibonacci series
message Event {
int64 number = 1; // A Fibonacci number in the series
id Demploymessage used for the stream request
}
// Define a void type
message Empty {}
Transport Layer
The primary reason for choosing gRPC is its ability to create a layer of abstraction over underlying transport protocols. In the accompanying graphic, we observe various implementations utilizing different transport protocols. Below, we will explore the different protocols that gRPC employs under the hood:
- HTTP/2: gRPC predominantly uses HTTP/2 as its transport protocol. This protocol provides several advantages such as multiplexing, flow control, header compression, and bidirectional communication, which enhance the performance and efficiency of gRPC.
- Cronet: Cronet is a network stack provided by Google that can be used with gRPC to leverage advanced network features like better handling of network changes and optimization for mobile networks. It provides an efficient, secure, and high-performance networking library.
- In-Process Transport: gRPC supports in-process transport, which allows communication between different components of the same process. This transport avoids the overhead of network communication, making it extremely fast and efficient for scenarios where services run within the same application.
- Custom Transport: gRPC also allow to write a custom transport layer
gRPC Stub
Most of us are familiar with stubs from unit tests. A function stub is simply a function definition without its implementation, containing the input arguments with their types and the function return type. In gRPC, stubs serve a similar purpose but are used to abstract the remote procedure calls. The protoc compiler generates these stubs, which can then be used by both the client and server for communication.
Generating the stubs:
First, we need to install the Protocol Buffer Compiler (protoc). Here are the installation commands for different operating systems:
macOS: brew install protoc
windows: coco install protoc
Linux (Debian based): apt install protoc
Linux (RHEL based): dnf install protoc
For Dart (Flutter Framework)
- Install The package:
flutter pub add protoc
- Activate the Plugin:
pub global activate protoc_plugin
- Generate Dart Code, for gRPC:
protoc --dart_out=grpc:applications/ app/lib/src/generated -Iprotos common/protos/service.proto
For Python:
- Installing the gRPC packages: pip install grpcio grpcio-tools
- Generate Python Code:
protoc — python_out=grpc:applications/server/lib/generated -Iprotos common/protos/service.proto
Implementing the Server
As already mentioned, the server in our case is written in Python. The protoc compiler generates the necessary gRPC cod e, exposing the API definition as an inheritable class. Each RPC call is implemented as a function within this class.
class FibonacciServiceServicer(fibonacci_service_pb2_grpc.FibonacciServiceServicer):
def __init__(self):
self.events = deque() # To store Fibonacci events
def CalcFib(self, request, context):
n = request.number
result = self.calculate_fibonacci(n)
self.events.append(result)
return fibonacci_pb2.FibonacciResponse(result=result)
def StreamOngoingEvents(self, request, context):
last_sent_index = 0
while True:
if last_sent_index < len(self.events):
for i in range(last_sent_index, len(self.events)):
yield fibonacci_pb2.Event(number=self.events[i])
last_sent_index += 1
time.sleep(1)
def calculate_fibonacci(self, n):
if n <= 0:
return 0
elif n == 1:
return 1
else:
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
fibonacci_pb2_grpc.add_FibonacciServiceServicer_to_server(
FibonacciServiceServicer(), server
)
server.add_insecure_port("[::]:50051")
print("Server started on port 50051")
server.start()
try:
while True:
time.sleep(86400) # Keep server running
except KeyboardInterrupt:
server.stop(0)
if __name__ == "__main__":
serve()
Running the Server
At this point we can start the gRPC server and directly invoke gRPC function with a gRPC client. One of those clients is BloomRPC, it allows you to import the .proto
files and lets you invoke the function on the server. You can start the server with:
python3 server.py
Packaging the Server
The next step is to bundle the application to a binary, this can be done with tools such as PyInstaller or Nuitka. For this we use PyInstaller by running this command:python -m pyinstaller --onefile --noconfirm --clean --log-level=WARN --name=”$exeNameFull” --paths=”./grpc_generated” server.py
At this point we are already half done with the question on how to package the backend into the frontend. Let's get into the Frontend.
Implementing the Frontend
Flutter and gRPC
To get started, we need to add the grpc and protobuf dependencies to our pubspec.yaml file. This ensures that we have all the necessary packages for gRPC communication and protocol buffer handling.
dependencies:
flutter:
sdk: flutter
grpc: ^3.0.0
protobuf: ^2.0.0
Once the necessary dependencies are added to your pubspec.yaml file and the Dart files generated by protoc are copied into the lib folder of your Flutter application, you can proceed with setting up the gRPC client. Follow the steps below to complete the integration:
- Import the Generated Files and gRPC Package: At the top of your Dart file, import the generated gRPC and Protocol-Bufers files, along with the gRPC package
import 'package:grpc/grpc.dart';
import 'path/to/generated/your_service.pb.dart';
import 'path/to/generated/your_service.pbgrpc.dart';
2. Create a wrapper for the gRPC Client Channel: Initialize a client channel that will be used to communicate with the gRPC server. This can be done in two ways, for http and unix sockets:
void getChannel(port=50055, address="localhost") {
const address = port.isNull ?
InternetAddress(address, type: InternetAddressType.unix)
: address;
final channel = ClientChannel(
address,
port: port, // Your servers port
options: const ChannelOptions(
credentials: ChannelCredentials.insecure(),
)
}
this function allows us to call the RPC service via unix domain sockets or through a http/2 connection.
3. Create an Instance of the Generated Stub: Use the client channel to create an instance of the generated stub class. This stub will be used to call the remote methods defined in your service.
final stubSock = FibonacciService(getChannel(port: null, address: "/tmp/test.socket")); // for unix sockets
final stubHttp = FibonacciService(getChannel()); // for http connections
4. Invoking the RPC function: Now we can invoke the gRPC function from our Flutter frontend. We need to manually start the python server on a specific port in this example it is 50051. This can be done by running:
final response = await ;
print(‘Response from server: ${responsemessage}’);
Running the Backend alongside the frontend
Running the backend alongside the frontend involves setting up the necessary infrastructure to ensure seamless communication between the Flutter app and the backend service. This section will guide you through the steps needed to manage the backend process, handle platform-specific requirements, and ensure that the backend service is always available when needed.
Setting Up the Backend Process
To run the backend alongside the frontend, we can use a function to spawn the backend process. This function handles starting the backend server and ensures it binds to the appropriate port.
Future<void> spawnBackendProc({String host = "localhost", int? port}) async {
var dir = await getApplicationSupportDirectory();
var filePath = await _prepareExecutable(dir.path);
// Ask OS to provide a free port if port is null and host is localhost
if (port == null && host == "localhost") {
var serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
port = serverSocket.port;
serverSocket.close();
defaultPort = port;
}
if (defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.linux) {
await Process.run("chmod", ["u+x", filePath]);
}
var p = await Process.start(filePath, [port.toString()]);
int? exitCode;
p.exitCode.then((v) {
exitCode = v;
});
await Future.delayed(const Duration(seconds: 1));
if (exitCode != null) {
throw 'Failure while launching server process. It stopped right after starting. Exit code: $exitCode';
}
}
Preparing the Executable
The _prepareExecutable function is crucial for ensuring the backend server is ready to run. This function will vary depending on how your backend is set up. For a Python server, you might need to ensure the server script is executable
Future<String> _prepareExecutable(String directory) async {
var file = File(p.join(directory, _getAssetName()));
var versionFile = File(p.join(directory, versionFileName));
// Load the asset data
ByteData pyExe = await PlatformAssetBundle().load('assets/${_getAssetName()}');
// Write the file and version file every time
await _writeFile(file, pyExe, versionFile);
return file.path;
}
Future<void> _writeFile(File file, ByteData data, File versionFile) async {
await file.writeAsBytes(data.buffer.asUint8List());
await versionFile.writeAsString(currentFileVersionFromAssets);
}
String _getAssetName() {
// Return the name of the asset file
return 'your_asset_name_here';
}
Considerations for running this in Production
Packaging with Flutter Flavors
One of the problems of this application is that when the application is shipped it will always contain the binaries for the microservice. This could be solved with flutter Flavors
Supporting the Web
since gRPC is based on http/2
- Polyfills: Include necessary JavaScript polyfills in web/index.html for features unsupported by older browsers.
- gRPC-Web Proxy: Use Envoy proxy to translate gRPC calls to HTTP/1.1 or WebSockets, ensuring compatibility with older browsers.
Containerization
Hint:
- Dockerize Backend: Create a Dockerfile for your backend service to standardize the runtime environment.
- Orchestration: Use Docker Compose or Kubernetes to manage multi-container deployments for microservices.
gRPC-Web Socket Proxy
- Envoy Configuration: Set up Envoy proxy with grpc-web support to bridge gRPC calls over WebSockets, enabling older browser support.
By implementing these hints, you can effectively package and deploy your Flutter application with a gRPC backend, ensuring compatibility and maintainability across different environments and platforms.
Conclusion
Integrating Flutter with a gRPC-based microservice architecture opens up versatile deployment possibilities. This approach allows for dynamic microservices that can be deployed in various ways, such as standalone desktop applications, web-based solutions with a separate backend, and mobile apps with a distributed backend architecture.
Flutter’s multi-platform capabilities ensure that the same codebase can be used to create powerful applications across different environments. gRPC, with its efficient and secure communication protocol, supports robust interactions between microservices, enhancing performance and reliability.
Deploying microservices in a standalone desktop application benefits from the simplicity of bundling everything into one package. For web-based solutions, separating the frontend and backend allows for scalability and flexibility in handling different loads and user demands. Mobile apps can leverage the same distributed backend, ensuring a consistent user experience across devices.