Reputation: 474
Background: I have a Todo app I'm trying to make where I can either save my tasks locally (sqflite) or remotely to firebase. Below is a snapshot of the adding a Todo page.
Problem
I recently added a toggle button as a separate widget and takes the selected button (personal/remote) and returns a boolean via a callback function to my AddTodoScreen.
However, when I click remote and add the task it says that the task is being saved locally and not remotely, and that the boolean for isRemote is false.
But when I checked my callback function its able to set my variable to true. It's just that when I reference the variable it hasn't updated despite using setState.
CODE:
callback function
getSaveLocation(bool isRemote) {
setState(() {
remoteTask = isRemote;
print("SET STATE CALLED VALUE OF ISREMOTE: $isRemote");
});
}
Logic for adding the todo
ToggleButton(callbackFunction: getSaveLocation,),
FloatingActionButton.extended(onPressed: ()
{
//creates ToDo item
var todo = Todo(
id: idGenerator(),
title: controllerTask.value.text,
description: controllerDescription.value.text,
isRemote: remoteTask, //THIS SHOULD CHANGE FROM THE CALLBACK
);
print("the value of is remote");
print(todo.isRemote);
//get Todos bloc add new item
context.read<TodosBloc>().add(AddTodo(todo: todo));
Navigator.pop(context);
},
label: const Text('add task',
style: TextStyle(color: Colors.black),
),
FULL CODE: addTodoScreen
class _AddTodoScreen extends State<AddTodoScreen> {
final controllerTask = TextEditingController();
String taskTitle = '';
final controllerDescription = TextEditingController();
String description ='';
bool remoteTask = false; //THIS VARIABLE CHANGES TO TRUE NOT RECOGNIZED
@override
void initState() {
super.initState();
controllerTask.addListener(() => setState(() {}));
controllerDescription.addListener(() => setState(() {}));
}
@override
Widget build(BuildContext context) {
int idGenerator() {
final now = DateTime.now();
return now.microsecondsSinceEpoch;
}
getSaveLocation(bool isRemote) {
setState(() {
remoteTask = isRemote;
print("SET STATE CALLED VALUE OF ISREMOTE: $isRemote");
});
}
return Scaffold(
appBar: AppBar(
title: const Text('BloC Pattern: Add a To Do'),
),
body: BlocListener<TodosBloc, TodosState>(
listener: (context, state) {
// TODO: implement listener
if(state is TodosLoaded) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('added'),
)
);
}
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_inputField('Title', controllerTask),
_inputField('Description', controllerDescription),
//CALLBACK FN CHANGES 'remoteTask' value based on toggle button
ToggleButton(callbackFunction: getSaveLocation,),
FloatingActionButton.extended(
onPressed: () {
//creates ToDo item should take the updated remoteTask value
var todo = Todo(
id: idGenerator(),
title: controllerTask.value.text,
description: controllerDescription.value.text,
isRemote: remoteTask, //WHEN REFERENCED ALWAYS RETURNS FALSE
);
print("the value of is remote");
print(todo.isRemote);
//get Todos bloc add new item
context.read<TodosBloc>().add(AddTodo(todo: todo));
Navigator.pop(context);
},
label: const Text('add task',
style: TextStyle(color: Colors.black),
),
),
],
),
),
),
]
)
),
),
),
);
}
}
CODE: toggle button widget
class ToggleButton extends StatefulWidget {
final Function callbackFunction;
const ToggleButton({Key? key, required this.callbackFunction}) : super(key: key);
@override
ToggleButtonState createState() => ToggleButtonState();
}
class ToggleButtonState extends State<ToggleButton> {
List<bool> isSelected = [true, false];
@override
Widget build(BuildContext context) => Container(
color: Colors.green.withOpacity(.5),
child: ToggleButtons(
fillColor: Colors.lightBlue.shade900,
selectedColor: Colors.white,
isSelected: isSelected,
renderBorder: false,
children: const [
Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: Text('Personal',)
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: Text('Remote',)
),
],
onPressed: (int newIndex){
setState(() {
for(int idx = 0; idx < isSelected.length; idx++ ){
if(idx == newIndex){
isSelected[idx] = true;
}
else{
isSelected[idx] = false;
}
}
//check the first toggle button value
bool isRemote = isSelected[1];
print("Sending this value $isRemote to callback function");
widget.callbackFunction(isSelected[1]);
});
},
),
);
}
I'm fairly certain the problem isn't from the toggle button because I am getting print outs saying the state changes.
Also willing to take suggestions i.e moving the logic into my BloC or handling the update without a callback. Fairly new to MVVM/Bloc architecture pattern so maybe I should be handling this logic elsewhere or with a new Bloc?
EDIT
Todo Bloc Code
class TodosBloc extends Bloc<TodoEvent, TodosState> {
TodoRepository todosRepository;
TodosBloc(this.todosRepository) : super(TodosLoading()) {
on<LoadTodos>(_onLoadTodos);
on<AddTodo>(_onAddTodo);
on<DeleteTodo>(_onDeleteTodo);
on<UpdateTodo>(_onUpdateTodo);
on<EditTodo>(_onEditTodo);
}
Future<void> _onLoadTodos(LoadTodos event, Emitter<TodosState> emit,) async {
List<Todo?> allTodos = await todosRepository.getTodos() as List<Todo?>;
emit(TodosLoaded(todos: allTodos));
}
Future<void> _onAddTodo(
AddTodo event,
Emitter<TodosState> emit,
) async {
final state = this.state;
if (state is TodosLoaded) {
await todosRepository.add(event.todo);
emit(TodosLoaded(todos: List.from(state.todos)..add(event.todo),),
);
}
}
}
Todo Event
abstract class TodoEvent extends Equatable {
const TodoEvent();
@override
List<Object> get props => [];
}
class AddTodo extends TodoEvent {
final Todo todo;
const AddTodo({
required this.todo,
});
@override
List<Object> get props => [todo];
}
TodoState
@immutable
abstract class TodosState {}
class TodosLoading extends TodosState {}
class TodosLoaded extends TodosState {
final List<Todo?> todos;
TodosLoaded({
this.todos = const <Todo?>[],
});
@override
List<Object> get props => [todos];
}
class TodosError extends TodosState {}
Todo repository
class TodoRepository<Todo> extends IRepository {
//final ITodoRepository<Todo> hiveLocalStorage;
final DatabaseHelper localSqlLiteRepository;
final RemoteDataSource firebaseRepository;
TodoRepository({
required this.localSqlLiteRepository,
required this.firebaseRepository,
});
@override
Future add(todo) async {
print("ADDING TODO");
var isRemote = todo.isRemote;
print("VALUE OF ISREMOTE: $isRemote"); //VALUE IS ALWAYS FALSE
// When code reaches this point the value of todo doesn't use the
//updated value
if(todo.isRemote){
try{
await firebaseRepository.add(todo);
}
on Exception catch (e){
print(e);
}
}
else {
await localSqlLiteRepository.add(todo);
}
return;
}
}
REPO
per request the link to the github repo
https://github.com/Silvuurleaf/task_appv2
Upvotes: 1
Views: 1766
Reputation: 381
We have some flaws:
The first: The state being changed is that of the screen and not that of the object, when the 'todo' object is created, a memory address is assigned to it and since it was created inside onPressed, nowhere will it be possible to change it the correct instance of it.
The second: I saw that there is a state with the list of 'todo' objects, so I suppose that in a previous screen there is a list of them or there is the intention of somewhere to list them and this screen that was exposed in the problem is the selection of a 'whole' object. Based on this premise, I suggest that this object (or at least an id that represents it) be passed as a parameter to this screen so that it can be changed.
The third: there is a lot of logic in the presentation layer, this is not necessarily wrong, but it deviates from good practices and makes project maintenance difficult.
Note: I recommend an excellent page that will help you structure your project. https://resocoder.com/ I especially recommend the clean architecture tutorial, even if your target is MVVM it will give you good guidance on how Flutter works.
Note 2: sorry for my english, I'm not native. If with these tips you can't reach the solution, could you create a repository with this code so that I can help you better?
Edit:
I found the problem:
on the file todo_model.dart, you have the code
Todo({
required this.id,
required this.title,
required this.description,
this.isRemote,
this.isCanceled,
this.isCompleted
}) {
isCanceled = isCanceled ?? false;
isCompleted = isCompleted ?? false;
isRemote = isCompleted ?? false;
}
The right is:
Todo({
required this.id,
required this.title,
required this.description,
this.isRemote,
this.isCanceled,
this.isCompleted
}) {
isCanceled = isCanceled ?? false;
isCompleted = isCompleted ?? false;
isRemote = isRemote ?? false;
}
Upvotes: 1
Reputation: 710
To fix this is to move the creation of the Todo object inside the onPressed function of the FloatingActionButton. This will ensure that the latest value of remoteTask is used when creating the Todo.
Code:
FloatingActionButton.extended(
onPressed: () {
var todo = Todo(
id: idGenerator(),
title: controllerTask.value.text,
description: controllerDescription.value.text,
isRemote: remoteTask,
);
context.read<TodosBloc>().add(AddTodo(todo: todo));
Navigator.pop(context);
},
label: const Text(
'add task',
style: TextStyle(color: Colors.black),
),
)
Upvotes: -1
Reputation: 686
your ToggleButton's callbackFunction is look like in below code, I have modified it, Basically if you are passing any values in the callback functions you have to define it's dataType there for properly working
class ToggleButton extends StatefulWidget {
final Function(bool) callbackFunction;
const ToggleButton({Key? key, required this.callbackFunction}) : super(key: key);
@override
ToggleButtonState createState() => ToggleButtonState();
}
class ToggleButtonState extends State<ToggleButton> {
List<bool> isSelected = [true, false];
@override
Widget build(BuildContext context) => Container(
color: Colors.green.withOpacity(.5),
child: ToggleButtons(
fillColor: Colors.lightBlue.shade900,
selectedColor: Colors.white,
isSelected: isSelected,
renderBorder: false,
children: const [
Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: Text('Personal',)
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: Text('Remote',)
),
],
onPressed: (int newIndex){
setState(() {
for(int idx = 0; idx < isSelected.length; idx++ ){
if(idx == newIndex){
isSelected[idx] = true;
}
else{
isSelected[idx] = false;
}
}
//check the first toggle button value
bool isRemote = isSelected[1];
print("Sending this value $isRemote to callback function");
widget.callbackFunction(isSelected[1]);
});
},
),
);
}
Upvotes: 0