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)
],
);
}
}
CodePudding user response:
You have defined isSelected inside the build method. The build method is invoked each time you set the state. Please move it to initState.
bool checked= false;
Define bool checked = false outside the build method or in initstate
CodePudding user response:
Putting variables inside build
method will reset the bool and always be false. Also on your snippet you are just holding text without check state. On evey-build it will also reset on this point.
You can remove StatefulBuilder
while this is already a StatefulWidget
. Try using state-management like riverpod or bloc instead of using global variables in project cases.
As for the solution, I am creating a model class.
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,
);
}
}
Now the list will be var toDoList = [Task(text: "")];
And your ToDos
class ToDos extends StatefulWidget {
final List<Task> 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) {
//todo: remove [StatefulBuilder]
return StatefulBuilder(builder: (BuildContext context, setStateSB) {
return Container(
child: Padding(
padding: EdgeInsets.all(8.0),
child: ToDoColumn(setStateSB),
),
);
});
}
//current
Column ToDoColumn(
StateSetter setStateSB,
) {
return Column(children: [
for (int i = 0; i < widget.list.length; i )
CheckboxListTile(
value: widget.list[i].isChecked,
onChanged: (_value_1) {
widget.list[i] = widget.list[i].copyWith(isChecked: _value_1);
setState(() {});
setStateSB(() {});
},
title: Text('${widget.list[i].isChecked}'),
activeColor: Colors.green,
checkColor: Colors.black,
)
]);
}
}
More about StatefulWidget
and cookbook on flutter
CodePudding user response:
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)