NoviceCoder
NoviceCoder

Reputation: 474

Flutter Callback function that uses setState to update a variable is not updating when referenced

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.

enter image description here

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

Answers (3)

francisco gomes
francisco gomes

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

Mobin Ansar
Mobin Ansar

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

Tushar Patel
Tushar Patel

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

Related Questions