Cross-Platform Development with Flutter Rust Bridge

OpenReplay Tech Blog - Sep 8 - - Dev Community

by Franca Balogun

Cross-platform progress lets creators establish empowering sets that function on various operating systems utilizing one single code base. This helps save time and resources since it eliminates the need for independent codes for different platforms. This article will explain using the Flutter Rust Bridge package to simplify cross-platform development.

The most prominent framework used for cross-platform development is Flutter because it uses the Dart programming language to develop compiled mobile applications and web and desktop applications from just one source code. Besides Flutter, Rust is acknowledged for its performance and safety, especially in systems programming and applications needing concurrent execution. You can improve Flutter app functionality by using Rust. This has been made possible by the Flutter Rust Bridge (FRB), which eases interaction between Flutter (Dart) and Rust.

This undertaking aims to create a basic battery testing application that showcases the effectiveness of cross-platform programming using Flutter and Rust together. This software will collect and present the power available to a device’s battery and its current charging status. We intend to develop a quick and efficient app by taking advantage of the flexible UI that Flutter offers and Rust's reliable performance. This journey will take us through each procedure, from preparing the development environment to creating and integrating the Rust backend with the Flutter front end.

Prerequisites

Before we start creating our battery test app, it’s worth preparing the development environment and guaranteeing we have all the essential devices and technology. If you want to follow this project closely, you should be familiar with Flutter and Rust. On one side, Flutter is a UI toolkit used to build applications for mobile phones, web, and desktop platforms, all from one codebase. On the other hand, Rust is a system programming language that allows writing concurrent programs without sacrificing safety or performance.

You want to work on this project using the Flutter SDK, Rust programming language, and Flutter Rust Bridge (FRB). To develop the cross-platform front end, you will need the Flutter SDK. About backend logic, Rust can do it better. The FRB allows one to communicate between Flutter and Rust using all the advantages of functionality and safety within a Flutter application through the Rust language.

Setting up a Flutter-and-Rust Environment

The initial move is to install the Flutter SDK, which can be obtained from the official website. After downloading, follow the installation instructions given for your operating system. Once the SDK has been installed, set up your development environment using Visual Studio Code or Android Studio as Integrated Development Environments (IDEs). They provide excellent support for creating applications using Flutter. To check whether it has been correctly installed on your system, type this command in your terminal:

flutter doctor
Enter fullscreen mode Exit fullscreen mode

This command evaluates the condition of your environment and generates a report on the current state of your installation.

The following step involves installing Rust from the official Rust website. This website provides an uncomplicated installation script suitable for several operating systems. After completing the installation, set up your Rust environment by obeying the instructions. To confirm that you have installed it properly, run it on your terminal:

rustc --version
Enter fullscreen mode Exit fullscreen mode

This command can ascertain that Rust is properly installed and can be accessed through your command-line interface.

Since you have set up Flutter alongside Rust evaluation settings, it is time to create a Flutter project. Open your command window and execute the command:

flutter create battery_test_app
Enter fullscreen mode Exit fullscreen mode

The command generates a new Flutter project comprising all necessary directories and files for its operation system. When the project is generated, go to your project folder by running on your command prompt screen:

cd battery_test_app
Enter fullscreen mode Exit fullscreen mode

These are just some of the basic steps that should be taken to create an operational environment for developing your application that will test batteries. This preparation is important since it provides you with every tool and setting, which ensures that integrating Rust into Flutter and using the Flutter Rust Bridge work seamlessly.

Building the Rust Backend

Since we are making a battery testing application, we must create its backend using Rust. In this section, we will show you how to configure the Rust project, add the battery status functionality, and prepare the Rust library for connecting with the Flutter front end.

Creating the Rust Library

First, create a new Rust library. Using the terminal, move to a convenient folder before typing the command.

cargo new --lib battery_test_lib
Enter fullscreen mode Exit fullscreen mode

A new Rust library project containing all the required files and folder structure will be generated by this command. Move into the created folder using the following:

cd battery_test_lib
Enter fullscreen mode Exit fullscreen mode

Next, the Cargo.toml file is where you need to put some dependencies that would help retrieve battery information. For instance, you can use the battery crate to enable a cross-platform API to query the battery state. Just include this line in your [dependencies] section located in your Cargo.toml file to add it:

battery = "0.7"
Enter fullscreen mode Exit fullscreen mode

This command tells you that you are using a particular version, called version 0.7, of the battery crate.

Implementing the Battery Status Functionality

