EggBender
EggBender

Reputation: 1020

How to check/uncheck a Checkbox inside a stateful child WIdget in Flutter?

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:

enter image description here

Update

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

Answers (1)

sameer kashyap
sameer kashyap

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

Related Questions