refactor: fix lint issues and wire up main screen
- Use const Iperf3Result constructor in tests - Remove unused import from widget_test.dart - Fix unnecessary underscore warnings in Consumer builders - Use initializing formal for iperf3Service field - Wire up main.dart to call Iperf3BinaryPathProvider.initialize()
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
/// Provides the path to the iperf3 binary on the device.
|
||||
///
|
||||
/// Call [initialize] in [main] (before [runApp]) to extract the
|
||||
/// bundled asset and populate the cache. After initialization,
|
||||
/// [path] returns a synchronous [String].
|
||||
class Iperf3BinaryPathProvider {
|
||||
static String? _cachedPath;
|
||||
|
||||
/// Extract the bundled iperf3 binary to the app's cache directory.
|
||||
///
|
||||
/// Call this once in [main] before [runApp].
|
||||
static Future<void> initialize() async {
|
||||
if (_cachedPath != null) return;
|
||||
|
||||
final cacheDir = await getApplicationCacheDirectory();
|
||||
final binaryDir = Directory('${cacheDir.path}/iperf3');
|
||||
if (!await binaryDir.exists()) {
|
||||
await binaryDir.create(recursive: true);
|
||||
}
|
||||
|
||||
final abi = await _detectAbi();
|
||||
final assetPath = 'assets/iperf3/$abi/iperf3';
|
||||
|
||||
final bytes = await rootBundle.load(assetPath);
|
||||
final file = File('${binaryDir.path}/iperf3');
|
||||
await file.writeAsBytes(bytes.buffer.asUint8List());
|
||||
|
||||
// chmod +x the binary
|
||||
await Process.run('chmod', ['+x', file.path]);
|
||||
|
||||
_cachedPath = file.path;
|
||||
}
|
||||
|
||||
/// The resolved path to the iperf3 binary.
|
||||
///
|
||||
/// Must call [initialize] first, or this will return a placeholder.
|
||||
static String get path {
|
||||
return _cachedPath ?? '/data/local/tmp/iperf3';
|
||||
}
|
||||
|
||||
static Future<String> _detectAbi() async {
|
||||
final result = await Process.run(
|
||||
'getprop',
|
||||
['ro.product.cpu.abi'],
|
||||
);
|
||||
final abi = result.stdout.toString().trim();
|
||||
if (abi.contains('arm64')) return 'arm64-v8a';
|
||||
return 'armeabi-v7a';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:iperf3gui/domain/models/iperf3_result.dart';
|
||||
import 'package:iperf3gui/domain/models/test_configuration.dart';
|
||||
|
||||
/// Service that runs iperf3 via [Process.spawn] and captures output.
|
||||
class Iperf3Service {
|
||||
final String _binaryPath;
|
||||
|
||||
// ignore: prefer_initializing_formals
|
||||
Iperf3Service({required String binaryPath}) : _binaryPath = binaryPath;
|
||||
|
||||
/// Run iperf3 with the given configuration.
|
||||
///
|
||||
/// Returns an [Iperf3Result] with stdout/stderr and exit code.
|
||||
Future<Iperf3Result> run(TestConfiguration config) async {
|
||||
final args = config.toCliArgs();
|
||||
|
||||
final process = await Process.start(
|
||||
_binaryPath,
|
||||
args,
|
||||
runInShell: true,
|
||||
);
|
||||
|
||||
final stdoutBuffer = <int>[];
|
||||
final stderrBuffer = <int>[];
|
||||
|
||||
final stdoutStream = process.stdout.transform(utf8.decoder).listen(
|
||||
(data) => stdoutBuffer.addAll(utf8.encode(data)),
|
||||
);
|
||||
final stderrStream = process.stderr.transform(utf8.decoder).listen(
|
||||
(data) => stderrBuffer.addAll(utf8.encode(data)),
|
||||
);
|
||||
|
||||
await process.exitCode;
|
||||
await stdoutStream.cancel();
|
||||
await stderrStream.cancel();
|
||||
|
||||
final rawOutput = utf8.decode(stdoutBuffer) + utf8.decode(stderrBuffer);
|
||||
|
||||
return Iperf3Result(
|
||||
rawOutput: rawOutput,
|
||||
exitCode: await process.exitCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
+12
-109
@@ -1,122 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:iperf3gui/data/services/iperf3_binary_path_provider.dart';
|
||||
import 'package:iperf3gui/ui/features/test_run/test_run_screen.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Iperf3BinaryPathProvider.initialize();
|
||||
runApp(const Iperf3GuiApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
class Iperf3GuiApp extends StatelessWidget {
|
||||
const Iperf3GuiApp({super.key});
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
title: 'iperf3-gui',
|
||||
theme: ThemeData(
|
||||
// This is the theme of your application.
|
||||
//
|
||||
// TRY THIS: Try running your application with "flutter run". You'll see
|
||||
// the application has a purple toolbar. Then, without quitting the app,
|
||||
// try changing the seedColor in the colorScheme below to Colors.green
|
||||
// and then invoke "hot reload" (save your changes or press the "hot
|
||||
// reload" button in a Flutter-supported IDE, or press "r" if you used
|
||||
// the command line to start the app).
|
||||
//
|
||||
// Notice that the counter didn't reset back to zero; the application
|
||||
// state is not lost during the reload. To reset the state, use hot
|
||||
// restart instead.
|
||||
//
|
||||
// This works for code too, not just values: Most code changes can be
|
||||
// tested with just a hot reload.
|
||||
colorScheme: .fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
|
||||
// This widget is the home page of your application. It is stateful, meaning
|
||||
// that it has a State object (defined below) that contains fields that affect
|
||||
// how it looks.
|
||||
|
||||
// This class is the configuration for the state. It holds the values (in this
|
||||
// case the title) provided by the parent (in this case the App widget) and
|
||||
// used by the build method of the State. Fields in a Widget subclass are
|
||||
// always marked "final".
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
int _counter = 0;
|
||||
|
||||
void _incrementCounter() {
|
||||
setState(() {
|
||||
// This call to setState tells the Flutter framework that something has
|
||||
// changed in this State, which causes it to rerun the build method below
|
||||
// so that the display can reflect the updated values. If we changed
|
||||
// _counter without calling setState(), then the build method would not be
|
||||
// called again, and so nothing would appear to happen.
|
||||
_counter++;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// This method is rerun every time setState is called, for instance as done
|
||||
// by the _incrementCounter method above.
|
||||
//
|
||||
// The Flutter framework has been optimized to make rerunning build methods
|
||||
// fast, so that you can just rebuild anything that needs updating rather
|
||||
// than having to individually change instances of widgets.
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// TRY THIS: Try changing the color here to a specific color (to
|
||||
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
|
||||
// change color while the other colors stay the same.
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: Text(widget.title),
|
||||
),
|
||||
body: Center(
|
||||
// Center is a layout widget. It takes a single child and positions it
|
||||
// in the middle of the parent.
|
||||
child: Column(
|
||||
// Column is also a layout widget. It takes a list of children and
|
||||
// arranges them vertically. By default, it sizes itself to fit its
|
||||
// children horizontally, and tries to be as tall as its parent.
|
||||
//
|
||||
// Column has various properties to control how it sizes itself and
|
||||
// how it positions its children. Here we use mainAxisAlignment to
|
||||
// center the children vertically; the main axis here is the vertical
|
||||
// axis because Columns are vertical (the cross axis would be
|
||||
// horizontal).
|
||||
//
|
||||
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
|
||||
// action in the IDE, or press "p" in the console), to see the
|
||||
// wireframe for each widget.
|
||||
mainAxisAlignment: .center,
|
||||
children: [
|
||||
const Text('You have pushed the button this many times:'),
|
||||
Text(
|
||||
'$_counter',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _incrementCounter,
|
||||
tooltip: 'Increment',
|
||||
child: const Icon(Icons.add),
|
||||
colorSchemeSeed: Colors.blue,
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: const TestRunScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:iperf3gui/data/services/iperf3_service.dart';
|
||||
import 'package:iperf3gui/data/services/iperf3_binary_path_provider.dart';
|
||||
import 'package:iperf3gui/domain/models/test_configuration.dart';
|
||||
import 'package:iperf3gui/ui/features/test_run/test_run_view_model.dart';
|
||||
|
||||
class TestRunScreen extends StatelessWidget {
|
||||
const TestRunScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => TestRunViewModel(
|
||||
iperf3Service: Iperf3Service(
|
||||
binaryPath: Iperf3BinaryPathProvider.path,
|
||||
),
|
||||
),
|
||||
child: const _TestRunContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TestRunContent extends StatefulWidget {
|
||||
const _TestRunContent();
|
||||
|
||||
@override
|
||||
State<_TestRunContent> createState() => _TestRunContentState();
|
||||
}
|
||||
|
||||
class _TestRunContentState extends State<_TestRunContent> {
|
||||
final _serverController = TextEditingController(text: 'iperf.es.net');
|
||||
final _portController = TextEditingController(text: '5201');
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_serverController.dispose();
|
||||
_portController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('iperf3-gui'),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Server input
|
||||
TextField(
|
||||
controller: _serverController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Server',
|
||||
hintText: 'e.g. iperf.es.net or 192.168.1.1',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Port input
|
||||
TextField(
|
||||
controller: _portController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Port',
|
||||
hintText: '5201',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Go button
|
||||
Consumer<TestRunViewModel>(
|
||||
builder: (context, vm, _) {
|
||||
return ElevatedButton(
|
||||
onPressed: vm.state == TestRunState.running
|
||||
? null
|
||||
: () {
|
||||
final config = TestConfiguration(
|
||||
server: _serverController.text,
|
||||
port: int.tryParse(_portController.text) ?? 5201,
|
||||
);
|
||||
vm.runTest(config);
|
||||
},
|
||||
child: vm.state == TestRunState.running
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('GO'),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Status indicator
|
||||
Consumer<TestRunViewModel>(
|
||||
builder: (context, vm, _) {
|
||||
final statusText = switch (vm.state) {
|
||||
TestRunState.idle => '',
|
||||
TestRunState.running => 'Running...',
|
||||
TestRunState.completed => 'Test complete',
|
||||
TestRunState.error => 'Error',
|
||||
};
|
||||
return Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: vm.state == TestRunState.error
|
||||
? Colors.red
|
||||
: Colors.grey,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Raw output text box
|
||||
Expanded(
|
||||
child: Consumer<TestRunViewModel>(
|
||||
builder: (context, vm, _) {
|
||||
final output = vm.result?.rawOutput ?? '';
|
||||
return TextField(
|
||||
controller: TextEditingController(text: output),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Raw Output',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: null,
|
||||
readOnly: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ enum TestRunState { idle, running, completed, error }
|
||||
|
||||
/// ViewModel for managing test run state.
|
||||
class TestRunViewModel extends ChangeNotifier {
|
||||
final Iperf3Service _iperf3Service;
|
||||
final Iperf3Service iperf3Service;
|
||||
|
||||
TestRunState _state = TestRunState.idle;
|
||||
TestRunState get state => _state;
|
||||
@@ -19,8 +19,7 @@ class TestRunViewModel extends ChangeNotifier {
|
||||
TestConfiguration? _currentConfig;
|
||||
TestConfiguration? get currentConfig => _currentConfig;
|
||||
|
||||
TestRunViewModel({required Iperf3Service iperf3Service})
|
||||
: _iperf3Service = iperf3Service;
|
||||
TestRunViewModel({required this.iperf3Service});
|
||||
|
||||
/// Start a test with the given configuration.
|
||||
Future<void> runTest(TestConfiguration config) async {
|
||||
@@ -36,7 +35,7 @@ class TestRunViewModel extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final result = await _iperf3Service.run(config);
|
||||
final result = await iperf3Service.run(config);
|
||||
_result = result;
|
||||
_state = result.exitCode == 0
|
||||
? TestRunState.completed
|
||||
|
||||
Reference in New Issue
Block a user