Search code examples
flutterdartflutter-state

flutter dynamic Checkboxes don't check


Hi im new to Flutter and coding and tried do build my first to do app. I've created a textformfield to add new todos with a button in a container above. By pressing a button, a new todo will appear on the todo Container. I managed to dynamically give a column new todos with a CheckboxListtTitle. However, when adding a new todo, the todo with a checkbox appears but can't be checked. What did I do wrong here?

to_do_section.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/foundation/key.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_application_1/presentation/widgets/landing_page.dart';
import 'package:flutter_application_1/responsive_layout.dart';
import 'package:flutter/scheduler.dart' show timeDilation;

var toDoList = <String>[userInput];

class ToDos extends StatefulWidget {
  final List<String> list;
  const ToDos({
    Key? key,
    required this.list,
  }) : super(key: key);

  @override
  State<ToDos> createState() => _ToDosState();
}

class _ToDosState extends State<ToDos> {
  @override
  Widget build(BuildContext context) {
    SizeConfig().init(context);
    bool checked= false;
    bool? value_1;
    var isSelected = [false, false];

    return StatefulBuilder(
        builder: (BuildContext context, StateSetter setState) {
      return Padding(
        padding: EdgeInsets.only(
            top: SizeConfig.blockSizeHorizontal * 10,
            left: SizeConfig.blockSizeVertical * 2.5,
            right: SizeConfig.blockSizeVertical * 2.5,
            bottom: SizeConfig.screenHeight / 8),
        child: SizedBox(
          width: SizeConfig.blockSizeHorizontal * 100,
          height: SizeConfig.blockSizeVertical * 40,
          child: Container(
            decoration: BoxDecoration(
                color: Colors.grey[400],
                borderRadius: BorderRadius.circular(30),
                border: Border.all(
                    color: Colors.black45, style: BorderStyle.solid, width: 4)),
            child: Padding(
              padding: EdgeInsets.all(8.0),
              child: ToDoColumn(setState, checked),
            ),
          ),
        ),
      );
    });
  }

  
  Column ToDoColumn(
      StateSetter setState, bool checked) {
        
    return Column(children: [
      for (final value in widget.list)
        CheckboxListTile(
          value: checked,
          onChanged: (_value_1) {
            setState(() {
              checked = _value_1!;
            });
          },
          title: Text('${value}'),
          activeColor: Colors.green,
          checkColor: Colors.black,
        )
    ]);
  }
}

landing_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_application_1/presentation/widgets/to_do_section.dart';

 final _textController = TextEditingController();
    String userInput = "";
class LandingPage extends StatefulWidget {
  const LandingPage({Key? key}) : super(key: key);

  @override
  State<LandingPage> createState() => _LandingPageState();
}

class _LandingPageState extends State<LandingPage> {
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Center(child: Text("To-Do-App")),
        backgroundColor: Colors.redAccent,
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [ToDos(list: toDoList), ToDoAdd()],
        ),
      ),
    );
  }

 

  Column ToDoAdd() {
    return Column(
          children:  [
            Padding(
              padding: EdgeInsets.only(top: 8.0, left: 20, right: 20, bottom: 20),
              child: TextField(
                onChanged: (value) => setState(() => userInput = value) ,
                
                textAlign: TextAlign.center,
                decoration: InputDecoration(
                  border: OutlineInputBorder(),
                  hintText: "Add a new ToDo",
                )  ,
              ),
              
            ),
            MaterialButton(
              color: Colors.redAccent,
                onPressed: () {
                  setState(() {
                  toDoList.add(userInput);
                  
                });
                  
                },
                child: Text("Admit", style: TextStyle(color: Colors.white),),
                ),
                Text(userInput)
          ],
        );
  }


}

Solution

  • Every variable declared inside the build method will be redeclared on each build. You could move its declaration outside and assign a value in the initState method.

    Also, the use of the StatefulBuilder is not justified in your code. You could simply use the StatefulWidget as it is.

    I will take the model class from @yeasin-sheikh

    class Task {
      final String text;
      final bool isChecked;
    
      Task({
        required this.text,
        this.isChecked = false,
      });
    
      Task copyWith({
        String? text,
        bool? isChecked,
      }) {
        return Task(
          text: text ?? this.text,
          isChecked: isChecked ?? this.isChecked,
        );
      }
    }
    

    This example is a reduced one that accomplish what you are trying to do. Please notice that I'm not reproducing your design. The function addRandomToDo() could be replaced with your code for adding Tasks to the app.

    class ToDoPage extends StatefulWidget {
      const ToDoPage({Key? key}) : super(key: key);
    
      @override
      State<ToDoPage> createState() => _ToDoPageState();
    }
    
    class _ToDoPageState extends State<ToDoPage> {
      final tasks = <Task>[];
    
      void addRandomToDo() {
        setState(() {
          tasks.add(Task(text: 'Random ToDo ${tasks.length + 1}'));
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('ToDo App'),
          ),
          body: ListView.builder(
              itemCount: tasks.length,
              itemBuilder: (context, index) {
                return CheckboxListTile(
                  key: ObjectKey(tasks[index]),
                  title: Text(tasks[index].text),
                  value: tasks[index].isChecked,
                  onChanged: (isChecked) {
                    setState(() {
                      tasks[index] = tasks[index].copyWith(isChecked: isChecked);
                    });
                  },
                );
              }),
          floatingActionButton: FloatingActionButton(
            onPressed: addRandomToDo,
            tooltip: 'Add ToDo',
            child: const Icon(Icons.add),
          ),
        );
      }
    }
    

    Tips:

    • The use of Column for this example is not recommended. The best is to use a ListView.builder() and the reason is that when you use a Column and you have more element than the screen can render the app will also render the ones that are offscreen and will result in a lack of performance but the use of ListView will be best because it only renders a few elements offscreen (maybe two or three) to avoid a memory overload and it will load and render the elements as you scroll.

    • Is a best practice to not use functions to return a piece of the screen but to split it into widgets. (Instead of having a ToDoColumn function just change it for a ToDoColumnWidget). The explanation for this is that will incurre in a lack of performance because all functions will be re-rendered on every widget build.

    • This piece of code :

      EdgeInsets.only(top: 8.0, left: 20, right: 20, bottom: 20)

    • Could be changed for this one:

      EdgeInsets.fromLTRB(20, 8, 20, 20)