Workflow


A step-by-step guide


The workflow is identical to the one demonstrated in the video. I will also provide the commands used in the video for reference. Please keep in mind that this is only a high-level overview for now. More detailed explanations will be provided later on.

⚠️ If you genuinely want to practically code the described steps outlined below, I assume that you have completed the setup of your workplace and all tools, as described in the "Fundamentals" section.

1. Check the Setup

flutter doctor
rustup show

Before you go on, be sure that possible errors are fixed.

2. Initialization Steps

Create Flutter Project

Create Flutter Project

In the first step, the following command sets up a new Flutter project with the necessary file structure and dependencies, ready for you to start developing your app. It creates a directory with the specified project name and populates it with the required Flutter files and folders. Now you're all set to unleash your creativity and build amazing Flutter applications!

flutter create --empty example1

Β 

Create Rust Library Project Project

Create Rust Library Project Project

This includes the required structure for the Rust code and any additional resources or dependencies needed for the backend implementation:

cargo new --lib rust

Β 

Setup the Flutter Rust Bridge

Setup the Flutter Rust Bridge

cargo install flutter_rust_bridge_codegen

flutter pub add --dev ffigen:9.0.1 && flutter pub add ffi

flutter pub add flutter_rust_bridge

flutter pub add -d build_runner

flutter pub add -d freezed

flutter pub add freezed_annotation

Hint: ffigen is already available with a higher version. The Flutter Rust Bridge v1.0 is still working with ffigen version >=8.0.0 and <10.0.0. So I am using version v9.0.1 here.

cargo install cargo-ndk

In Cargo.toml:

[dependencies]
flutter_rust_bridge = "1"

[lib]
crate-type = ["staticlib", "cdylib"]

In android/app/build.gradle, fix error (only if GradleException is found):

Replace GradleException by FileNotFoundException

In android/app/build.gradle, add at the bottom:

[
        Debug: null,
        Profile: '--release',
        Release: '--release'
].each {
    def taskPostfix = it.key
    def profileMode = it.value
    tasks.whenTaskAdded { task ->
        if (task.name == "javaPreCompile$taskPostfix") {
            task.dependsOn "cargoBuild$taskPostfix"
        }
    }
    tasks.register("cargoBuild$taskPostfix", Exec) {
        workingDir "../../rust"  // <-- ATTENTION: CHECK THE CORRECT FOLDER!!!
        environment ANDROID_NDK_HOME: "$ANDROID_NDK"
        commandLine 'cargo', 'ndk',
                '-t', 'arm64-v8a',
               '-o', '../android/app/src/main/jniLibs', 'build'
        if (profileMode != null) {
            args profileMode
        }
    }
}

Your Android Virtual Device should be compatible to "arm64-v8a". "arm64-v8a" is an architecture designation for Android devices. It refers to the 64-bit version of the ARM architecture commonly used in modern Android devices.

The term "arm64" represents the 64-bit version of the ARM architecture, while "v8a" indicates the ABI (Application Binary Interface) associated with that architecture. Thus, the combination of "arm64" and "v8a" refers to the 64-bit ARM architecture running on devices with that ABI.

3. Development Steps

Start coding the frontend

Start coding the frontend

Add a Flutter package that beautifies JSON outputs:

flutter pub add flutter_json_viewer

Write this content into main.dart:

import 'package:flutter/material.dart';
import 'package:flutter_json_viewer/flutter_json_viewer.dart';
import 'dart:convert';
//import 'ffi.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter - Rust - IOTA',
      theme: ThemeData(
        primarySwatch: Colors.green,
      ),
      home: const MyHomePage(title: 'Get Node Info'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  String? _ffiNodeInfo;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView(
        children: [
          Container(height: 16),
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 24),
            child: Card(
              child: Container(
                padding:
                    const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
                child: Column(
                  children: [
                    const Text('Result Pane',
                        style: TextStyle(
                            fontSize: 18, fontWeight: FontWeight.bold)),
                    Container(height: 8),
                    const Divider(height: 1.0, thickness: 1.0),
                    Container(height: 24),
                    JsonViewer(json.decode(_ffiNodeInfo ?? "{}")),
                    //JsonViewer(testArray),
                    Container(height: 24),
                    const Divider(height: 1.0, thickness: 1.0),
                    Container(height: 12),
                    ElevatedButton(
                      onPressed: _callFfiGetNodeInfo,
                      child: const Icon(Icons.play_arrow),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Future<void> _callFfiGetNodeInfo() async {
    const receivedText = '{"name": "HORNET","version": "2.0.0-rc.5"}';
    //final receivedText = await api.getNodeInfo();
    if (mounted) setState(() => _ffiNodeInfo = receivedText);
  }
}

The backend call _callFfiGetNodeInfo() is mocked, meaning that instead of making a real request to the server, a simulated response is generated. This approach allows us to emulate the behavior of the backend without actually relying on a live server. By mocking the backend call, we can focus on testing and developing the frontend functionality independently, ensuring that the app's features and user interactions are working as intended.

Important: Before starting the application, make sure that your Virtual Android Device is running.

You can start the application with the command:

flutter run

Β 

Generate the Dart Interface Code (FFI)

Generate the Dart Interface Code (FFI)

πŸ“š IOTA SDK Library Update πŸ“š The Dependency on iota.rs must be replaced by iota-sdk.rs. To do this, iota-sdk needs to be added as a dependency in Cargo.toml instead of iota-client.

The first step on the Rust side is to include the IOTA library and other necessary resources.

In Cargo.toml add:

[dependencies]
...
iota-client = {
  version = "2.0.1-rc.7",
  default-features = false,
  features = [ "tls" ]
}

serde_json = { version = "1.0.89", default-features = false }
anyhow = "1.0.66"
tokio = { version = "1.21.2", default-features = false, features = ["macros"] }
[dependencies]
...
iota-sdk = { version = "1.1.4", default-features = false, features = [
    "client",
    "tls",
] }

serde_json = { version = "1.0.108", default-features = false }
anyhow = "1.0.75"
tokio = { version = "1.34.0", default-features = false, features = ["full"] }

Create the file api.rs. The file api.rs is YOUR RUST WORKING FILE. The Flutter-Rust-Bridge code generator will identify all public functions within the api.rs file and generate the corresponding Dart Interface from these methods. This means that all public functions available in the Rust code will be exposed and accessible for utilization within the Flutter app.

By automatically generating the Dart Interface, the Flutter-Rust-Bridge simplifies the process of bridging the communication between the Flutter frontend and the Rust backend, enabling seamless interaction and integration between the two languages.

Add this content to api.rs:

use iota_client::Client;
use anyhow::Result;
use tokio::runtime::Runtime;

pub fn get_node_info() -> Result<String> {
    let rt = Runtime::new().unwrap();
    rt.block_on(async {
        let node_url = "https://api.testnet.shimmer.network";

        // Create a client with that node.
        let client = Client::builder()
            .with_node(&node_url)?
            .with_ignore_node_health()
            .finish()?;

        // Get node info.
        let info = client.get_info().await?;

        Ok(serde_json::to_string_pretty(&info).unwrap())
    })

}
use iota_sdk::client::Client;
use anyhow::Result;
use tokio::runtime::Runtime;

pub fn get_node_info() -> Result<String> {
    let rt = Runtime::new().unwrap();
    rt.block_on(async {
        let node_url = "https://api.testnet.shimmer.network";

        // Create a client with that node.
        let client = Client::builder()
            .with_node(&node_url)?
            .with_ignore_node_health()
            .finish()
            .await?;

        // Get node info.
        let info = client.get_info().await?;

        Ok(serde_json::to_string_pretty(&info).unwrap())
    })
}

Replace the content in lib.rs by:

mod api;

Β 

Integrate the backend

Integrate the backend

πŸ“š IOTA SDK Library Update πŸ“š The Dependency on iota.rs must be replaced by iota-sdk.rs.

This one is easy! It's one of the tasks you need to do whenever the Rust API has changed (e.g. after changing method signatures or add/removing methods). In our example, generate the Dart Interface by executing this command:

flutter_rust_bridge_codegen --rust-input rust/src/api.rs --dart-output ./lib/bridge_generated.dart --dart-decl-output ./lib/bridge_definitions.dart

Β 

Integrate the Dart Interface into your frontend code

Integrate the Dart Interface into your frontend code

πŸ“š IOTA SDK Library Update πŸ“š The Dependency on iota.rs must be replaced by iota-sdk.rs.

Next to main.dart, add a new file called ffi.dart and add this content:

// This file initializes the dynamic library and connects it with the stub
// generated by flutter_rust_bridge_codegen.

import 'dart:ffi';

import 'bridge_generated.dart';
import 'bridge_definitions.dart';
export 'bridge_definitions.dart';

// Re-export the bridge so it is only necessary to import this file.
export 'bridge_generated.dart';
import 'dart:io' as io;

const _base = 'rust';

// On MacOS, the dynamic library is not bundled with the binary,
// but rather directly **linked** against the binary.
final _dylib = io.Platform.isWindows ? '$_base.dll' : 'lib$_base.so';

final Rust api = RustImpl(io.Platform.isIOS || io.Platform.isMacOS
    ? DynamicLibrary.executable()
    : DynamicLibrary.open(_dylib));

Integrating the library involves loading it into our project, enabling us to execute its methods and utilize its functionalities.

Now, in main.dart, comment out line 4:

ffi.dart  // remove the two slashs

The final step is to insert the appropriate function calls to invoke the desired methods from the library. In main.dart, update the function _callFfiGetNodeInfo().

Replace:

    const receivedText = '{"name": "HORNET","version": "2.0.0-rc.5"}';
    //final receivedText = await api.getNodeInfo();

by:

    //const receivedText = '{"name": "HORNET","version": "2.0.0-rc.5"}';
    final receivedText = await api.getNodeInfo();

4. Build and Run Step

Build and Run a target

Build and Run a target

Here: We build an run the app on Android!

Important: Before starting the application, make sure that your Virtual Android Device is running.

Open the App with the command:

flutter run

To inform Flutter about the target platform for which it should build with flutter run, the corresponding platform is initiated beforehand.

During the build process, the Rust code is cross-compiled into a library specific to the target platform (here: arm64-v8a). This compiled library is then automatically copied into the Android project folder.

Then the app is launched.

Summary

Here is a brief summary about all steps, in one picture:

Summary of Workflow

All steps of the workflow