Building for Android
Completing the remaining steps for Android.
In the preceding sections, we've effectively configured Flutter and Rust code independently. Additionally, we've incorporated IOTA's Rust libraries into Cargo.toml
. Furthermore, we've confirmed the validity of the Rust code for each example by compiling the libraries for our target platforms (Android, macOS, and iOS devices). Now, it's time for the finalizing steps.
Android Setup
If you haven't already, install the cargo-ndk
command using:
cargo install cargo-ndk
In android/app/build.gradle, fix error:
Replace GradleException by FileNotFoundException
Integrate cargo build
into the Gradle build process
If you've had NO problem with the 3rd party library libsodium, 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',
// the 2 ABIs below are used by real Android devices
// '-t', 'armeabi-v7a',
'-t', 'arm64-v8a',
// the below 2 ABIs are usually used for Android simulators,
// add or remove these ABIs as needed.
// '-t', 'x86',
// '-t', 'x86_64',
'-o', '../android/app/src/main/jniLibs', 'build'
if (profileMode != null) {
args profileMode
}
}
}
Otherwise, if you've HAD a problem with the 3rd party library libsodium, 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"
environment SODIUM_LIB_DIR: "/Users/yourname/playground_app/android/app/src/main/jniLibs/arm64-v8a" // <-- ATTENTION: CHECK THE CORRECT FOLDER!!!
environment SODIUM_SHARED: 1
commandLine 'cargo', 'ndk',
// the 2 ABIs below are used by real Android devices
// '-t', 'armeabi-v7a',
'-t', 'arm64-v8a',
// the below 2 ABIs are usually used for Android simulators,
// add or remove these ABIs as needed.
// '-t', 'x86',
// '-t', 'x86_64',
'-o', '../android/app/src/main/jniLibs', 'build'
if (profileMode != null) {
args profileMode
}
}
}
Please note that due to the manual use of libsodium, there are two additional lines related to SODIUM_LIB_DIR and SODIUM_SHARED (compared to the former configurations) -> please also refer to Libsodium library for Android.
Enabling Dynamic Library Loading
Even if the our dynamic library librust.so
has been successfully compiled in Android, you haven't won yet. It also needs to be able to be loaded.
There is an error message that only becomes apparent when using the app, after you have completed all steps and successfully launched the app in Flutter.
Let's say you want to invoke the "Generate Mnemonic" example. Here's where you stumble when you click on "Execute" in Example 2, as the console displays the following message:
Workaround
As the message text also says: The "library 'libc++_shared.so' is not found". So, adding this library is the solution.
Where do we get this one from?
It is provided by the NDK. For example, if you're using NDK v25.2.9519653 on macOS, navigate to the folder ~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/lib/ where you will find subfolders for each of the targets:
Alternatively, here are also some download links from Android NDK 25 on macOS:
Where do we need to place it?
Once you have copied or downloaded the libc++_shared.so
library, you should ensure that you place it in a directory where it can be accessed by your application. To simplify the process, you can place it in the same folder as the librust.so
library.
Preparatory steps on the Dart side
Here, you will find the same steps as in the Simple App.
Generate the Dart Interface
Use this command (you need to be in the root of your project):
flutter_rust_bridge_codegen \
--rust-input rust/src/api.rs \
--dart-output ./lib/bridge_generated.dart \
--dart-decl-output ./lib/bridge_definitions.dart \
--wasm
Include the library
Create a file ffi.dart
and paste this content. As you can see it returns the api variable.
// 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));
Adjust the Dart code in the examples
As the final step, we now need to integrate and use the API (ffi.dart) in each example class. I will provide the final code for all examples here.
It's a bit of effort, but take comfort in knowing that you've covered Android, macOS, and iOS all at once.
Example 1: Get Node Information
The source code for Example 1 can be found at lib/examples/example_0.dart
.
At the top, add:
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart';
import '../ffi.dart';
Replace the fake code of _callFfiNodeInfo()
by:
Future<void> _callFfiNodeInfo() async {
String nodeUrl =
Provider.of<AppProvider>(context, listen: false).currentNetwork.url;
String faucetUrl = Provider.of<AppProvider>(context, listen: false)
.currentNetwork
.faucetApiUrl ??
'';
final NetworkInfo networkInfo =
NetworkInfo(nodeUrl: nodeUrl, faucetUrl: faucetUrl);
try {
customOverlay.show(context);
final receivedText = await api.getNodeInfo(networkInfo: networkInfo);
if (mounted) {
Provider.of<AppProvider>(context, listen: false).nodeInfo =
receivedText;
setState(() => exampleSteps[1].setOutput(receivedText));
}
customOverlay.hide();
} on FfiException catch (e) {
setState(() => exampleSteps[1].setOutput(e.message));
customOverlay.hide();
}
}
Example 2: Generate Mnemonics
The source code for Example 2 can be found at lib/examples/example_1.dart
.
At the top, add:
import '../ffi.dart';
Replace the fake code of _callFfiGenerateMnemonic()
by:
Future<void> _callFfiGenerateMnemonic() async {
customOverlay.show(context);
final receivedText = await api.generateMnemonic();
if (mounted) {
setState(() {
Provider.of<AppProvider>(context, listen: false).mnemonic =
receivedText;
exampleSteps[1].setOutput(receivedText);
});
}
customOverlay.hide();
}
Example 3: Create Wallet Account
The source code for Example 3 can be found at lib/examples/example_2.dart
.
At the top, add:
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart';
import '../ffi.dart';
Replace the fake code of _callFfiCreateWalletAccount()
by:
Future<void> _callFfiCreateWalletAccount() async {
String nodeUrl =
Provider.of<AppProvider>(context, listen: false).currentNetwork.url;
String faucetUrl = Provider.of<AppProvider>(context, listen: false)
.currentNetwork
.faucetApiUrl ??
'';
final NetworkInfo networkInfo =
NetworkInfo(nodeUrl: nodeUrl, faucetUrl: faucetUrl);
final WalletInfo walletInfo = WalletInfo(
alias: exampleSteps[1].output ?? 'Account_1',
mnemonic: Provider.of<AppProvider>(context, listen: false).mnemonic,
strongholdPassword:
exampleSteps[2].output ?? 'my_super_secret_stronghold_password',
strongholdFilepath: _strongholdFilePath,
lastAddress: "",
);
try {
customOverlay.show(context);
final receivedText = await api.createWalletAccount(
networkInfo: networkInfo, walletInfo: walletInfo);
if (mounted) {
setState(() => exampleSteps[3].setOutput(receivedText));
}
customOverlay.hide();
} on FfiException catch (e) {
setState(() => exampleSteps[3].setOutput(e.message));
customOverlay.hide();
}
}
Example 4: Generate Address
The source code for Example 4 can be found at lib/examples/example_3.dart
.
At the top, add:
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart';
import '../ffi.dart';
Replace the fake code of _callFfiGenerateAddress()
by:
Future<void> _callFfiGenerateAddress() async {
final WalletInfo walletInfo = WalletInfo(
alias: exampleSteps[1].output ?? 'Account_1',
mnemonic: Provider.of<AppProvider>(context, listen: false).mnemonic,
strongholdPassword:
exampleSteps[2].output ?? 'my_super_secret_stronghold_password',
strongholdFilepath: _strongholdFilePath,
lastAddress: Provider.of<AppProvider>(context, listen: false).lastAddress,
);
try {
customOverlay.show(context);
final receivedText = await api.generateAddress(walletInfo: walletInfo);
if (mounted) {
setState(() {
Provider.of<AppProvider>(context, listen: false).lastAddress =
receivedText;
exampleSteps[3].setOutput(receivedText);
});
}
customOverlay.hide();
} on FfiException catch (e) {
customOverlay.hide();
setState(() => exampleSteps[3].setOutput(e.message));
}
}
Example 5: Request Funds
The source code for Example 5 can be found at lib/examples/example_4.dart
.
At the top, add:
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart';
import '../ffi.dart';
Replace the fake code of _callFfiRequestFunds()
by:
Future<void> _callFfiRequestFunds() async {
String nodeUrl =
Provider.of<AppProvider>(context, listen: false).currentNetwork.url;
String faucetUrl = Provider.of<AppProvider>(context, listen: false)
.currentNetwork
.faucetApiUrl ??
'';
final NetworkInfo networkInfo =
NetworkInfo(nodeUrl: nodeUrl, faucetUrl: faucetUrl);
final WalletInfo walletInfo = WalletInfo(
alias: "",
mnemonic: "",
strongholdPassword: "",
strongholdFilepath: _strongholdFilePath,
lastAddress: Provider.of<AppProvider>(context, listen: false).lastAddress,
);
try {
customOverlay.show(context);
final receivedText = await api.requestFunds(
networkInfo: networkInfo, walletInfo: walletInfo);
if (mounted) {
setState(() => exampleSteps[1].setOutput(receivedText));
}
customOverlay.hide();
} on FfiException catch (e) {
customOverlay.hide();
setState(() => exampleSteps[1].setOutput(e.message));
}
}
Example 6: Check Balance
The source code for Example 6 can be found at lib/examples/example_5.dart
.
At the top, add:
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart';
import 'package:provider/provider.dart';
import '../coin_utils.dart';
import '../data/app_provider.dart';
import '../ffi.dart';
Replace the fake code of _callFfiCheckBalance()
by:
Future<void> _callFfiCheckBalance() async {
String coinCurrency =
Provider.of<AppProvider>(context, listen: false).currentNetwork.coin;
final WalletInfo walletInfo = WalletInfo(
alias: exampleSteps[0].output ?? 'Account_1',
mnemonic: "",
strongholdPassword: "",
strongholdFilepath: _strongholdFilePath,
lastAddress: Provider.of<AppProvider>(context, listen: false).lastAddress,
);
try {
customOverlay.show(context);
final receivedBaseCoinBalance =
await api.checkBalance(walletInfo: walletInfo);
if (mounted) {
String totalString =
displayBalance(receivedBaseCoinBalance.total, coinCurrency);
String availableString =
displayBalance(receivedBaseCoinBalance.available, coinCurrency);
String result =
'Total: $totalString\nAvailable: $availableString';
setState(() {
exampleSteps[1].setOutput(result);
Provider.of<AppProvider>(context, listen: false).balanceTotal =
receivedBaseCoinBalance.total;
Provider.of<AppProvider>(context, listen: false).balanceAvailable =
receivedBaseCoinBalance.available;
});
}
customOverlay.hide();
} on FfiException catch (e) {
customOverlay.hide();
setState(() => exampleSteps[1].setOutput(e.message));
}
}
Example 7: Create DID
The source code for Example 7 can be found at lib/examples/example_6.dart
.
At the top, add:
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart';
import '../ffi.dart';
Replace the fake code of _callFfiCreateDecentralizedIdentifier()
by:
Future<void> _callFfiCreateDecentralizedIdentifier() async {
String nodeUrl =
Provider.of<AppProvider>(context, listen: false).currentNetwork.url;
final NetworkInfo networkInfo =
NetworkInfo(nodeUrl: nodeUrl, faucetUrl: '');
final WalletInfo walletInfo = WalletInfo(
alias: "",
mnemonic: "",
strongholdPassword: 'my_super_secret_stronghold_password',
strongholdFilepath: _strongholdFilePath,
lastAddress: Provider.of<AppProvider>(context, listen: false).lastAddress,
);
try {
customOverlay.show(context);
final receivedText = await api.createDecentralizedIdentifier(
networkInfo: networkInfo, walletInfo: walletInfo);
if (mounted) {
setState(() {
exampleSteps[0].setOutput(receivedText);
});
}
customOverlay.hide();
} on FfiException catch (e) {
customOverlay.hide();
setState(() => exampleSteps[0].setOutput(e.message));
}
}
Bin to Hex
The source code can be found at lib/widgets/my_drawer.dart
.
At the top, add:
import '../ffi.dart';
Replace the fake code in line 59 receivedText = 'fake 0000 0000 0000';
by:
receivedText = await api.binToHex(val: substr, len: 1);
Video
Follow the video for the remaining steps.