Go ahead and open the src/lib.rs file. This is where you’ll describe how the library will work. You must create functions that obtain battery level and charging status with the aid of a battery crate. A sample implementation follows:

use battery::Manager;
use battery::Result;

pub fn get_battery_status() -> Result<(i32, String)> {
    let manager = Manager::new()?;
    let batteries = manager.batteries()?;
    let battery = batteries.peekable().next().unwrap()?;

    let level = (battery.state_of_charge().value * 100.0) as i32;
    let status = format!("{:?}", battery.state());

    Ok((level, status))
}
Enter fullscreen mode Exit fullscreen mode

Here is the code for battery status in the Rust programming language. A method named get_battery_status creates an instance of Battery, then gets the first available one and reads its current SoC (state of charge) level and status. While the battery level returns a percentage, the status returns a string. The return value of this function is a tuple containing the integer battery level and the string charge state.

Compiling and exporting

For the Flutter front end to use functions from the Rust language, it is important first to create a Rust library and then generate bindings with the help of the Flutter Rust Bridge (FRB).
To begin with, run the command that will compile the library in release mode, creating optimized binary files:

cargo build --release 
Enter fullscreen mode Exit fullscreen mode

Locate the output file in the target/release folder.

The next step is adding the Flutter Rust Bridge dependency to your Rust project. For this, add FRB to your Cargo.toml file:

[dependencies]
flutter_rust_bridge = "0.5"
Enter fullscreen mode Exit fullscreen mode

Modify your src/lib.rs file using the required annotations and imports to export Rust functions by utilizing FRB:

use flutter_rust_bridge::RustToDart;
use battery::Result;

#[derive(RustToDart)]
pub struct BatteryStatus {
    level: i32,
    status: String,
}

pub fn get_battery_status() -> Result<BatteryStatus> {
    let manager = Manager::new()?;
    let batteries = manager.batteries()?;
    let battery = batteries.peekable().next().unwrap()?;

    let level = (battery.state_of_charge().value * 100.0) as i32;
    let status = format!("{:?}", battery.state());

    Ok(BatteryStatus { level, status })
}
Enter fullscreen mode Exit fullscreen mode

The rust code offered works with the flutter_rust_bridge crate to allow exposure of rust functions in a Flutter app’s interface. To begin with, we need to import all relevant crates and create a struct BatteryStatus that will store an integer battery level and a string for the charging state. This structure is annotated with RustToDart; hence, it can be used by any code generation utility for flutter_rust_bridge.

A result containing a BatteryStatus struct is returned next by the get_battery_status function. Within this function, a battery manager is first initialized, and then gets the available battery cache. The battery level percentage is included in the level field, while the status field incorporates a string-formatted charging status. In the end, this function returns a BatteryStatus struct with adjusted values; hence, get_battery_status can be called from the Flutter application, giving structured information about your device’s energy source.

To create bindings, run the FRB tool and type in your terminal:

flutter_rust_bridge_codegen
Enter fullscreen mode Exit fullscreen mode

This command will create the appropriate code to connect Flutter and Rust, enabling smooth function calls and data transfers between both environments.

Having implemented the Rust backend in readiness for incorporation, you connect it to the Flutter front end. This arrangement allows you to take advantage of Rust’s performance and safety features within a cross-platform Flutter application, thus providing a sturdy base for the battery test app.

Integrating Rust with Flutter

Connecting Rust and Flutter consists of arranging the Flutter Rust Bridge (FRB) so that they can interact easily. This part will walk you through linking the compiled Rust library with the Flutter project, generating required bindings using FRB, and demonstrating how to call functions written in Rust from the web application UI.

Setting up the bridge

Start by linking the constructed Rust library to your Flutter project. Rust libraries have been generated during previous sessions and placed into the target/release folder. Hence, they need to be reachable from your Flutter project.

The subsequent step involves establishing a new directory for compiled Rust libraries inside the Flutter project directory. You may, for example, create a folder called rust_libs.

mkdir rust_libs
Enter fullscreen mode Exit fullscreen mode

Take the compiled Rust libraries in the target/release folder and place them in the rust_libs folder. The specific files that need to be copied will depend on what platform you’re aiming for (libbattery_test_lib.so - for Linux, libbattery_test_lib.dylib – for macOS, battery_test_lib.dll - for Windows).

Your Flutter project is now prepared to use these library files by making the necessary adjustments. Make changes to the android/app/build.gradle file by adding this in the Android section:

