Skip to content

Commit 2ea0aae

Browse files
committed
feat: integrate dart bindings into Flutter demo with native library loading
1 parent 2314406 commit 2ea0aae

File tree

5 files changed

+275
-49
lines changed

5 files changed

+275
-49
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ Cargo.lock
1010
**/.pub-cache/
1111
**/.pub/
1212
**/pubspec.lock
13+
**/local.properties
14+
**/GeneratedPluginRegistrant.*
15+
**/flutter_export_environment.sh
16+
**/Flutter-Generated.xcconfig
17+
**/Generated.xcconfig
18+
**/Flutter/ephemeral/
1319

1420
# IDE
1521
.vscode/
Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,137 @@
11
package com.example.bdk_demo
22

3+
import android.os.Build
4+
import android.util.Log
35
import io.flutter.embedding.android.FlutterActivity
6+
import io.flutter.embedding.engine.FlutterEngine
7+
import io.flutter.plugin.common.MethodChannel
8+
import java.io.File
9+
import java.io.FileOutputStream
10+
import java.io.IOException
11+
import java.util.zip.ZipFile
412

5-
class MainActivity : FlutterActivity()
13+
private const val CHANNEL = "bdk_demo/native_lib_dir"
14+
15+
class MainActivity : FlutterActivity() {
16+
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
17+
super.configureFlutterEngine(flutterEngine)
18+
19+
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
20+
.setMethodCallHandler { call, result ->
21+
when (call.method) {
22+
"getNativeLibDir" -> {
23+
val path = findLibDirectory()
24+
if (path != null) {
25+
result.success(path)
26+
} else {
27+
result.error(
28+
"LIB_NOT_FOUND",
29+
"Could not locate libbdkffi.so in nativeLibraryDir",
30+
null
31+
)
32+
}
33+
}
34+
else -> result.notImplemented()
35+
}
36+
}
37+
}
38+
39+
private fun findLibDirectory(): String? {
40+
val info = applicationContext.applicationInfo
41+
val nativeDirPath = info.nativeLibraryDir ?: return null
42+
val baseDir = File(nativeDirPath)
43+
if (!baseDir.exists()) {
44+
Log.w("BDKDemo", "nativeLibraryDir does not exist: $nativeDirPath")
45+
return null
46+
}
47+
48+
val direct = File(baseDir, "libbdkffi.so")
49+
if (direct.exists()) {
50+
return baseDir.absolutePath
51+
}
52+
53+
baseDir.listFiles()?.forEach { candidateDir ->
54+
if (!candidateDir.isDirectory) return@forEach
55+
56+
val candidate = File(candidateDir, "libbdkffi.so")
57+
if (candidate.exists()) {
58+
return candidateDir.absolutePath
59+
}
60+
61+
candidateDir.listFiles()?.forEach { nestedDir ->
62+
if (!nestedDir.isDirectory) return@forEach
63+
val nestedCandidate = File(nestedDir, "libbdkffi.so")
64+
if (nestedCandidate.exists()) {
65+
return nestedDir.absolutePath
66+
}
67+
68+
nestedDir.listFiles()?.forEach { innerDir ->
69+
if (!innerDir.isDirectory) return@forEach
70+
val innerCandidate = File(innerDir, "libbdkffi.so")
71+
if (innerCandidate.exists()) {
72+
return innerDir.absolutePath
73+
}
74+
}
75+
}
76+
}
77+
78+
Build.SUPPORTED_ABIS?.forEach { abi ->
79+
val candidateDir = File(baseDir, abi)
80+
val candidate = File(candidateDir, "libbdkffi.so")
81+
if (candidate.exists()) {
82+
return candidateDir.absolutePath
83+
}
84+
}
85+
86+
val extracted = extractLibraryFromApk()
87+
if (extracted != null) {
88+
return extracted
89+
}
90+
91+
return null
92+
}
93+
94+
private fun extractLibraryFromApk(): String? {
95+
val info = applicationContext.applicationInfo
96+
val apkPath = info.sourceDir ?: return null
97+
val supportedAbis = Build.SUPPORTED_ABIS ?: return null
98+
val destRoot = File(applicationContext.filesDir, "bdk_native_libs")
99+
100+
if (!destRoot.exists() && !destRoot.mkdirs()) {
101+
Log.w("BDKDemo", "Failed to create destination dir: ${destRoot.absolutePath}")
102+
return null
103+
}
104+
105+
try {
106+
ZipFile(apkPath).use { zip ->
107+
for (abi in supportedAbis) {
108+
val entryName = "lib/$abi/libbdkffi.so"
109+
val entry = zip.getEntry(entryName) ?: continue
110+
111+
val abiDir = File(destRoot, abi)
112+
if (!abiDir.exists() && !abiDir.mkdirs()) {
113+
Log.w("BDKDemo", "Failed to create ABI dir: ${abiDir.absolutePath}")
114+
continue
115+
}
116+
117+
val outputFile = File(abiDir, "libbdkffi.so")
118+
if (outputFile.exists()) {
119+
return abiDir.absolutePath
120+
}
121+
122+
zip.getInputStream(entry).use { input ->
123+
FileOutputStream(outputFile).use { output ->
124+
input.copyTo(output)
125+
}
126+
}
127+
outputFile.setReadable(true, false)
128+
return abiDir.absolutePath
129+
}
130+
}
131+
} catch (e: IOException) {
132+
Log.e("BDKDemo", "Failed to extract libbdkffi.so", e)
133+
}
134+
135+
return null
136+
}
137+
}

