I am a junior developer and I'm trying to learn flutter while doing a project, I'm using the Tables and sometime encountering problems, a problem i had is to show always the scrollbar also if not at the end of the table but for that i found a solution fortunately, but now I'm using a double table model to make the first column stay fixed when i scroll horizontally but the problem I'm encountering is that with this solution I can't make the horizontal scrollbar be always visible also if in the middle of the table.
Here the body of the screen I'm creating with this problem:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class Body extends StatefulWidget {
final int getColumnWidth;
Body({super.key, required this.getColumnWidth});
@override
State<Body> createState() => _BodyState();
}
class _BodyState extends State<Body> {
Timer? _incrementTimer;
Timer? _decrementTimer;
final List<String> rowTexts = [
"Manomissione",
"Batteria Scarica",
"Assenza rete",
"Rapina",
"Coercizione",
"Avaria generale (fusibili, batteria sensori radio)",
"Ritorno rete",
"Anomalia GSM"
];
final List<String> columnTexts = [
"Evento fonia",
"Evento SMS",
"Attiva sirene",
"Attiva bip",
"Ritardo segnalazione"
];
final Map<String, Map<String, bool>> checkboxControllers = {
"1-1": {"value": true, "visible": true},
"1-2": {"value": true, "visible": true},
"1-3": {"value": true, "visible": true},
"1-4": {"value": false, "visible": false},
"2-1": {"value": true, "visible": true},
"2-2": {"value": true, "visible": true},
"2-3": {"value": false, "visible": false},
"2-4": {"value": true, "visible": true},
"3-1": {"value": true, "visible": true},
"3-2": {"value": true, "visible": true},
"3-3": {"value": false, "visible": false},
"3-4": {"value": true, "visible": true},
"4-1": {"value": true, "visible": true},
"4-2": {"value": true, "visible": true},
"4-3": {"value": false, "visible": false},
"4-4": {"value": false, "visible": false},
"5-1": {"value": true, "visible": true},
"5-2": {"value": true, "visible": true},
"5-3": {"value": false, "visible": false},
"5-4": {"value": false, "visible": false},
"6-1": {"value": true, "visible": true},
"6-2": {"value": true, "visible": true},
"6-3": {"value": false, "visible": false},
"6-4": {"value": false, "visible": false},
"7-1": {"value": true, "visible": true},
"7-2": {"value": true, "visible": true},
"7-3": {"value": false, "visible": false},
"7-4": {"value": false, "visible": false},
"8-1": {"value": true, "visible": true},
"8-2": {"value": false, "visible": false},
"8-3": {"value": false, "visible": false},
"8-4": {"value": true, "visible": true},
// Add more entries as needed
};
@override
void dispose() {
_incrementTimer?.cancel();
_decrementTimer?.cancel();
super.dispose();
}
void _startIncrementTimer(TextEditingController textController, int tipo) {
// Start a timer that increments the value every 100 milliseconds
_incrementTimer = Timer.periodic(Duration(milliseconds: 100), (_) {
int currentValue = int.parse(textController.text);
setState(() {
switch (tipo) {
case 1:
currentValue++;
textController.text =
(currentValue < 255 ? currentValue : 255).toString();
break;
}
});
});
}
void _startDecrementTimer(TextEditingController textController, int tipo) {
// Start a timer that decrements the value every 100 milliseconds
_decrementTimer = Timer.periodic(Duration(milliseconds: 100), (_) {
int currentValue = int.parse(textController.text);
setState(() {
switch (tipo) {
case 1:
currentValue--;
textController.text =
(currentValue > 0 ? currentValue : 0).toString();
break;
}
});
});
}
void _stopTimer() {
// Cancel both increment and decrement timers
_incrementTimer?.cancel();
_decrementTimer?.cancel();
}
final Map<String, TextEditingController> ritardiControllers = {
"3-5": TextEditingController(text: "20"),
"4-5": TextEditingController(text: "20"),
"5-5": TextEditingController(text: "20"),
"7-5": TextEditingController(text: "20"),
"8-5": TextEditingController(text: "10"),
// Aggiungi altri label text e controller di testo
};
ScrollController _vertical = new ScrollController(),
_horizontal = new ScrollController();
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scrollbar(
controller: _vertical,
thumbVisibility: true,
trackVisibility: true,
child: SingleChildScrollView(
controller: _vertical,
padding: EdgeInsets.all(1.0),
child: SizedBox(
width: MediaQuery.of(context).size.width -
(48 + 17 + widget.getColumnWidth) <
750
? 750
: MediaQuery.of(context).size.width -
(48 + 17 + widget.getColumnWidth),
child: Row(
children: [
Table(
columnWidths: {
0: FixedColumnWidth(
320), // Adjust the width for the first column
},
border: TableBorder.all(),
children: List.generate(
9,
(rowIndex) => TableRow(
children: List.generate(
1,
(colIndex) => TableCell(
child: SizedBox(
height: 53,
child: Container(
color: Colors.grey.shade600,
child: Align(
alignment: Alignment.centerLeft,
child: buildCellElement(
rowIndex,
colIndex,
),
),
),
),
),
),
),
),
),
Expanded(
child: Scrollbar(
controller: _horizontal,
thumbVisibility: true,
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(
vertical: 4,
),
controller: _horizontal,
scrollDirection: Axis.horizontal,
child: Table(
columnWidths: {
0: IntrinsicColumnWidth(),
1: IntrinsicColumnWidth(),
2: IntrinsicColumnWidth(),
3: IntrinsicColumnWidth(),
4: IntrinsicColumnWidth(),
5: IntrinsicColumnWidth(),
},
border: TableBorder.all(),
children: List.generate(
9,
(rowIndex) => TableRow(
children: List.generate(
5,
(colIndex) => TableCell(
child: SizedBox(
height: 53,
child: Container(
color: rowIndex == 0
? Colors.grey.shade600
: null,
width:
colIndex == 4 && rowIndex != 0 ? 150 : 80,
child: buildCellElement(
rowIndex,
colIndex + 1,
),
),
),
),
),
),
),
),
),
),
),
],
),
),
),
);
}
Widget buildCellElement(int rowIndex, int colIndex) {
final cellLabel = "$rowIndex-$colIndex";
if (colIndex != 0 && rowIndex != 0 && colIndex != 5) {
final checkboxData = checkboxControllers[cellLabel];
if (checkboxData != null) {
return Visibility(
visible: checkboxData["visible"] ?? false,
child: Align(
alignment: Alignment.center,
child: Transform.scale(
scale: 0.8,
child: Checkbox(
value: checkboxData["value"] ?? false,
onChanged: (bool? value) {
setState(() {
checkboxData["value"] = value ?? false;
});
// Handle checkbox state changes
// You can use checkboxData["value"] and checkboxData["visible"] here
// to update the state as needed.
},
),
),
),
);
} else {
return Container();
}
} else {
if (colIndex == 0 && rowIndex == 0) {
return Container();
} else if (colIndex == 0) {
return Text(
rowTexts[rowIndex - 1],
style: TextStyle(fontSize: 12),
);
} else if (rowIndex == 0) {
return Center(
child: Text(
columnTexts[colIndex - 1],
style: TextStyle(fontSize: 12),
));
} else if (colIndex == 5) {
final ritardoData = ritardiControllers[cellLabel];
if (ritardoData != null) {
return Row(
children: [
Text(
rowIndex + colIndex == 8 || rowIndex + colIndex == 13
? "minuti"
: "secondi",
style: TextStyle(fontSize: 12),
),
Spacer(),
Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.black), // Set the color of the border
borderRadius: BorderRadius.circular(8),
),
height: 28,
width: 52,
child: TextFormField(
style: TextStyle(fontSize: 12),
maxLength: 3,
textAlign: TextAlign.right,
decoration: InputDecoration(
counterText: "",
contentPadding: EdgeInsets.all(8.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
),
),
controller: ritardoData,
keyboardType: TextInputType.numberWithOptions(
decimal: false,
signed: true,
),
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
],
onEditingComplete: () {
if (int.parse(ritardoData.text) > 255) {
setState(() {
ritardoData.text = "255";
});
} else if (int.parse(ritardoData.text) < 0) {
setState(() {
ritardoData.text = "0";
});
}
},
),
),
numberArrowsContainer(ritardoData, 1),
],
);
} else {
return Container();
}
}
return Text(
cellLabel,
style: TextStyle(fontSize: 12),
);
}
}
Column numberArrowsContainer(TextEditingController textController, int tipo) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GestureDetector(
onTapDown: (_) {
// When the button is pressed, start incrementing
_startIncrementTimer(textController, tipo);
},
onTapUp: (_) {
// When the button is released, stop incrementing
_stopTimer();
},
onTap: () {
// When the button is tapped (not held), increment once
int currentValue = int.parse(textController.text);
setState(() {
switch (tipo) {
case 1:
currentValue++;
textController.text =
(currentValue < 255 ? currentValue : 255).toString();
break;
}
});
},
child: Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 0.5,
),
),
),
child: Icon(
Icons.arrow_drop_up,
size: 18.0,
),
),
),
GestureDetector(
onTapDown: (_) {
// When the button is pressed, start decrementing
_startDecrementTimer(textController, tipo);
},
onTapUp: (_) {
// When the button is released, stop decrementing
_stopTimer();
},
onTap: () {
// When the button is tapped (not held), decrement once
int currentValue = int.parse(textController.text);
setState(() {
switch (tipo) {
case 1:
currentValue--;
textController.text =
(currentValue > 0 ? currentValue : 0).toString();
break;
}
});
},
child: InkWell(
child: Icon(
Icons.arrow_drop_down,
size: 18.0,
),
),
),
],
);
}
}
Here's the solution I found for the first case, so only table in the screen:
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
class Body extends StatefulWidget {
final int getColumnWidth;
Body({super.key, required this.getColumnWidth});
@override
State<Body> createState() => _BodyState();
}
class _BodyState extends State<Body> {
Timer? _incrementTimer;
Timer? _decrementTimer;
final List<String> columnTexts = [
"n zona",
"Collocazione",
"Zona software",
"Descrizione zona",
"Allarme",
"Ripristino",
"Manomissione",
"Ripristino"
];
final Map<String, bool> checkboxControllers = {
"1-4": false,
"1-5": false,
"1-6": false,
"1-7": false,
"2-5": false,
"2-6": false,
"2-7": false,
"2-4": false,
"3-5": false,
"3-6": false,
"3-7": false,
"3-4": false,
"4-5": false,
"4-6": false,
"4-7": false,
"4-4": false,
"5-5": false,
"5-6": false,
"5-7": false,
"5-4": false,
};
@override
void dispose() {
_incrementTimer?.cancel();
_decrementTimer?.cancel();
super.dispose();
}
void _startIncrementTimer(TextEditingController textController, int tipo) {
// Start a timer that increments the value every 100 milliseconds
_incrementTimer = Timer.periodic(Duration(milliseconds: 100), (_) {
int currentValue = int.parse(textController.text);
setState(() {
switch (tipo) {
case 1:
currentValue++;
textController.text =
(currentValue < 255 ? currentValue : 255).toString();
break;
}
});
});
}
void _startDecrementTimer(TextEditingController textController, int tipo) {
// Start a timer that decrements the value every 100 milliseconds
_decrementTimer = Timer.periodic(Duration(milliseconds: 100), (_) {
int currentValue = int.parse(textController.text);
setState(() {
switch (tipo) {
case 1:
currentValue--;
textController.text =
(currentValue > 0 ? currentValue : 0).toString();
break;
}
});
});
}
void _stopTimer() {
// Cancel both increment and decrement timers
_incrementTimer?.cancel();
_decrementTimer?.cancel();
}
ScrollController _vertical = new ScrollController(),
_horizontal = new ScrollController();
@override
Widget build(BuildContext context) {
// TODO: implement build
return Column(
children: [
Card(
elevation: 5,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("TRASMISSIONE DIGITALE - REPORT ZONE"),
],
),
),
),
Expanded(
child: Scrollbar(
controller: _horizontal,
thumbVisibility: true,
child: Scrollbar(
controller: _vertical,
thumbVisibility: true,
notificationPredicate: (notif) => notif.depth == 1,
child: SingleChildScrollView(
controller: _horizontal,
scrollDirection: Axis.horizontal,
padding: EdgeInsets.all(1.0),
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(
vertical: 4,
),
controller: _vertical,
child: SizedBox(
width: MediaQuery.of(context).size.width -
(48 + 17 + widget.getColumnWidth) <
750
? 750
: MediaQuery.of(context).size.width -
(48 + 17 + widget.getColumnWidth),
child: Table(
columnWidths: {
0: FixedColumnWidth(60),
1: FixedColumnWidth(160),
2: FixedColumnWidth(90),
3: FlexColumnWidth(),
4: FlexColumnWidth(),
5: FlexColumnWidth(),
6: FlexColumnWidth(),
7: FlexColumnWidth(),
},
border: TableBorder.all(),
children: List.generate(
5,
(rowIndex) => TableRow(
children: List.generate(
8,
(colIndex) => TableCell(
child: SizedBox(
height: 53,
child: Container(
color: rowIndex == 0 || colIndex == 0
? Colors.grey.shade600
: null,
child: buildCellElement(
rowIndex,
colIndex,
),
),
),
),
),
),
),
),
),
),
),
),
),
),
],
);
}
Widget buildCellElement(int rowIndex, int colIndex) {
final cellLabel = "$rowIndex-$colIndex";
if (colIndex >= 4 && rowIndex >= 1) {
final checkboxData = checkboxControllers[cellLabel];
if (checkboxData != null) {
return Row(
children: [
Align(
alignment: Alignment.center,
child: Transform.scale(
scale: 0.8,
child: Checkbox(
value: checkboxData,
onChanged: (bool? value) {
setState(() {
checkboxControllers[cellLabel] = value ?? false;
});
// Handle checkbox state changes
// You can use checkboxData["value"] and checkboxData["visible"] here
// to update the state as needed.
},
),
),
),
Text(checkboxData ? "Si" : "No"),
],
);
} else {
return Container();
}
} else {
if (rowIndex == 0) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Text(
columnTexts[colIndex],
style: TextStyle(fontSize: 12),
)),
);
} else if (colIndex == 0) {
return Align(
alignment: Alignment.centerRight,
child: Text(
"${rowIndex.toString()} ",
style: TextStyle(fontSize: 12),
),
);
} else if (colIndex == 1) {
return Align(
alignment: Alignment.centerLeft,
child: Text("Collocazione di prova"),
);
} else if (colIndex == 2) {
return Center(
child: Text(
(rowIndex + Random().nextInt(56)).toString(),
));
} else if (colIndex == 3) {
return Center(
child: Text(
("dddddddddddddddd"),
));
}
return Text(
cellLabel,
style: TextStyle(fontSize: 12),
);
}
}
}
EDIT: SOLVED
I used the Stack widget to create the page I was expecting, it is really useful in this situations, thanks for the help.