sourceSets {
 main {
 jniLibs.srcDirs = ['../rust_libs']
 }
}
Enter fullscreen mode Exit fullscreen mode

The Android build has to contain the Rust library files, and its configuration is given to Gradle.

On iOS, you can open ios/Runner.xcodeproj in Xcode and add the Rust library files to the project. Therefore, right-click on the Runner folder in the project navigator, choose “Add Files to ‘Runner’…” and then select the Rust library files in the rust_libs directory.

Generating Bindings Using FRB

You should use the Flutter Rust Bridge (FRB) code generation tool to generate the required bindings for Flutter and Rust communication. First, make sure you have the FRB tool. You can install it using Cargo.

cargo install flutter_rust_bridge_codegen
Enter fullscreen mode Exit fullscreen mode

After that, you can use the FRB tool to produce the bindings. In the directory of your Flutter project, make a new script called build.sh with this content:

#!/bin/bash

flutter_rust_bridge_codegen \
 -r rust_libs/battery_test_lib.h \
    -d lib/rust_bridge_generated.dart
Enter fullscreen mode Exit fullscreen mode

Using this script, the tool generates Dart bindings for the Rust library and puts them under the lib/rust_bridge_generated.dart file. Of course, for the script to be run, it must be made executable by executing:

chmod +x build.sh
Enter fullscreen mode Exit fullscreen mode

Execute the command to produce the bindings.

./build.sh
Enter fullscreen mode Exit fullscreen mode

You can directly execute Rust functions through your Flutter code, as the generated Dart bindings will permit this.

Calling Rust Functions from Flutter

After the bindings, you can use Rust functionalities in your Flutter codes. To import the bindings that you generated, start by opening the lib/main.dart file within your Flutter project, and then proceed with importing them as demonstrated below:

import 'rust_bridge_generated.dart';
Enter fullscreen mode Exit fullscreen mode

Then, use a method to invoke the get_battery_status function in Rust and show the outcome within your Flutter application. Here is an example of how this can be achieved:

import 'package:flutter/material.dart';
import 'rust_bridge_generated.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
 home: BatteryStatusScreen(),
 );
 }
}

class BatteryStatusScreen extends StatefulWidget {
  @override
  _BatteryStatusScreenState createState() => _BatteryStatusScreenState();
}

class _BatteryStatusScreenState extends State<BatteryStatusScreen> {
  String batteryStatus = 'Fetching battery status...';

  @override
  void initState() {
    super.initState();
    fetchBatteryStatus();
 }

  Future<void> fetchBatteryStatus() async {
    try {
      final batteryStatusResult = await api.getBatteryStatus();
      setState(() {
 batteryStatus = 'Battery Level: ${batteryStatusResult.level}%\n'
                        'Charging Status: ${batteryStatusResult.status}';
 });
 } catch (e) {
      setState(() {
 batteryStatus = 'Failed to fetch battery status: $e';
 });
 }
 }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
 appBar: AppBar(
 title: Text('Battery Status'),
 ),
 body: Center(
 child: Text(batteryStatus),
 ),
 );
 }
}
Enter fullscreen mode Exit fullscreen mode

The Flutter code here does just that; it creates a simple app showing battery levels from the Rust backend. It imports the needed Flutter material package and Dart files made for Rust functions. The main method initiates the app by executing MyApp, which is an instance of the widget. It consists of StatelessWidget hence, MaterialApp has BatteryStatusScreen as its first screen. StatefulWidget has brought forth BatteryStatusScreen, which creates its state via the _BatteryStatusScreenState class. BatteryStatus is an empty string stored in this class’s instance variable named batteryStatus.

The function to get the battery status from the Rust backend is called in the initState method, which is the function fetchBatteryStatus. We use this asynchronous fetchBatteryStatus and api.getBatteryStatus to get battery status. If the call returns a successful response, then the value obtained by it would be used to update the batteryStatus variable with the level and charging states corresponding to that value; otherwise, if that call fails, then this variable will be updated with an error message. The build method constructs UI such that there exists an AppBar labeled “Battery Status,” alongside a Center widget containing a Text widget that shows the current values of the variable in the batteryStatusstate. Such a design will ensure that the user interface reflects whatever has transpired regarding batteries acquired through the Rust backend.

Building the Flutter front-end

Now that our Flutter app has a smoothly functioning Rust backend, it is time to consider a front end. This section will focus on creating a user interface (UI) that displays battery details and communicates with Rust’s backend to obtain live information.

Designing the UI

