mardi 8 décembre 2020

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:

  • Changing the checkbox bool value directly from the parent within a setState call.
  • Calling a function inside each child widget to change the checkbox value from within the child itself.

enter image description here

The parent widget code listing the child widgets:

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;

  @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>();
    var boxes = boxDatas.map((boxData) => buildTimelogBox(boxData)).toList();
    timelogBoxes = boxes;
    widgets.addAll(boxes);
    return widgets;
  }

  LockTimelogBox buildTimelogBox(TimelogBoxData boxData) {
    var box = new LockTimelogBox(data: boxData, giveSelectFunction: addSelectFunction, giveDeselectFunction: addDeselectFunction);
    return box;
  }

  void initValues() {
    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(() {
      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() {
    setSelectAllOption(!selectAllOptionActive);
    if (selectAllOptionActive)
      selectAll();
    else
      deselectAll();
  }

  setSelectAllOption(bool selectAllActive) {
    setState(() {
      selectAllOptionActive = selectAllActive;
    });
  }

  void selectAll() {
    print(selectFunctions);
    // Call functions passed up from children for changing their Checkbox bool value.
    selectFunctions.forEach((func) {
      func();
    });
    // First approach:
    // timelogBoxes.forEach((box) {
// First approach:
    // timelogBoxes.forEach((box) {
    //  setState(() {
    //      box.data.isChecked = true;
    //  });
    // });
  }

  void deselectAll() {
    print(selectFunctions);
    // Call functions passed up from children for changing their Checkbox bool value.
    deselectFunctions.forEach((func) {
      func();
    });
    // First approach:
    // timelogBoxes.forEach((box) {
    // First approach:
    // timelogBoxes.forEach((box) {
    //  setState(() {
    //      box.data.isChecked = false;
    //  });
    // });
  }

  addSelectFunction(Function f) {
    selectFunctions.add(f);
  }

  addDeselectFunction(Function f) {
    deselectFunctions.add(f);
  }
}

The child widget code containing the Checkbox:

class LockTimelogBox extends StatefulWidget {
  final TimelogBoxData data;
  final Function giveSelectFunction;
  final Function giveDeselectFunction;

  LockTimelogBox({this.data, this.giveSelectFunction, this.giveDeselectFunction});

  @override
  TimeLogBoxState createState() => TimeLogBoxState();
}

class TimeLogBoxState extends State<LockTimelogBox> {
  double displayWidth = 1;
  double displayHeight = 1;
  double boxRowFontsizeFactor;

  @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: widget.data.isChecked,
            onChanged: onCheckChange,
          ),
        ),
      ),
    );
  }

  void onCheckChange(bool isChecked) {
    setIsChecked(isChecked);
  }

  setIsChecked(bool isChecked) {
    setState(() {
      widget.data.isChecked = isChecked;
    });
  }

  void onBoxTap() {
    setIsChecked(!widget.data.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;
  }

  setDisplayDimensions() {
    if (displayWidth == 1 || displayWidth == null) displayWidth = DisplayHelper.getDisplayWidth(context);
    if (displayHeight == 1 || displayHeight == null) displayHeight = DisplayHelper.getDisplayHeight(context);
  }

  select() {
    onCheckChange(true);
    // setState(() {
    //   widget.data.isChecked = true;
    // });
  }

  deselect() {
    onCheckChange(false);
    // setState(() {
    //   widget.data.isChecked = false;
    // });
  }
}

Thanks!




Aucun commentaire:

Enregistrer un commentaire