Reputation: 1020
I ahve a parent Widget called LockTimelogsList
, which contains a ListView
of LockTimelogBox
. Each child widget/LockTimelogBox
contains a Checkbox
I need to update from the parent LockTimelogsList
. When the user presses "Select all" or "Deselect all", LockTimelogsList
needs to iterate though the LockTimelogBoxes
and set the checkbox inside each to true/false.
The trouble I'm having is that iterating the listed child widgets and changing the and updating their checkbox bool value, the checkbox does not update it's view, even after calling setState
.
I've tried two approaches:
Now the code works. It updates all the checkboxes correctly. Take a look at the accepted answer in case anyone reading this does the same mistakes I did. I updated the code to the working now. You can see the working code below.
[![enter image description here][2]][2]
Current parent (list) code:
class LockTimelogsList extends StatefulWidget {
LockTimelogsList({Key key}) : super(key: key);
@override
_LockTimelogsListState createState() => _LockTimelogsListState();
}
class _LockTimelogsListState extends State<LockTimelogsList> {
List<TimeLogModel> unlockedLogs;
List<ProjectModel> projects;
List<WorkOrderModel> workOrders;
List<TaskModel> tasks;
ProjectHttpService projectHttpService;
WorkOrderHttpService workOrderHttpService;
TaskHttpService taskHttpService;
TimeLogHttpService timeLogHttpService;
double displayHeight;
double displayWidth;
bool selectAllOptionActive;
List<LockTimelogBox> timelogBoxes;
List<Function> selectFunctions;
List<Function> deselectFunctions;
List<TimelogBoxData> boxDatas;
List<bool> checkboxes;
@override
void initState() {
initServices();
initValues();
loadInitData();
super.initState();
}
@override
Widget build(BuildContext context) {
setDisplayDimensions();
return SafeArea(
child: Scaffold(
backgroundColor: Colors.white,
appBar: buildAppBar(),
body: buildBody(),
));
}
buildAppBar() {
return AppBar(
backgroundColor: themeConfig.appBarBg,
title: Row(
children: [Icon(Icons.lock), Text(" Lås timmar")],
),
);
}
buildBody() {
return Stack(
children: [buildControlsLayer(), buildLoadDialogLayer()],
);
}
buildControlsLayer() {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [buildTopSelectBar(), buildTimelogList()],
);
}
buildLoadDialogLayer() {
return Container();
}
buildTimelogList() {
return Expanded(
flex: 100,
child: Scrollbar(
child: ListView(
children: buildTimelogBoxes(),
)));
}
buildTimelogBoxes() {
var boxDatas = TimelogBoxDataHelper.createList(unlockedLogs, workOrders, tasks, projects);
var widgets = new List<Widget>();
timelogBoxes.clear();
for (var i = 0; i < boxDatas.length; i++) {
var box = buildTimelogBox(boxDatas[i], checkboxes[i], i);
widgets.add(box);
timelogBoxes.add(box);
}
this.boxDatas = boxDatas;
return widgets;
}
LockTimelogBox buildTimelogBox(TimelogBoxData boxData, bool isChecked, int listIndex) {
var box = new LockTimelogBox(
data: boxData,
giveSelectFunction: addSelectFunction,
giveDeselectFunction: addDeselectFunction,
isChecked: isChecked,
checkChangeCallback: checkChangeCallback,
listIndex: listIndex,
);
return box;
}
void initValues() {
checkboxes = [];
boxDatas = [];
displayHeight = 1;
displayWidth = 1;
unlockedLogs = [];
projects = [];
workOrders = [];
tasks = [];
selectAllOptionActive = true;
timelogBoxes = [];
selectFunctions = [];
deselectFunctions = [];
}
Future loadInitData() async {
await loadTimelogs();
if (unlockedLogs.length > 0) await loadProjectData();
}
void initServices() {
projectHttpService = new ProjectHttpService();
workOrderHttpService = new WorkOrderHttpService();
taskHttpService = new TaskHttpService();
timeLogHttpService = new TimeLogHttpService();
}
void onLoadTimelogsError() {
MessageDialogHelper.show("Fel vid laddning av tidsloggar", "Fel uppstod vid laddning av tidsloggar.", context);
}
void onLoadTimelogsSuccess(Response response) {
var timelogs = TimelogHelper.getFromJson(response.body);
setTimelogs(timelogs);
}
setTimelogs(List<TimeLogModel> timelogs) {
setState(() {
// temp
timelogs.forEach((log) {
checkboxes.add(false);
});
// -
unlockedLogs = timelogs;
});
}
Future loadTimelogs() async {
try {
var response = await timeLogHttpService.getUnlockedLogs(globals.userId);
if (HttpHelper.isSuccess(response))
onLoadTimelogsSuccess(response);
else
onLoadTimelogsError();
} catch (e) {
onLoadTimelogsError();
}
}
Future loadProjectData() async {
var futures = new List<Future>();
futures.add(loadProjects(unlockedLogs));
futures.add(loadWorkOrders(unlockedLogs));
futures.add(loadTasks(unlockedLogs));
var results = await Future.wait(futures);
var projects = getFromResults<ProjectModel>(results);
var workOrders = getFromResults<WorkOrderModel>(results);
var tasks = getFromResults<TaskModel>(results);
setProjects(projects);
setWorkOrders(workOrders);
setTasks(tasks);
}
Future<List<ProjectModel>> loadProjects(List<TimeLogModel> timelogs) async {
var futures = new List<Future<ProjectModel>>();
var projectIds = TimelogHelper.getProjectIds(timelogs);
projectIds.forEach((id) {
futures.add(loadProject(id));
});
var projects = await Future.wait(futures);
return projects;
}
Future<ProjectModel> loadProject(String id) async {
try {
var response = projectHttpService.getProject(id);
return response.then<ProjectModel>((resp) {
if (HttpHelper.isSuccess(resp))
return ProjectHelper.getSingleFromJson(resp.body, true);
else
return null;
}).catchError((err) {
return null;
});
} catch (e) {
return null;
}
}
Future<List<WorkOrderModel>> loadWorkOrders(List<TimeLogModel> timelogs) async {
var futures = new List<Future<WorkOrderModel>>();
var workOrderIds = WorkOrderHelper.getWorkOrderIds(timelogs);
workOrderIds.forEach((id) {
futures.add(loadWorkOrder(id));
});
var workOrders = await Future.wait(futures);
return workOrders;
}
Future<WorkOrderModel> loadWorkOrder(String id) async {
try {
var response = workOrderHttpService.get(id);
return response.then<WorkOrderModel>((resp) {
if (HttpHelper.isSuccess(resp)) {
var list = WorkOrderHelper.getSingleFromJson(resp.body, true);
return list;
} else
return null;
}).catchError((err) {
return null;
});
} catch (e) {
return null;
}
}
Future<List<TaskModel>> loadTasks(List<TimeLogModel> timelogs) async {
var futures = new List<Future<TaskModel>>();
var taskIds = TaskHelper.getTaskIds(timelogs);
taskIds.forEach((id) {
futures.add(loadTask(id));
});
var tasks = await Future.wait(futures);
return tasks;
}
Future<TaskModel> loadTask(String id) async {
try {
var response = taskHttpService.getById(id);
return response.then<TaskModel>((resp) {
if (HttpHelper.isSuccess(resp)) {
var list = TaskHelper.getSingleFromJson(resp.body, true);
return list;
} else
return null;
}).catchError((err) {
return null;
});
} catch (e) {
return null;
}
}
List<T> getFromResults<T>(List<dynamic> results) {
List<T> result;
results.forEach((res) {
if (res is List<T>) result = res;
});
return result;
}
setProjects(List<ProjectModel> projects) {
setState(() {
this.projects = projects;
});
}
setWorkOrders(List<WorkOrderModel> workOrders) {
setState(() {
this.workOrders = workOrders;
});
}
setTasks(List<TaskModel> tasks) {
setState(() {
this.tasks = tasks;
});
}
buildTopSelectBar() {
return Expanded(
flex: 8,
child: Container(
decoration: BoxDecoration(
border: Border(bottom: BorderSide(width: displayHeight * 0.0005, color: Color(0x77FFFFFF))),
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFF13313b), Color(0xFF11131a)]),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: [buildSelectAllButton(), buildSelectCount()],
),
));
}
setDisplayDimensions() {
if (displayWidth == 1 || displayWidth == null) displayWidth = DisplayHelper.getDisplayWidth(context);
if (displayHeight == 1 || displayHeight == null) displayHeight = DisplayHelper.getDisplayHeight(context);
}
buildSelectAllButton() {
return Expanded(
flex: 100,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onSelectAllTap,
child: Container(
padding: EdgeInsets.only(left: displayWidth * 0.03),
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
child: Icon(
selectAllOptionActive ? Icons.check_box : Icons.cancel,
color: Colors.white,
size: displayWidth * 0.05,
),
),
Container(
margin: EdgeInsets.only(left: displayWidth * 0.015),
child: Text(
selectAllOptionActive ? "Markera alla" : "Avmarkera alla",
style: TextStyle(fontSize: displayWidth * 0.05, color: Colors.white),
),
)
],
),
),
),
));
}
buildSelectCount() {
return Expanded(
flex: 100,
child: Material(
color: Colors.transparent,
child: InkWell(
child: Container(
alignment: Alignment.center,
child: Text(
"",
style: TextStyle(fontSize: displayWidth * 0.05, color: Colors.white),
),
),
),
));
}
void onSelectAllTap() {
setState(() {
if (selectAllOptionActive)
selectAll();
else
deselectAll();
setSelectAllOption(!selectAllOptionActive);
});
}
setSelectAllOption(bool selectAllActive) {
setState(() {
selectAllOptionActive = selectAllActive;
});
}
void selectAll() {
print(selectFunctions);
print(timelogBoxes);
print(unlockedLogs);
for (var i = 0; i < checkboxes.length; i++) checkboxes[i] = true;
selectFunctions.forEach((func) {
func();
});
}
void deselectAll() {
for (var i = 0; i < checkboxes.length; i++) checkboxes[i] = false;
deselectFunctions.forEach((func) {
func();
});
}
addSelectFunction(Function f) {
selectFunctions.add(f);
}
addDeselectFunction(Function f) {
deselectFunctions.add(f);
}
checkChangeCallback(int listIndex, bool isChecked) {
setState(() {
checkboxes[listIndex] = isChecked;
selectAllOptionActive = !checkboxes.any((b) => b);
});
}
}
Current child (box) code:
class LockTimelogBox extends StatefulWidget {
final TimelogBoxData data;
final Function giveSelectFunction;
final Function giveDeselectFunction;
final bool isChecked;
final Function checkChangeCallback;
final int listIndex;
LockTimelogBox({this.data, this.giveSelectFunction, this.giveDeselectFunction, this.isChecked, this.checkChangeCallback, this.listIndex});
@override
TimeLogBoxState createState() => TimeLogBoxState();
}
class TimeLogBoxState extends State<LockTimelogBox> {
double displayWidth = 1;
double displayHeight = 1;
double boxRowFontsizeFactor;
bool isChecked;
@override
void initState() {
initValues();
widget.giveSelectFunction(select);
widget.giveDeselectFunction(deselect);
super.initState();
}
@override
Widget build(BuildContext context) {
setDisplayDimensions();
return Container(
height: displayHeight * 0.2,
width: displayWidth,
decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFF13313b), Color(0xFF11131a)]),
border: Border(bottom: BorderSide(width: displayHeight * 0.0005, color: Color(0x77FFFFFF)))),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onBoxTap,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: [buildCheckBoxContainer(), buildInfoContainer()],
),
),
),
);
}
buildCheckBoxContainer() {
return Expanded(
flex: 10,
child: Container(
alignment: Alignment.center,
child: buildCheckbox(),
));
}
buildInfoContainer() {
return Expanded(
flex: 70,
child: Container(
padding: EdgeInsets.only(top: displayHeight * 0.005, bottom: displayHeight * 0.005, left: displayWidth * 0.01, right: displayWidth * 0.01),
child: Column(
children: [buildDateRow(), buildProjectRow(), buildWorkOrderRow(), buildTaskRow()],
),
));
}
buildCheckbox() {
return Container(
child: Theme(
data: ThemeData(primarySwatch: Colors.green, unselectedWidgetColor: Colors.grey),
child: Transform.scale(
scale: 1.5,
child: Checkbox(
value: isChecked,
onChanged: onCheckChange,
),
),
),
);
}
void onCheckChange(bool isChecked) {
widget.checkChangeCallback(widget.listIndex, isChecked);
setIsChecked(isChecked);
}
setIsChecked(bool isChecked) {
if (this.mounted) {
setState(() {
this.isChecked = isChecked;
});
} else
this.isChecked = isChecked;
}
void onBoxTap() {
onCheckChange(!isChecked);
}
buildDateRow() {
return Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: [
Container(
alignment: Alignment.centerLeft,
child: Icon(
Icons.access_time,
color: Colors.white,
),
),
Container(
margin: EdgeInsets.only(left: displayWidth * 0.01),
alignment: Alignment.centerLeft,
child: Text(
DateFormat("yyyy-MM-dd HH:mm").format(widget.data.timeLog.start),
style: TextStyle(fontSize: displayWidth * boxRowFontsizeFactor, color: Colors.white),
),
)
],
));
}
buildProjectRow() {
return Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: [
Container(
alignment: Alignment.centerLeft,
child: Icon(
Icons.work,
color: Colors.white,
),
),
Container(
margin: EdgeInsets.only(left: displayWidth * 0.01),
alignment: Alignment.centerLeft,
child: Text(
widget.data.timeLog.projectName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: displayWidth * boxRowFontsizeFactor, color: Colors.white),
),
)
],
));
}
buildWorkOrderRow() {
return Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: [
Container(
alignment: Alignment.centerLeft,
child: Icon(
Icons.work,
color: Colors.white,
),
),
Container(
margin: EdgeInsets.only(left: displayWidth * 0.01),
alignment: Alignment.centerLeft,
child: Text(
widget.data.workOrder.name,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(fontSize: displayWidth * boxRowFontsizeFactor, color: Colors.white),
),
)
],
));
}
buildTaskRow() {
return Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: [
Container(
alignment: Alignment.centerLeft,
child: Icon(
Icons.playlist_add_check,
color: Colors.white,
),
),
Container(
margin: EdgeInsets.only(left: displayWidth * 0.01),
alignment: Alignment.centerLeft,
child: Text(
widget.data.timeLog.projectName,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(fontSize: displayWidth * boxRowFontsizeFactor, color: Colors.white),
),
)
],
));
}
void initValues() {
displayWidth = 1;
displayHeight = 1;
boxRowFontsizeFactor = 0.05;
isChecked = widget.isChecked;
}
setDisplayDimensions() {
if (displayWidth == 1 || displayWidth == null) displayWidth = DisplayHelper.getDisplayWidth(context);
if (displayHeight == 1 || displayHeight == null) displayHeight = DisplayHelper.getDisplayHeight(context);
}
select() {
setIsChecked(true);
}
deselect() {
setIsChecked(false);
}
}
Upvotes: 0
Views: 2273
Reputation: 1166
What you're doing wrong is mutating something that should be immutable, which is a very bad idea and practice.
The state class is specifically to handle your state change, so what you should instead do is declare a state variable inside the class and since there are multiple checkboxes, initialize a List<bool> checkBoxes
and map them to using the incoming data from the widget.data
or have separate variables for each if they're not much, and then use them in your CheckBox()
class TimeLogBoxState extends State<LockTimelogBox> {
...
bool isChecked = widget.data.isChecked;
buildCheckbox() {
return Container(
child: Theme(
data: ThemeData(primarySwatch: Colors.green, unselectedWidgetColor: Colors.grey),
child: Transform.scale(
scale: 1.5,
child: Checkbox(
value: isChecked,
onChanged:(val){
setState((){ isChecked = val});
},
),
),
),
);
}
Something like that.
Upvotes: 1