В моем приложении есть (помимо прочего) TextField
и ElevatedButton
. Я управляю состоянием с помощью Provider
. Я хочу, чтобы цвет кнопки менялся с белого на синий при изменении содержимого TextField.
У меня есть этот код в TextField:
onChanged: (_) {
context.read<Data>().changeButtonColor();
},
Это в ElevatedButton:
style: ElevatedButton.styleFrom(
backgroundColor: context.watch<Data>().buttonColor),
И это в Провайдере:
Color buttonColor = Colors.white;
void changeButtonColor() {
buttonColor = Colors.blue;
notifyListeners();
}
Цвет кнопки изменится, как и ожидалось. Однако у меня проблема в том, что курсор в TextField
переходит в начало текста, как только срабатывает событие onChanged
. Это делает приложение непригодным для использования.
Почему курсор делает это? И как мне сделать так, чтобы код работал как надо, т. е. без перехода курсора TextField
в начало текста?
Обновлено: родительский метод build()
включает в себя
Note? selNote = context.watch<Data>().selectedNote;
noteTextController.text = selNote.content;
Для справки, вот полный код двух виджетов:
// Note Text TextField---------------------------------
Padding(
padding: EdgeInsets.only(
top: 15,
bottom: 15,
),
child: TextField(
controller: noteTextController,
keyboardType: TextInputType.multiline,
maxLines: null,
minLines: 10,
decoration: InputDecoration(
border: InputBorder.none,
fillColor: Colors.white,
),
onChanged: (_) {
context.read<Data>().changeButtonColor();
},
),
),
// Save Button------------------------------------------
Padding(
padding: EdgeInsets.only(
top: 15,
bottom: 15,
),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: context.watch<Data>().buttonColor),
onPressed: () async {
selNote = context.read<Data>().selectedNote;
var title = noteTitleController.text;
var content = noteTextController.text;
// If this is a new note ... create it
if (selNote == null) {
context.read<Data>().add(title, content);
} else {
// If this is an already existing note ... update it
selNote!.title = title;
selNote!.content = content;
context.read<Data>().update(title: title, content: content);
}
},
child: Text('Save'),
),
),
@DhafinRayhan Я отредактировал вопрос. Два виджета Padding
выше являются дочерними элементами ListView,
, который является дочерним элементом Container
внутри ListView
.
Проблема в том, что вы меняете текст контроллера внутри метода сборки. Каждый раз, когда ваш TextField
вызывает обратный вызов onChanged
, значение цвета кнопки в провайдере будет обновляться, поэтому, когда его просматривают в том же виджете, он перезапускает метод build
, который вызывает обновление текста контроллера, следовательно, переход курсора.
Значение TextField
должно быть локальным, и вам не следует вручную обрабатывать изменение text
, если источником изменения является сам TextField
. Просто полагайтесь на внутренние изменения контроллера, и если вы хотите «отразить» состояние до более глобального состояния, вы можете сохранить обратный вызов onChanged
. Вам не нужно считывать значение обратно в контроллер.
В более общем смысле, вам не следует вызывать установщик controller.text
декларативным/реактивным способом. Его следует вызывать только в императивном порядке.
Некоторые связанные статьи:
Во-первых, спасибо за объяснение. Первые два абзаца очень полезны. Если у вас есть время, не могли бы вы немного расширить последний абзац? Я не уверен, что это значит. Я просто знаю, что рассматривал такие вещи, как Flutter’s Provider, как способ отделить пользовательский интерфейс от бизнес-логики, и подумал, что мне нужно использовать контроллеры для управления текстом TextField.
Когда вы передаете TextEditingController
TextField
, он самостоятельно обрабатывает обновление сохраненного text
. По сути, наличие этого контроллера внутри State
из StatefulWidget
— это уже вопрос управления состоянием.
PS Я прочитал [это] (freecodecamp.org/news/…), чтобы помочь мне понять в целом.
Во Flutter существует «эфемерное состояние», которое обрабатывает локальное состояние виджета, и «состояние приложения», которое обрабатывает состояния всего приложения. TextEditingController
всегда должно быть эфемерным состоянием. Независимо от того, хотите ли вы, чтобы значение было доступно для состояния приложения, это не имеет значения, контроллер все равно должен обрабатывать состояние самостоятельно. Затем вы можете создать новое состояние всего приложения и скопировать в него состояние контроллера, но не заставлять контроллер считывать значение из состояния приложения.
Код внутри build
не должен иметь побочных эффектов, таких как императивное действие, подобное тому, как вы приказываете контроллеру обновить свое значение. Его следует использовать только для рендеринга виджета и, вероятно, выполнять некоторые чистые вычисления, чтобы получить некоторые вычисленные значения, которые необходимо передать виджетам. С другой стороны, императивные методы могут иметь побочные эффекты, примерами которых являются onTap
или onPressed
обратный вызов кнопок и детекторов жестов.
Также обратите внимание, что среда Flutter может вызывать метод build
сколько угодно раз, и ожидается, что он будет вызывать метод build
до 60-120 раз в секунду, поэтому он должен быть чистым (но, конечно, из-за оптимизации , обычно такого не бывает).
Да, кстати, я добавил в ответ две ссылки, которые могут быть связаны. Надеюсь, это поможет!
Потрясающий. Оцените полноту вашего ответа(ов). Спасибо!
PS Для тех, кто увидит это сообщение позже, я не знал, когда писал его, что когда вы программно устанавливаете значение текстового контроллера, присваивая значение его текстовому свойству, курсор перейдет в начало текста. В этом суть вопроса, о котором я спрашивал. <br> PPS Еще один источник путаницы для меня связан с проблемой всего приложения и эфемерного состояния. Поскольку текст TextField
также отображается в других виджетах, я рассматриваю его как состояние всего приложения.
Как определить контроллер? Как выглядит родительский виджет? Кроме того, можете ли вы попробовать добавить ключ к
TextField
?