A good user experience largely hinges on creating a simple and intuitive interface. Flutter’s rich assortment of widgets and tools simplifies designing a responsive UI. Our battery test app should have an interface that displays the battery level and charging status. In your Flutter project, open up lib/main.dart. As outlined in our previous section, you should already have the necessary imports for the BatteryStatusScreen. Let’s now consider making modifications to the UI design to improve it. As such, let us add more styled elements to the BatteryStatusScreen widget:

import 'package:flutter/material.dart';
import 'rust_bridge_generated.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
 title: 'Battery Test App',
 theme: ThemeData(
 primarySwatch: Colors.blue,
 ),
 home: BatteryStatusScreen(),
 );
 }
}

class BatteryStatusScreen extends StatefulWidget {
  @override
  _BatteryStatusScreenState createState() => _BatteryStatusScreenState();
}

class _BatteryStatusScreenState extends State<BatteryStatusScreen> {
  String batteryStatus = 'Fetching battery status...';

  @override
  void initState() {
    super.initState();
    fetchBatteryStatus();
 }

  Future<void> fetchBatteryStatus() async {
    try {
      final batteryStatusResult = await api.getBatteryStatus();
      setState(() {
 batteryStatus = 'Battery Level: ${batteryStatusResult.level}%\n'
                        'Charging Status: ${batteryStatusResult.status}';
 });
 } catch (e) {
      setState(() {
 batteryStatus = 'Failed to fetch battery status: $e';
 });
 }
 }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
 appBar: AppBar(
 title: Text('Battery Status'),
 ),
 body: Center(
 child: Padding(
 padding: const EdgeInsets.all(16.0),
 child: Column(
 mainAxisAlignment: MainAxisAlignment.center,
 children: [
              Icon(
                Icons.battery_full,
 size: 100.0,
 color: Colors.green,
 ),
              SizedBox(height: 20.0),
              Text(
 batteryStatus,
 textAlign: TextAlign.center,
 style: TextStyle(fontSize: 20.0),
 ),
 ],
 ),
 ),
 ),
 );
 }
}
Enter fullscreen mode Exit fullscreen mode

This app displays battery status retrieved from Rust's backend in Flutter code. It starts with importing essential Flutter materialpackages and generating dart bindings for Rust functions. The application begins with the main function, whose instance of the myapp widget is run to initiate it. The main structure of this app has been laid out by MyApp, which extends StatelessWidget. It calls itself a battery test app and uses a dark theme throughout its design. BatteryStatusScreen is set as the home screen of this device.

The BatteryStatusScreen widget expands the StatefulWidget class so that its interface can vary according to the current battery condition on the device. The implementation of its state is found in the _BatteryStatusScreenState class, which holds a single variable batteryStatus defined as a constant placeholder string. At the start of this process, in our initState function, we will invoke the fetchBatteryStatus function because we want to be able to read from it and thus know if we are running low or not without having to inquire every time; otherwise, we’ll constantly have dead phones! In addition, it also helps to know when we have only a 15% charge remaining, as it could come in handy knowing whether or not it’s worth it to take our PowerBank with us. When this method is successful at obtaining data, the batteryStatus variable changes to the current level of charge and indicates whether or not the device is charging. When there are issues relaying these types of information back across, however, state variables become error messages instead.

Interacting with the Rust Backend

You are expected to communicate with Rust's backend using the Flutter front-end for dynamically changing user interfaces and real-time information regarding battery life. This is achieved through the fetchBatteryStatus method located within the BatteryStatusScreen widget, where it calls upon getBatteryStatus written in Rust, alters the state variable of batteryStatus, and then executes setState, which results in updating the UI. It would simplify things for users. The addition of the FloatingActionButton component to the BatteryStatusScreen widget provides the option of easily refreshing their battery status through a button.

import 'package:flutter/material.dart';
import 'rust_bridge_generated.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
 title: 'Battery Test App',
 theme: ThemeData(
 primarySwatch: Colors.blue,
 ),
 home: BatteryStatusScreen(),
 );
 }
}

class BatteryStatusScreen extends StatefulWidget {
  @override
  _BatteryStatusScreenState createState() => _BatteryStatusScreenState();
}

class _BatteryStatusScreenState extends State<BatteryStatusScreen> {
  String batteryStatus = 'Fetching battery status...';

  @override
  void initState() {
    super.initState();
    fetchBatteryStatus();
 }