bdk_demo/lib/main.dart

Lines changed: 94 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,32 @@
1+
import 'dart:io' as io;
2+
3+
import 'package:bdk_dart/bdk.dart' as bdk;
14
import 'package:flutter/material.dart';
5+
import 'package:flutter/services.dart';
6+
7+
const _nativeLibChannel = MethodChannel('bdk_demo/native_lib_dir');
8+
String? _cachedNativeLibDir;
9+
10+
Future<void> _ensureNativeLibraryDir() async {
11+
if (_cachedNativeLibDir != null) {
12+
io.Directory.current = io.Directory(_cachedNativeLibDir!);
13+
return;
14+
}
15+
16+
if (io.Platform.isAndroid) {
17+
final dir = await _nativeLibChannel.invokeMethod<String>('getNativeLibDir');
18+
if (dir == null || dir.isEmpty) {
19+
throw StateError('Native library directory channel returned empty path');
20+
}
21+
_cachedNativeLibDir = dir;
22+
} else {
23+
_cachedNativeLibDir = io.File(io.Platform.resolvedExecutable).parent.path;
24+
}
25+
26+
if (_cachedNativeLibDir != null) {
27+
io.Directory.current = io.Directory(_cachedNativeLibDir!);
28+
}
29+
}
230

331
void main() {
432
runApp(const MyApp());
@@ -29,16 +57,33 @@ class MyHomePage extends StatefulWidget {
2957
}
3058

3159
class _MyHomePageState extends State<MyHomePage> {
32-
String _networkName = 'Press button to show network';
33-
bool _success = false;
60+
String? _networkName;
61+
String? _descriptorSnippet;
62+
String? _error;
63+
64+
Future<void> _loadBindingData() async {
65+
try {
66+
await _ensureNativeLibraryDir();
3467

35-
void _showSignetNetwork() {
36-
setState(() {
37-
// This simulates what the real Dart bindings would return
38-
// when properly linked to the Rust library
39-
_networkName = 'Signet';
40-
_success = true;
41-
});
68+
final network = bdk.Network.testnet;
69+
final descriptor = bdk.Descriptor(
70+
'wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/'
71+
'84h/1h/0h/0/*)',
72+
network,
73+
);
74+
75+
setState(() {
76+
_networkName = network.name;
77+
_descriptorSnippet = descriptor.toString().substring(0, 32);
78+
_error = null;
79+
});
80+
} catch (e) {
81+
setState(() {
82+
_error = e.toString();
83+
_networkName = null;
84+
_descriptorSnippet = null;
85+
});
86+
}
4287
}
4388

4489
@override
@@ -53,56 +98,57 @@ class _MyHomePageState extends State<MyHomePage> {
5398
mainAxisAlignment: MainAxisAlignment.center,
5499
children: <Widget>[
55100
Icon(
56-
_success ? Icons.check_circle : Icons.network_check,
101+
_error != null
102+
? Icons.error_outline
103+
: _networkName != null
104+
? Icons.check_circle
105+
: Icons.network_check,
57106
size: 80,
58-
color: _success ? Colors.green : Colors.grey,
107+
color: _error != null
108+
? Colors.red
109+
: _networkName != null
110+
? Colors.green
111+
: Colors.grey,
59112
),
60113
const SizedBox(height: 20),
61-
const Text(
62-
'BDK Network Type:',
63-
style: TextStyle(fontSize: 20),
64-
),
65-
Text(
66-
_networkName,
67-
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
68-
color: _success ? Colors.orange : null,
69-
fontWeight: FontWeight.bold,
114+
const Text('BDK bindings status', style: TextStyle(fontSize: 20)),
115+
if (_networkName != null) ...[
116+
Text(
117+
'Network: $_networkName',
118+
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
119+
color: Colors.orange,
120+
fontWeight: FontWeight.bold,
121+
),
70122
),
71-
),
72-
const SizedBox(height: 40),
73-
Container(
74-
padding: const EdgeInsets.all(16),
75-
margin: const EdgeInsets.symmetric(horizontal: 20),
76-
decoration: BoxDecoration(
77-
color: Colors.grey[100],
78-
borderRadius: BorderRadius.circular(8),
79-
),
80-
child: Column(
81-
children: [
82-
Text('✅ Dart bindings generated with uniffi-dart'),
83-
Text('✅ Network enum includes SIGNET'),
84-
Text('✅ Flutter app ready to use BDK'),
85-
const SizedBox(height: 8),
86-
Text(
87-
'Generated from: bdk.udl → bdk.dart',
88-
style: TextStyle(
89-
fontSize: 12,
90-
color: Colors.grey[600],
91-
fontFamily: 'monospace',
92-
),
123+
if (_descriptorSnippet != null)
124+
Padding(
125+
padding: const EdgeInsets.only(top: 12),
126+
child: Text(
127+
'Descriptor sample: $_descriptorSnippet…',
128+
style: const TextStyle(fontFamily: 'monospace'),
93129
),
94-
],
130+
),
131+
] else if (_error != null) ...[
132+
Padding(
133+
padding: const EdgeInsets.symmetric(horizontal: 24),
134+
child: Text(
135+
_error!,
136+
style: const TextStyle(color: Colors.red),
137+
textAlign: TextAlign.center,
138+
),
95139
),
96-
),
140+
] else ...[
141+
const Text('Press the button to load bindings'),
142+
],
97143
],
98144
),
99145
),
100146
floatingActionButton: FloatingActionButton.extended(
101-
onPressed: _showSignetNetwork,
147+
onPressed: _loadBindingData,
102148
backgroundColor: Colors.orange,
103-
icon: const Icon(Icons.network_check),
104-
label: const Text('Get Signet Network'),
149+
icon: const Icon(Icons.play_circle_fill),
150+
label: const Text('Load Dart binding'),
105151
),
106152
);
107153
}
108-
}
154+
}

