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.
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
// 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user