Предыстория: у меня есть приложение 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
Франсиско прав, я воссоздал ваш код в упрощенной версии и обнаружил, что remoteTask меняется на true и без проблем переходит на другие страницы. Нам нужно увидеть, как ваш context.read<TodosBloc>().add(AddTodo(todo: todo)); работает, потому что с этой частью что-то не так. Вы можете предоставить ToDosBloc?
Я добавил код блока и репозитория, спасибо, что указали, где находится код ошибки. Я также могу сделать репо, если это будет проще.
Не могу точно сказать, где проблема. У вас возникает та же проблема, если вы передаете bool отдельно? Например, AddTodo(todo: todo, remote: remoteTask); и в вашем классе AddTodo возьмите 2 параметра, Todo todo и bool remote, затем List<Object> get props => [todo, remote];. Это может быть как-то связано с BuildContext блока
Я разместил свой ответ ниже
Я добавил полное репо, если это вообще поможет. В настоящее время я просматриваю ответ Франсисо и буду делать обновления. Спасибо вам всем.





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». Просто когда я ссылаюсь на эту переменную позже в своей плавающей кнопке действия, она не регистрирует изменение состояния.
Чтобы исправить это, нужно переместить создание объекта 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
Я также использую todo onpressed
Я хочу сказать, что ваше предложение - это то, что я уже делаю. Проблема в том, что я создаю свой объект Todo в функции onPressed FloatingActionButton и не получаю обновленных значений.
У нас есть недостатки:
Первый: изменяется состояние экрана, а не объекта, при создании объекта 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;
}
Спасибо, сделаю репозиторий и выложу. Сегодня постараюсь выполнить все ваши рекомендации. Я включу ссылку репо, как только это будет сделано.
Для ясности мой add_todo_screen имеет переменную состояния, удаленную, которую по умолчанию я выбираю как ложную. При взаимодействии с пользователем, выборе пульта для задачи, обратный вызов изменяет эту переменную на истинную только для экрана. Однако, поскольку я создал объект Todo внутри нажатия, он не может ссылаться на новые изменения состояния. Как мне тогда динамически создавать новые объекты todo по нажатию кнопки? P.S. ваш английский намного лучше, чем у других носителей языка.
По вашему совету я думаю, что мне нужно создать новый блок/событие, когда нажимается моя удаленная кнопка. Однако кнопка нажимается до того, как будет создан объект todo, поэтому не будет никакого объекта для обновления. Я не уверен в логике, которую я должен использовать, чтобы щелчок на удаленной кнопке учитывался при создании новой задачи.
Я просмотрю репозиторий и отправлю для них пул реквест, чтобы вы просмотрели мое решение.
Я нашел проблему и отредактировал ответ с решением. Рекомендую туториалы на упомянутой мной странице, они очень помогли вам понять, как работает разделение обязанностей каждого слоя кода.
Не могли бы вы показать мне код блока? Из того, что я проанализировал, вы меняете состояние экрана, а не объекта, если вы хотите изменить isRemote внутри объекта, эта логика должна быть в блоке