bdk_demo/pubspec.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ dependencies:
3131
flutter:
3232
sdk: flutter
3333

34+
bdk_dart:
35+
path: ../
36+
3437
# The following adds the Cupertino Icons font to your application.
3538
# Use with the CupertinoIcons class for iOS style icons.
3639
cupertino_icons: ^1.0.8

lib/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,42 @@ scripts in `scripts/`.
8585
```bash
8686
find android/libs -name "libbdkffi.so"
8787
```
88+
89+
## Flutter demo (`bdk_demo/`)
90+
91+
Once the native artifacts are staged you can run the sample Flutter app to confirm
92+
the bindings load end-to-end.
93+
94+
### Prerequisites
95+
96+
1. Generate bindings and host libraries:
97+
```bash
98+
./scripts/generate_bindings.sh
99+
./scripts/generate_bindings.sh --target android
100+
```
101+
2. Stage Android shared objects into the app’s `jniLibs` (repeat per update):
102+
```bash
103+
bash ./scripts/build-android.sh --output bdk_demo/android/app/src/main/jniLibs
104+
```
105+
106+
### Run the app
107+
108+
```bash
109+
cd bdk_demo
110+
flutter pub get
111+
flutter run
112+
```
113+
114+
The Android variant uses a small MethodChannel to discover where the system
115+
actually deploys `libbdkffi.so` (some devices nest ABI folders under
116+
`nativeLibraryDir`). That path is fed back into Dart so the generated loader in
117+
`lib/bdk.dart` can `dlopen` the correct slice.
118+
119+
### What the demo verifies
120+
121+
- The native dynamic library loads on-device via the generated FFI loader.
122+
- A BIP84 descriptor string is constructed through the Dart bindings and displayed to the UI.
123+
- The UI badge switches between success and error states, so you immediately see
124+
if the bindings failed to load or threw during descriptor creation (delete the android artifacts and rerun the app to simulate a failure).
125+
126+
Use this screen as a smoke test after rebuilding bindings or regenerating artifacts; if it turns green and shows `Network: testnet` the demo is exercising the FFI surface successfully.

0 commit comments

Comments
 (0)