我正在编写一个简单的提醒应用程序,该应用程序本质上是ListView
中的TextField
,当它们模糊或提交后,便会更新数据库。当用户点击复选框或在GestureDetector
外部时,我使用FocusNode
和TextField
来模糊TextField
。
这是唯一的路线,效果很好。但是,当我将相同的页面推到现有页面的顶部时,焦点行为将变得完全有问题,并且该应用程序将无法使用。
这里有一个视频演示:https://www.youtube.com/watch?v=13E9LY8yD3A
我的代码本质上是这样的:
/// main.dart
class MyApp extends StatelessWidget {
static FocusScopeNode rootScope; // just for debug
@override
Widget build(BuildContext context) {
rootScope = FocusScope.of(context);
return MaterialApp(home: ReminderPage());
}
}
-
/// reminder_page.dart
class ReminderPage extends StatelessWidget {
final _blurNode = FocusNode();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Remind'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: () {
// Push new identical page.
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ReminderPage(),
));
},
),
],
),
body: StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('reminders').snapshots(),
builder: (context, snapshot) {
return _buildBody(context, snapshot.data);
},
),
);
}
Widget _buildBody(BuildContext context, QuerySnapshot data) {
List<Reminder> reminders =
data.documents.map((s) => Reminder.fromSnapshot(s)).toList();
return GestureDetector(
onTap: () {
_blur(context);
},
child: ListView(
children: reminders.map((r) => ReminderCard(r)).toList(),
),
);
}
void _blur(context) {
FocusScope.of(context).requestFocus(_blurNode);
}
}
-
/// reminder_card.dart
class ReminderCard extends StatelessWidget {
final Reminder reminder;
final TextEditingController _controller;
final _focusNode = FocusNode();
final _blurNode = FocusNode();
ReminderCard(this.reminder)
: _controller = TextEditingController(text: reminder.text) {
_focusNode.addListener(() {
if (!_focusNode.hasFocus) {
reminder.updateText(_controller.text); // update database
}
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
_blur(context);
},
child: Row(
children: <Widget>[
_buildCheckBox(context),
_buildTextField(context),
],
),
);
}
Widget _buildCheckBox(context) {
return Checkbox(
value: reminder.done,
onChanged: (done) {
print(MyApp.rootScope.toStringDeep()); // print Focus tree
_blur(context);
reminder.updateDone(done); // update database
},
);
}
Widget _buildTextField(context) {
return TextField(
onSubmitted: reminder.updateText, // update database
focusNode: _focusNode,
);
}
void _blur(context) {
FocusScope.of(context).requestFocus(_blurNode);
}
}
我发现this question听起来很相似,但是我不了解自定义过渡如何解决任何问题,并且与焦点无关。像OP一样,我尝试了很多不同的操作来弄乱FocusScope
,包括调用detach()
,reparentIfNeeded()
或将根的FocusScope
一直向下传递因此不会每次都创建一个新的FocusScope
,但是没有一个可以提供任何有用的功能。而且我也尝试了自定义过渡,但无济于事。
调试输出在第一条路线上显示此信息(当我选中复选框时):
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): └─child 1: FocusScopeNode#76ef6
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): └─child 1: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#f07c7(FOCUSED)
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): └─child 1: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#f138f(FOCUSED)
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): └─child 1: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#e68b3(FOCUSED)
这是第二条路线:
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): ├─child 1: FocusScopeNode#a1008
I/flutter (28362): └─child 2: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#a76e6
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): ├─child 1: FocusScopeNode#a1008
I/flutter (28362): │ focus: FocusNode#02ebf(FOCUSED)
I/flutter (28362): │
I/flutter (28362): └─child 2: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#a76e6
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): ├─child 1: FocusScopeNode#a1008
I/flutter (28362): │ focus: FocusNode#917da(FOCUSED)
I/flutter (28362): │
I/flutter (28362): └─child 2: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#a76e6
所以当我们按第二条路线时,第一条路线的FocusScope看起来像是子级2,这对我来说是正确的。
我在做什么错了?
答案 0 :(得分:0)
感谢卢卡斯的上述评论,this other SO question我得以解决此问题。
首先,我减少了FocusNode
的数量:每个TextField
仅减少了一个,为父ReminderPage
减少了一个。父级现在具有一个功能blur()
,该功能使所有TextField
失去焦点;这样,当我在编辑TextField
的复选框时单击reminder.updateText()
的复选框时,正在编辑的复选框将失去焦点。
第二,我更改了StreamBuilder
函数(此处未显示),因此仅当文本与现有文本不同时才更新数据库。否则,由于TextField
,我们将重建卡,从而弄乱了正在编辑的TextEditingController
的焦点。
第三,我现在正在听FocusNode
而不是FocusNode
来对数据库进行更改。但是我仍然只在StreamBuilder
失去焦点时才更新数据库,否则ReminderPage
会重建页面并再次使焦点混乱。
但是,这仍然不能解释为什么当// --->
是应用程序的主页时,而不是将其推到路线顶部时,它为什么能正常工作。答案来自this other SO question,该问题也遇到了同样的问题:放置在初始屏幕后的窗口小部件会不断地进行重建,而用作应用程序主页时则不会不断地重建。我仍然不明白为什么这会有什么区别,但是相同的修复方法对我有用:将其更改为StatefulWidget,仅在实际更改时重新构建。
最终代码如下所示。我用/// main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: ReminderPage());
}
}
条注释突出了差异。
/// reminder_page.dart
class ReminderPage extends StatelessWidget {
final _blurNode = FocusNode();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Remind'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: () {
// Push new identical page.
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ReminderPage(),
));
},
),
],
),
body: StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('reminders').snapshots(),
builder: (context, snapshot) {
return _buildBody(context, snapshot.data);
},
),
);
}
Widget _buildBody(BuildContext context, QuerySnapshot data) {
List<Reminder> reminders =
data.documents.map((s) => Reminder.fromSnapshot(s)).toList();
return GestureDetector(
onTap: () {
// ---> Blur all TextFields when clicking in the background.
blur(context);
},
child: ListView(
// ---> Passing the parent to each child so they can call parent.blur()
children: reminders.map((r) => ReminderCard(r, this)).toList(),
),
);
}
// ---> This will unfocus all TextFields.
void blur(context) {
FocusScope.of(context).requestFocus(_blurNode);
}
}
-
/// reminder_card.dart
// ---> Converted to a StatefulWidget! That way we can save a snapshot of reminder
// as it was when we last built the widget, and only rebuild it if it changed.
class ReminderCard extends StatefulWidget {
final Reminder reminder;
final TextEditingController _controller;
// ---> Only one focus node, for the TextField.
final _focusNode = FocusNode();
// ---> The parent.
final ReminderPage page;
ReminderCard(this.reminder, this.page)
: _controller = TextEditingController(text: reminder.text) {
// ---> Listen to text changes. But only updating the database
// if the TextField is unfocused.
_controller.addListener(() {
if (!_focusNode.hasFocus) {
reminder.updateText(_controller.text); // update database
}
});
}
@override
ReminderCardState createState() => ReminderCardState();
}
class ReminderCardState extends State<ReminderCard> {
Widget card;
Reminder snapshotWhenLastBuilt;
@override
Widget build(BuildContext context) {
// ---> Only rebuild if something changed, otherwise return the
// card built previously.
// The equals() function is a method of the Reminder class that just tests a
// few fields.
if (card == null || !widget.reminder.equals(snapshotWhenLastBuilt)) {
card = _buildCard(context);
snapshotWhenLastBuilt = widget.reminder;
}
return card;
}
Widget _buildCard(context) {
return GestureDetector(
onTap: () {
// ---> Blur all TextFields when clicking in the background.
widget.page.blur(context);
},
child: Row(
children: <Widget>[
_buildCheckBox(context),
_buildTextField(context),
],
),
);
}
Widget _buildCheckBox(context) {
return Checkbox(
value: widget.reminder.done,
onChanged: (done) {
// ---> Blur all TextFields when clicking on a checkbox.
widget.page.blur(context);
widget.reminder.updateDone(done); // update database
},
);
}
Widget _buildTextField(context) {
return TextField(
focusNode: widget._focusNode,
controller: widget._controller,
);
}
}
-
it('name of test', fakeAsync(inject([ Service], (hcs: Service) => {
const pipe = new MyPipe(hcs);
tick();
const expectedResult = ...
//Here the constructor of the hcs-service has to be completet, otherwise the Pipe fails
const result = pipe.transform(...);
expect(result).toEqual(expectedResult);
})));