I managed to change my code.
class TimingService {
/*
final List<Stopwatchitem> _data = List.generate(
2,
(i) => Stopwatchitem(
timingpartid: 1,
timingid: 0,
raceorder: 0,
race: '',
racegroup: '',
raceheatindex: '',
heat: 0,
subheat: 0,
lane: 0,
startno: '',
startgroup: '',
club: '',
crew: '',
targetDuration: Duration.zero,
targetDurationString: '',
targetPercent: 0.0,
raceTime: Duration.zero,
raceTimeString: '',
// raceTimeString: '${now.minute}:${now.second},${now.millisecond}',
stopwatchState: 0,
raceEndAbsolute: DateTime.now(),
raceEndTimingtimestampid: 0,
place: 0
),
);
Future<List<Stopwatchitem>> get stopwatchesRaceInitial async {
SecureStorageService storage = SecureStorageService();
TimecalcHelper timecalcHelper = TimecalcHelper();
timecalcHelper.stationHashStart = await storage.getKey(Constants.TIMING_STATIONHASH_START) ?? '';
timecalcHelper.stationHashFinish = await storage.getKey(Constants.TIMING_STATIONHASH_FINISH) ?? '';
List<ParticipantsTblData> participants = await db.fetchAllParticipants();
List<Stopwatchitem> stopwatches = <Stopwatchitem>[];
for (int i=0; i<participants.length; i++) {
String racegroup = participants[i].race;
racegroup += (participants[i].heat != 0) ? ' - heat ${participants[i].heat}' : "";
Duration targetDuration = TimecalcHelper.iso8601ToDuration(participants[i].targetdurationiso8601);
TimeCalcResult timeCalcResult = await timecalcHelper.getRaceTimeOfParticipant(participants[i].timingpartid);
// DateTime now = DateTime.now().toUtc();
stopwatches.add(Stopwatchitem(
timingpartid: participants[i].timingpartid,
timingid: participants[i].timingid,
raceorder: participants[i].raceorder,
race: participants[i].race,
racegroup: racegroup,
raceheatindex: '${participants[i].race}%%${participants[i].heat}',
heat: participants[i].heat,
subheat: participants[i].subheat,
lane: participants[i].lane,
startno: participants[i].startno,
startgroup: participants[i].startgroup,
club: participants[i].club,
crew: participants[i].crew,
targetDuration: targetDuration,
targetDurationString: TimecalcHelper.getDurationAsString(targetDuration),
targetPercent: timeCalcResult.raceTargetPercent,
raceTime: timeCalcResult.raceTime,
raceTimeString: timeCalcResult.raceTimeString,
// raceTimeString: '${now.minute}:${now.second},${now.millisecond}',
stopwatchState: timeCalcResult.raceState,
raceEndAbsolute: timeCalcResult.raceEndAbsolute,
raceEndTimingtimestampid: timeCalcResult.raceEndTimingtimestampid,
place: 0
));
}
return stopwatches;
}
*/
Stream<List<Stopwatchitem>> get stopwatchesRaceStream async* {
SecureStorageService storage = SecureStorageService();
TimecalcHelper timecalcHelper = TimecalcHelper();
timecalcHelper.stationHashStart = await storage.getKey(Constants.TIMING_STATIONHASH_START) ?? '';
timecalcHelper.stationHashFinish = await storage.getKey(Constants.TIMING_STATIONHASH_FINISH) ?? '';
yield* Stream.periodic(Duration(milliseconds: 100), (_) async {
List<ParticipantsTblData> participants = await db.fetchAllParticipants();
List<Stopwatchitem> stopwatches = <Stopwatchitem>[];
for (int i=0; i<participants.length; i++) {
String racegroup = participants[i].race;
racegroup += (participants[i].heat != 0) ? ' - heat ${participants[i].heat}' : "";
Duration targetDuration = TimecalcHelper.iso8601ToDuration(participants[i].targetdurationiso8601);
TimeCalcResult timeCalcResult = await timecalcHelper.getRaceTimeOfParticipant(participants[i].timingpartid);
// DateTime now = DateTime.now().toUtc();
stopwatches.add(Stopwatchitem(
timingpartid: participants[i].timingpartid,
timingid: participants[i].timingid,
raceorder: participants[i].raceorder,
race: participants[i].race,
racegroup: racegroup,
raceheatindex: '${participants[i].race}%%${participants[i].heat}',
heat: participants[i].heat,
subheat: participants[i].subheat,
lane: participants[i].lane,
startno: participants[i].startno,
startgroup: participants[i].startgroup,
club: participants[i].club,
crew: participants[i].crew,
targetDuration: targetDuration,
targetDurationString: TimecalcHelper.getDurationAsString(targetDuration),
targetPercent: timeCalcResult.raceTargetPercent,
raceTime: timeCalcResult.raceTime,
raceTimeString: timeCalcResult.raceTimeString,
// raceTimeString: '${now.minute}:${now.second},${now.millisecond}',
stopwatchState: timeCalcResult.raceState,
raceEndAbsolute: timeCalcResult.raceEndAbsolute,
raceEndTimingtimestampid: timeCalcResult.raceEndTimingtimestampid,
place: 0
));
}
return stopwatches;
}).asyncMap((event) => event);
}
}
New data comes from the TimecalcHelper which gets the values from the internal database.
class StopwatchTimetrialStartPage extends StatefulWidget {
const StopwatchTimetrialStartPage({super.key});
@override
State<StopwatchTimetrialStartPage> createState() => _StopwatchTimetrialStartPage();
}
class _StopwatchTimetrialStartPage extends State<StopwatchTimetrialStartPage> {
final TimingService service = TimingService();
StopwatchesDataSource? _dataSource;
StreamSubscription? _sub;
@override
void initState() {
super.initState();
_sub = service.stopwatchesRaceStream.listen((data) {
if (_dataSource == null) {
/// ✅ FIRST LOAD → full grid build
_dataSource = StopwatchesDataSource(data);
setState(() {});
} else {
/// ✅ SUBSEQUENT UPDATES → only update column
_dataSource!.updateData(data);
}
});
}
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
const smallTextStyle = TextStyle(fontSize: 14, color: Colors.white);
if (_dataSource == null) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
return Scaffold(
appBar: AppBar(title: Text('Stopwatches')),
body:
Column(
children: <Widget>[
Container(
height: 250, // to test Grid scrolling
child:
SfDataGrid(
source: _dataSource!,
verticalScrollPhysics: const ScrollPhysics(), // Scrolling aktivieren
// horizontalScrollPhysics: const NeverScrollableScrollPhysics(),
shrinkWrapRows: true, // show all available rows
allowSwiping: true,
swipeMaxOffset: 100.0,
selectionMode: SelectionMode.single,
columnWidthMode: ColumnWidthMode.auto,
headerGridLinesVisibility: GridLinesVisibility.horizontal,
gridLinesVisibility: GridLinesVisibility.horizontal,
headerRowHeight: 0,
rowHeight: 86,
columns: <GridColumn>[
GridColumn(
columnName: 'timingpartid',
label: Text(''),
visible: false,
),
GridColumn(
columnName: 'stopwatchstate',
label: Text(''),
visible: false,
),
GridColumn(
columnName: 'startno',
label: Text(''),
),
GridColumn(
columnName: 'partdata',
width: 140,
label: Text(''),
),
GridColumn(
columnName: 'racetime',
width: 170,
label: Text(''),
),
],
placeholder: const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.thumb_down_alt_outlined, size: 30),
SizedBox(height: 8),
Text('keine Daten vorhanden', style: TextStyle(fontSize: 16),),
],
),
),
onCellTap: (details) async {
debugPrint(details.rowColumnIndex.columnIndex.toString());
if (details.rowColumnIndex.rowIndex != 0) {
DataGridRow row = _dataSource!.effectiveRows[details.rowColumnIndex.rowIndex - 1];
debugPrint(row.getCells()[0].value.toString());
final int timingpartId = int.parse(row.getCells()[0].value.toString());
final int stopwatchState = int.parse(row.getCells()[1].value.toString());
if (stopwatchState == Constants.STOPWATCH_STATE_WAITING_FOR_START) {
await startStopwatch(timingpartId);
}
}
},
onSwipeStart: (details) {
if (details.swipeDirection == DataGridRowSwipeDirection.startToEnd) {
details.setSwipeMaxOffset(0);
} else if (details.swipeDirection == DataGridRowSwipeDirection.endToStart) {
details.setSwipeMaxOffset(100.0);
}
return true;
},
endSwipeActionsBuilder:
(BuildContext context, DataGridRow row, int rowIndex) {
return GestureDetector(
onTap: () async {
debugPrint(rowIndex.toString());
debugPrint(_dataSource!.rows[rowIndex].getCells().elementAt(0).value.toString());
await setStopwatchToDNS(int.parse(_dataSource!.rows[rowIndex].getCells().elementAt(0).value.toString()));
},
child: const ColoredBox(
color: Colors.redAccent,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Icons.person_off, size: 40, color:Colors.white),
Text('DNS', style: smallTextStyle),
],
)
)
);
}
),
),
Consumer<AuthProvider>(builder: (context, controller, _) {
return TextButton(
child: const Text("logout"),
onPressed: () {
controller.logoutPressed(context);
}
);
})
]
)
);
}
}
class StopwatchesDataSource extends DataGridSource {
final dataTextStyle = const TextStyle(fontSize: 17, color: Colors.white, fontWeight: FontWeight.w400);
final disciplineTextStyle = const TextStyle(fontSize: 17, color: Color(0xffff6700), fontWeight: FontWeight.w700);
final nameTextStyle = TextStyle(fontSize: 14, color: const Color.fromARGB(255, 75, 40, 40));
final followerstatusTextStyle = TextStyle(fontSize: 16, color: Colors.white70, fontWeight: FontWeight.w600);
final amountdataTextStyle = TextStyle(fontSize: 16, color: Colors.white70, fontWeight: FontWeight.w600);
final activityTextStyle = TextStyle(fontSize: 16, color: Colors.white70, fontWeight: FontWeight.w600);
final buttonTextStyle = TextStyle(fontSize: 14, color: Color(0xffff6700), fontWeight: FontWeight.w600);
final startnoTextStyle = TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.w800);
final caption1TextStyle = TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w800);
final caption2TextStyle = TextStyle(fontSize: 14, color: Colors.white, fontWeight: FontWeight.w400);
final raceTextStyle = TextStyle(fontSize: 14, color: Colors.white, fontWeight: FontWeight.w400);
final targetdurationTextStyle = TextStyle(fontSize: 14, color: Colors.white, fontWeight: FontWeight.w400);
final infoTextStyle = TextStyle(fontSize: 20, color: Colors.white70, fontFamily: 'Courier', fontWeight: FontWeight.w800);
final racetimeStyle = TextStyle(fontSize: 48, color: Color(0xffff6700), fontFamily: 'HelveticaNeue-Thin', fontWeight: FontWeight.w400);
@override
List<DataGridRow> get rows => _rows;
List<DataGridRow> _rows = [];
StopwatchesDataSource(List<Stopwatchitem> data) {
_rows = _buildRows(data);
}
List<DataGridRow> _buildRows(List<Stopwatchitem> data) {
return data.map<DataGridRow>(
(dataGridRow) => DataGridRow(
cells: [
DataGridCell<int>(columnName: 'timingpartid', value: dataGridRow.timingpartid),
DataGridCell<int>(columnName: 'stopwatchstate', value: dataGridRow.stopwatchState),
DataGridCell<String>(columnName: 'startno', value: dataGridRow.startno),
DataGridCell<Map<String, String>>(columnName: 'partdata', value: {
'club': dataGridRow.club,
'crew': dataGridRow.crew,
'race': dataGridRow.race,
'targetduration': dataGridRow.targetDurationString,
}),
DataGridCell<String>(columnName: 'racetime', value: dataGridRow.raceTimeString),
]))
.toList();
}
/// ✅ ONLY update time column
void updateData(List<Stopwatchitem> newData) {
for (int i = 0; i < newData.length; i++) {
_rows[i].getCells()[4] = DataGridCell<String>(
columnName: 'racetime',
value: newData[i].raceTimeString,
);
}
notifyListeners(); // ✅ partial refresh
}
@override
DataGridRowAdapter? buildRow(DataGridRow row) {
Color getRowBackgroundColor() {
final int index = effectiveRows.indexOf(row);
if (index % 2 != 0) {
return const Color.fromARGB(255, 0, 47, 108);
}
return const Color(0xff001e46);
}
// Radius only at full left and full right
BorderRadius getBorderRadius(String columnName) {
if (columnName == 'startno') {
return const BorderRadius.only(
topLeft: Radius.circular(15),
bottomLeft: Radius.circular(15),
);
}
if (columnName == 'racetime') {
return const BorderRadius.only(
topRight: Radius.circular(15),
bottomRight: Radius.circular(15),
);
}
return BorderRadius.zero;
}
return DataGridRowAdapter(
cells: row.getCells().map<Widget>((cellData) {
switch (cellData.columnName) {
case 'startno':
return DecoratedBox(
decoration : BoxDecoration(
borderRadius: getBorderRadius(cellData.columnName),
color: getRowBackgroundColor(),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CircleAvatar(radius: 30, backgroundColor: Colors.grey, child: Text(cellData.value, style: startnoTextStyle,), ),
],
),
);
case 'partdata':
final Map<String, String> values = cellData.value as Map<String, String>;
String caption1 = '';
String caption2 = '';
if (values['club'] != '') {
caption1 = values['club']!;
caption2 = values['crew']!;
} else {
caption1 = values['crew']!;
caption2 = '';
}
return DecoratedBox(
decoration : BoxDecoration(
borderRadius: getBorderRadius(cellData.columnName),
color: getRowBackgroundColor(),
),
child: Padding(
padding: const EdgeInsets.only(top: 12, bottom: 16, left: 4, right: 1),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Row(children: [
Expanded(child: Text(caption1, style: caption1TextStyle, overflow: TextOverflow.ellipsis)),
],),
if (caption2 != '') Row(children: [
Expanded(child: Text(caption2, style: caption2TextStyle, overflow: TextOverflow.ellipsis)),
],),
Row(children: [
Text(values['race']!, style: raceTextStyle),
Text(values['targetduration']!, style: targetdurationTextStyle),
],),
],
),
),
);
case 'racetime':
return DecoratedBox(
decoration : BoxDecoration(
borderRadius: getBorderRadius(cellData.columnName),
color: getRowBackgroundColor(),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(cellData.value, style: racetimeStyle),
],
),
);
default:
return Text('-');
}
}).toList());
}
}
Unfortunately swiping still does not work.
Is there any way to get this working or is swiping not available with this refresh rate?