Функция обратного вызова Flutter, которая использует setState для обновления переменной, не обновляется при ссылке

Предыстория: у меня есть приложение Todo, которое я пытаюсь создать, где я могу сохранять свои задачи локально (sqflite) или удаленно в firebase. Ниже приведен снимок добавления страницы Todo.

Проблема

Недавно я добавил кнопку-переключатель в виде отдельного виджета, беру выбранную кнопку (личную/удаленную) и возвращаю логическое значение через функцию обратного вызова на мой AddTodoScreen.

Однако, когда я нажимаю удаленный и добавляю задачу, он говорит, что задача сохраняется локально, а не удаленно, и что логическое значение для isRemote равно false.

Но когда я проверил свою функцию обратного вызова, она смогла установить для моей переменной значение true. Просто когда я ссылаюсь на переменную, она не обновляется, несмотря на использование setState.

КОД:

функция обратного вызова

getSaveLocation(bool isRemote) {
  setState(() {
    remoteTask = isRemote;
    print("SET STATE CALLED VALUE OF ISREMOTE: $isRemote");
  });
}

Логика добавления задачи

    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),
  ),

ПОЛНЫЙ КОД: 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),
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ]
            )
          ),
        ),
      ),
    );
  }
}

КОД: кнопка переключения виджета

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]);
        });
      },
    ),
  );
}

Я совершенно уверен, что проблема не в кнопке переключения, потому что я получаю распечатки, говорящие об изменении состояния.

Также готов принять предложения, например, перенести логику в мой блок или обработать обновление без обратного вызова. Довольно новый для шаблона архитектуры MVVM/Bloc, поэтому, может быть, мне следует обрабатывать эту логику в другом месте или с новым блоком?

РЕДАКТИРОВАТЬ

Код Todo Block

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),),
      );
    }
  }
}

Тодо Событие

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 {}

Репозиторий задач

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;
  }
}

РЕПО

по запросу ссылка на репозиторий github

https://github.com/Silvuurleaf/task_appv2

Не могли бы вы показать мне код блока? Из того, что я проанализировал, вы меняете состояние экрана, а не объекта, если вы хотите изменить isRemote внутри объекта, эта логика должна быть в блоке

francisco gomes 06.05.2023 02:56

Франсиско прав, я воссоздал ваш код в упрощенной версии и обнаружил, что remoteTask меняется на true и без проблем переходит на другие страницы. Нам нужно увидеть, как ваш context.read<TodosBloc>().add(AddTodo(todo: todo)); работает, потому что с этой частью что-то не так. Вы можете предоставить ToDosBloc?

Texv 06.05.2023 08:10

Я добавил код блока и репозитория, спасибо, что указали, где находится код ошибки. Я также могу сделать репо, если это будет проще.

NoviceCoder 07.05.2023 02:40

Не могу точно сказать, где проблема. У вас возникает та же проблема, если вы передаете bool отдельно? Например, AddTodo(todo: todo, remote: remoteTask); и в вашем классе AddTodo возьмите 2 параметра, Todo todo и bool remote, затем List<Object> get props => [todo, remote];. Это может быть как-то связано с BuildContext блока

Texv 07.05.2023 11:59

Я разместил свой ответ ниже

francisco gomes 07.05.2023 17:29

Я добавил полное репо, если это вообще поможет. В настоящее время я просматриваю ответ Франсисо и буду делать обновления. Спасибо вам всем.

NoviceCoder 08.05.2023 22:01
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
6
163
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

callbackFunction вашего ToggleButton выглядит как в приведенном ниже коде, Я изменил его. В основном, если вы передаете какие-либо значения в функциях обратного вызова, вы должны определить его тип данных для правильной работы.

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]);
            });
          },
        ),
      );
    }

