v1.2.0 — Phase 2: Riverpod providers wired up

- settings_provider: AppSettings (ecuType, pollingInterval) StateNotifier
- bt_provider: BtNotifier (disconnected/connecting/connected/error states),
  btServiceProvider singleton, pairedDevicesProvider FutureProvider,
  internal frameStream piped through StreamController
- sensor_provider: sensorStateProvider StreamProvider (auto S300/KPro),
  latestSensorProvider convenience Provider
- theme_provider: AppThemeVariant (red/green × dark/light) StateNotifier
- main.dart: fully Riverpod — ProviderScope, ConsumerWidget throughout,
  no setState for BT state, providers own all lifecycle
This commit is contained in:
HVBT Dev 2026-04-13 20:13:32 +05:30
parent 11cf7c2b63
commit 6a80d8fc1f
6 changed files with 583 additions and 479 deletions

View File

@ -0,0 +1,138 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../bluetooth/bt_poller.dart';
import '../bluetooth/bt_service.dart';
import 'settings_provider.dart';
// Connection status enum
enum BtStatus { disconnected, connecting, connected, error }
// State
class BtState {
final BtStatus status;
final String? deviceName;
final String? error;
final int frameCount;
final int droppedFrames;
const BtState({
required this.status,
this.deviceName,
this.error,
this.frameCount = 0,
this.droppedFrames = 0,
});
const BtState.disconnected()
: this(status: BtStatus.disconnected);
BtState connecting(String name) =>
BtState(status: BtStatus.connecting, deviceName: name);
BtState connected(String name) =>
BtState(status: BtStatus.connected, deviceName: name);
BtState withError(String msg) =>
BtState(status: BtStatus.error, deviceName: deviceName, error: msg);
BtState withFrameStats(int frames, int dropped) => BtState(
status: status,
deviceName: deviceName,
frameCount: frames,
droppedFrames: dropped,
);
bool get isConnected => status == BtStatus.connected;
bool get isConnecting => status == BtStatus.connecting;
}
// Notifier
class BtNotifier extends StateNotifier<BtState> {
final Ref _ref;
BtPoller? _poller;
StreamSubscription<Uint8List>? _pollSub;
final StreamController<Uint8List> _frameController =
StreamController<Uint8List>.broadcast();
BtNotifier(this._ref) : super(const BtState.disconnected());
/// Raw validated 128-byte frame stream consumed by sensorStateProvider.
Stream<Uint8List> get frameStream => _frameController.stream;
Future<void> connect(BluetoothDevice device) async {
state = state.connecting(device.name ?? device.address);
final service = _ref.read(btServiceProvider);
final settings = _ref.read(settingsProvider);
try {
await service.connect(device);
_poller = BtPoller(
service,
ecuType: settings.ecuType,
pollingInterval: settings.pollingInterval,
);
_pollSub = _poller!.frameStream.listen(
(frame) {
_frameController.add(frame);
state = state.withFrameStats(
_poller!.validFrames, _poller!.droppedFrames);
},
onError: (Object e) {
_frameController.addError(e);
state = state.withError(e.toString());
},
onDone: () => state = state.withError('BT stream closed'),
);
_poller!.start();
state = state.connected(device.name ?? device.address);
} catch (e) {
await service.disconnect();
state = state.withError(e.toString());
}
}
Future<void> disconnect() async {
await _pollSub?.cancel();
_poller?.stop();
_poller?.dispose();
_poller = null;
await _ref.read(btServiceProvider).disconnect();
state = const BtState.disconnected();
}
@override
void dispose() {
_pollSub?.cancel();
_poller?.dispose();
_frameController.close();
super.dispose();
}
}
// Providers
/// Singleton BtService lives as long as the app.
final btServiceProvider = Provider<BtService>((ref) {
final service = BtService();
ref.onDispose(service.dispose);
return service;
});
/// BT connection state notifier.
final btProvider =
StateNotifierProvider<BtNotifier, BtState>((ref) => BtNotifier(ref));
/// Paired BT devices list (re-fetchable via ref.refresh).
final pairedDevicesProvider = FutureProvider<List<BluetoothDevice>>((ref) {
return FlutterBluetoothSerial.instance.getBondedDevices();
});

View File

@ -0,0 +1,29 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../bluetooth/bt_poller.dart';
import '../protocol/kpro_parser.dart';
import '../protocol/s300_parser.dart';
import '../protocol/sensor_state.dart';
import 'bt_provider.dart';
import 'settings_provider.dart';
/// Emits a parsed [SensorState] for every valid frame received from the ECU.
/// Automatically picks S300 or KPro parser based on [settingsProvider].
final sensorStateProvider = StreamProvider<SensorState>((ref) {
final ecuType = ref.watch(settingsProvider).ecuType;
final btNotifier = ref.watch(btProvider.notifier);
return btNotifier.frameStream.map((frame) {
if (ecuType == EcuType.s300) return parseS300(frame);
return parseKPro(frame);
});
});
/// Last successfully parsed sensor state (never null after first frame).
/// Falls back to SensorState.zero() before any data arrives.
final latestSensorProvider = Provider<SensorState>((ref) {
return ref
.watch(sensorStateProvider)
.whenData((s) => s)
.value ?? SensorState.zero();
});

View File

@ -0,0 +1,31 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../bluetooth/bt_poller.dart';
class AppSettings {
final EcuType ecuType;
final Duration pollingInterval;
const AppSettings({
this.ecuType = EcuType.s300,
this.pollingInterval = const Duration(milliseconds: 100),
});
AppSettings copyWith({EcuType? ecuType, Duration? pollingInterval}) =>
AppSettings(
ecuType: ecuType ?? this.ecuType,
pollingInterval: pollingInterval ?? this.pollingInterval,
);
}
class SettingsNotifier extends StateNotifier<AppSettings> {
SettingsNotifier() : super(const AppSettings());
void setEcuType(EcuType type) => state = state.copyWith(ecuType: type);
void setPollingInterval(Duration d) =>
state = state.copyWith(pollingInterval: d);
}
final settingsProvider =
StateNotifierProvider<SettingsNotifier, AppSettings>(
(ref) => SettingsNotifier(),
);

View File

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
enum AppThemeVariant { redDark, redLight, greenDark, greenLight }
class ThemeNotifier extends StateNotifier<AppThemeVariant> {
ThemeNotifier() : super(AppThemeVariant.redDark);
void set(AppThemeVariant v) => state = v;
}
final themeProvider =
StateNotifierProvider<ThemeNotifier, AppThemeVariant>(
(ref) => ThemeNotifier(),
);
/// Returns the accent color for the current theme variant.
extension AppThemeVariantX on AppThemeVariant {
Color get accentColor {
switch (this) {
case AppThemeVariant.redDark:
case AppThemeVariant.redLight:
return const Color(0xFFFF4444);
case AppThemeVariant.greenDark:
case AppThemeVariant.greenLight:
return const Color(0xFF00E676);
}
}
bool get isDark =>
this == AppThemeVariant.redDark || this == AppThemeVariant.greenDark;
}

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ description: HV BT Automotive ECU Dashboard
publish_to: 'none'
version: 1.0.1+2
version: 1.2.0+3
environment:
sdk: '>=3.0.0 <4.0.0'