Files
chanora/poc/flutter_rust_bridge_hello/lib/main.dart
T
EdisonJwa 2bbad5feb9 feat(poc/bridge): add flutter_rust_bridge hello spike
Proof-of-concept proving the Flutter/Rust bridge exit criterion from
docs/architecture/proof-of-concept-plan.md §2:
  "Flutter can call Rust and receive event stream data."

The spike exposes one synchronous fallible command (greet) returning
a typed GreetResult / GreetError DTO, and one async event stream
(counter_stream) emitting typed CounterTick events. The Flutter app
demonstrates both flows on a Material 3 surface; the headless test
suite in test/poc_verification_test.dart exercises the same API
directly through dart:ffi.

Verified on 2026-05-13 (Linux desktop, Flutter 3.41.9 / Dart 3.11.5,
flutter_rust_bridge 2.12.0, Rust 1.95). All three tests pass:
  - greet() returns typed result for valid input
  - greet() surfaces typed error for empty input
  - counterStream() delivers the expected event sequence

Authority: PoC plan §2, DEC-014 (typed Flutter/Rust bridge),
SAD-068, SDD-079, SysDes-049.
Naming note: the PoC plan lists this as flutter-rust-bridge-hello,
but Dart pubspec.yaml package names require underscores; the
directory uses underscores accordingly.
Not product code; not promoted into chanora_bridge.

Layout note: includes the full Flutter platform scaffold (android,
ios, macos, windows, web, linux). Only the Linux desktop target has
been built and verified.
2026-05-14 12:26:09 +08:00

176 lines
5.5 KiB
Dart

// Chanora PoC — Flutter side of the flutter_rust_bridge hello spike.
//
// Demonstrates both halves of the PoC exit criterion
// (docs/architecture/proof-of-concept-plan.md §2):
// 1) Flutter calls a Rust command and renders the typed result.
// 2) Flutter subscribes to a Rust event stream and renders ticks live.
//
// This is PoC code, not product UI. The Chanora Design System (Material 3)
// lives in docs/ui-ux/ and is owned by the product Flutter app, not here.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_rust_bridge_hello/src/rust/api/simple.dart';
import 'package:flutter_rust_bridge_hello/src/rust/frb_generated.dart';
Future<void> main() async {
await RustLib.init();
runApp(const PocApp());
}
class PocApp extends StatelessWidget {
const PocApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Chanora PoC — FRB hello',
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xFF3F51B5),
),
home: const PocHome(),
);
}
}
class PocHome extends StatefulWidget {
const PocHome({super.key});
@override
State<PocHome> createState() => _PocHomeState();
}
class _PocHomeState extends State<PocHome> {
final _nameCtl = TextEditingController(text: 'Chanora');
String _greetOutput = '(no call yet)';
bool _greetOk = true;
StreamSubscription<CounterTick>? _streamSub;
final List<CounterTick> _ticks = <CounterTick>[];
bool _streamRunning = false;
@override
void dispose() {
_streamSub?.cancel();
_nameCtl.dispose();
super.dispose();
}
void _callGreet() {
try {
final r = greet(name: _nameCtl.text);
setState(() {
_greetOk = true;
_greetOutput = '${r.message}\n(elapsed: ${r.elapsedMicros} µs)';
});
} catch (e) {
setState(() {
_greetOk = false;
_greetOutput = 'error: $e';
});
}
}
void _startStream() {
_streamSub?.cancel();
setState(() {
_ticks.clear();
_streamRunning = true;
});
final stream = counterStream(count: 5, intervalMs: 400);
_streamSub = stream.listen(
(tick) => setState(() => _ticks.add(tick)),
onDone: () => setState(() => _streamRunning = false),
onError: (e) => setState(() {
_streamRunning = false;
_ticks.add(CounterTick(seq: BigInt.from(-1), note: 'error: $e'));
}),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Chanora PoC — flutter_rust_bridge hello'),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: ListView(
children: [
Text('1. Command (Dart → Rust → typed Result)', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: TextField(
controller: _nameCtl,
decoration: const InputDecoration(
labelText: 'name',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 12),
FilledButton(onPressed: _callGreet, child: const Text('greet')),
],
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _greetOk
? theme.colorScheme.surfaceContainerHighest
: theme.colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Text(_greetOutput, style: const TextStyle(fontFamily: 'monospace')),
),
const Divider(height: 32),
Text('2. Event stream (Rust → Dart Stream<CounterTick>)', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
Row(
children: [
FilledButton(
onPressed: _streamRunning ? null : _startStream,
child: Text(_streamRunning ? 'streaming…' : 'start counter_stream'),
),
const SizedBox(width: 12),
Text('${_ticks.length} ticks received'),
],
),
const SizedBox(height: 8),
Container(
constraints: const BoxConstraints(minHeight: 120),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: _ticks.isEmpty
? const Text('(no ticks yet)')
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final t in _ticks)
Text('seq=${t.seq} ${t.note}',
style: const TextStyle(fontFamily: 'monospace')),
],
),
),
const SizedBox(height: 24),
Text(
'PoC exit criterion (docs/architecture/proof-of-concept-plan.md §2):\n'
'"Flutter can call Rust and receive event stream data."',
style: theme.textTheme.bodySmall,
),
],
),
),
);
}
}