Я проверил, и моя функция обратного вызова уже передает логическое значение обратно, даже не определяя его в кнопке переключения. Я попробовал ваше предложение, но оно не сработало. возвращаемое значение истинно, и я вижу, что оно изменяет состояние моей переменной «remoteTask». Просто когда я ссылаюсь на эту переменную позже в своей плавающей кнопке действия, она не регистрирует изменение состояния.

NoviceCoder 01.05.2023 22:53

Чтобы исправить это, нужно переместить создание объекта Todo внутрь функции onPressed FloatingActionButton. Это обеспечит использование последнего значения remoteTask при создании Todo.

Код:

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),
  ),
)

Мое создание задачи происходит в функции onPressed

NoviceCoder 04.05.2023 00:14

Я также использую todo onpressed

Mobin Ansar 04.05.2023 05:57

Я хочу сказать, что ваше предложение - это то, что я уже делаю. Проблема в том, что я создаю свой объект Todo в функции onPressed FloatingActionButton и не получаю обновленных значений.

NoviceCoder 05.05.2023 02:21
Ответ принят как подходящий

У нас есть недостатки:

Первый: изменяется состояние экрана, а не объекта, при создании объекта todo ему присваивается адрес памяти, и поскольку он был создан внутри onPressed, его нигде нельзя будет изменить это правильный пример этого.

Второй: я видел, что есть состояние со списком объектов «todo», поэтому я предполагаю, что на предыдущем экране есть их список или есть намерение где-то их перечислить, и этот экран, который был выставлен в проблема заключается в выборе «целого» объекта. Основываясь на этой предпосылке, я предлагаю передать этот объект (или, по крайней мере, идентификатор, который его представляет) в качестве параметра этому экрану, чтобы его можно было изменить.

Третье: на уровне представления много логики, это не обязательно неправильно, но отклоняется от хороших практик и усложняет сопровождение проекта.

Примечание. Я рекомендую отличную страницу, которая поможет вам структурировать ваш проект. https://resocoder.com/ Я особенно рекомендую учебник по чистой архитектуре, даже если вашей целью является MVVM, он даст вам хорошее представление о том, как работает Flutter.

Примечание 2: извините за мой английский, я не родной. Если с помощью этих советов вы не можете найти решение, не могли бы вы создать репозиторий с этим кодом, чтобы я мог помочь вам лучше?

Редактировать:

Я нашел проблему:

в файле todo_model.dart у вас есть код

  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;
  }

Право это:

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;
 }

Спасибо, сделаю репозиторий и выложу. Сегодня постараюсь выполнить все ваши рекомендации. Я включу ссылку репо, как только это будет сделано.

NoviceCoder 08.05.2023 21:57

Для ясности мой add_todo_screen имеет переменную состояния, удаленную, которую по умолчанию я выбираю как ложную. При взаимодействии с пользователем, выборе пульта для задачи, обратный вызов изменяет эту переменную на истинную только для экрана. Однако, поскольку я создал объект Todo внутри нажатия, он не может ссылаться на новые изменения состояния. Как мне тогда динамически создавать новые объекты todo по нажатию кнопки? P.S. ваш английский намного лучше, чем у других носителей языка.

NoviceCoder 08.05.2023 22:09

По вашему совету я думаю, что мне нужно создать новый блок/событие, когда нажимается моя удаленная кнопка. Однако кнопка нажимается до того, как будет создан объект todo, поэтому не будет никакого объекта для обновления. Я не уверен в логике, которую я должен использовать, чтобы щелчок на удаленной кнопке учитывался при создании новой задачи.

NoviceCoder 08.05.2023 23:55

Я просмотрю репозиторий и отправлю для них пул реквест, чтобы вы просмотрели мое решение.

francisco gomes 09.05.2023 01:03

Я нашел проблему и отредактировал ответ с решением. Рекомендую туториалы на упомянутой мной странице, они очень помогли вам понять, как работает разделение обязанностей каждого слоя кода.

francisco gomes 09.05.2023 01:36

Другие вопросы по теме