  Future<void> fetchBatteryStatus() async {
    try {
      final batteryStatusResult = await api.getBatteryStatus();
      setState(() {
 batteryStatus = 'Battery Level: ${batteryStatusResult.level}%\n'
                        'Charging Status: ${batteryStatusResult.status}';
 });
 } catch (e) {
      setState(() {
 batteryStatus = 'Failed to fetch battery status: $e';
 });
 }
 }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
 appBar: AppBar(
 title: Text('Battery Status'),
 ),
 body: Center(
 child: Padding(
 padding: const EdgeInsets.all(16.0),
 child: Column(
 mainAxisAlignment: MainAxisAlignment.center,
 children: [
              Icon(
                Icons.battery_full,
 size: 100.0,
 color: Colors.green,
 ),
              SizedBox(height: 20.0),
              Text(
 batteryStatus,
 textAlign: TextAlign.center,
 style: TextStyle(fontSize: 20.0),
 ),
 ],
 ),
 ),
 ),
 floatingActionButton: FloatingActionButton(
 onPressed: fetchBatteryStatus,
 tooltip: 'Refresh',
 child: Icon(Icons.refresh),
 ),
 );
 }
}
Enter fullscreen mode Exit fullscreen mode

This Flutter code establishes a basic app that presents battery status details obtained from a Rust backend. The MyApp widget is run as an instance in the main function to start the app by providing a title, theme, and a BatteryStatusScreenas its home screen. The BatteryStatusScreen widget is stateful and administered by _BatteryStatusScreenState. At first, batteryStatus is set to “Fetching battery status…”. In the initState method, the fetchBatteryStatus function retrieves the battery status from the Rust using api.getBatteryStatus. If the call goes through, it will update the batteryStatuswith the battery level and whether it is charging, but in case of failure, an error message will be shown instead.

UI is constructed in a build method that comprises an AppBar with the title “Battery Status,” a centered icon widget that depicts the battery, a SizedBox for spacing out different components, and a Text widget displaying the status of the battery. If you want to refresh it manually, the FloatingActionButton calls the fetchBatteryStatus method to get the latest information about its current status. How we can keep the UI in sync with live messages about how our power cell is charged depends on the Rust server fetching service.

Running the app

Once Flutter’s interface has been developed and the Rust back-end has been added to it, these are the steps to take to start running the app:

  • Build the Rust Code: Before you start the Flutter project, be sure to compile your Rust code and share an output library. You can run the following commands to achieve this:
cargo build --release
Enter fullscreen mode Exit fullscreen mode
  • Link the Rust Library: Ensure your Flutter project is linked to the compiled library. Based on the platform you are using (Android or iOS), certain steps are required to integrate the Rust library.

For Android: You should put the assembled Rust library (e.g., libyourlibrary.so) into the proper folder for your Flutter project, usually found at android/app/src/main/jniLibs/.

Next, make changes in your android/app/build.gradle in such a manner that it contains this library:

android {
 ...
 sourceSets {
 main {
 jniLibs.srcDirs = ['src/main/jniLibs']
 }
 }
}
Enter fullscreen mode Exit fullscreen mode

For iOS: Your Flutter project should have the compiled Rust library (for instance, libyourlibrary.a) in the correct place, generally ios/.

To link the Rust library, you should update your Xcode project settings as follows:

  • Open ios/Runner.xcworkspace in Xcode.
  • Choose your project from the project navigator.
  • Select the target for your app.
  • Go to the "Build Phases" tab.
  • Add the Rust library to "Link Binary With Libraries."

  • Run the app: Open a terminal, navigate to the directory of your Flutter project, and execute:

flutter run
Enter fullscreen mode Exit fullscreen mode

The command builds your Flutter application and runs it on an emulator or connected device. If every configuration is correct, your app should display the battery status information obtained from the Rust backend.

Output:
GIF-240807_153740[1]

From the Rust back-end, the Flutter app gets the battery status information. When the app launches, it initially shows a message that it’s fetching battery status. Then, once it has retrieved the battery status, it is displayed through UI updating, which shows such things as “Battery Level: 98%” and “Charging Status: Charging.”

Conclusion

Ultimately, a battery test application that uses both Flutter and Rust is a good demonstration of how two technologies for cross-platform development can be combined. The app combines Flutter's user interface capabilities with Rust's performance and system-level access, creating a strong platform for monitoring battery status. Setting up the Flutter environment was the first step of this process. This was followed by configuring Rust and linking the two through a Flutter-Rust Bridge (FRB). We then created a Rust backend that could access our battery status queries connected to the front end of our application, which was built using Fluttering and output information on it in a more user-friendly manner.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .