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.
176 lines
5.5 KiB
Dart
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,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|