首页 > 解决方案 > FocusScope 被 Navigator 搞砸了

问题描述

我正在编写一个简单的提醒应用程序,它本质上是一个ListViewof TextFields,当模糊或提交时,它会更新数据库。我使用一堆GestureDetectors 和FocusNodes 来模糊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);
  }
}

我发现这个问题听起来非常相似,但我不明白自定义转换如何解决任何问题并且与焦点有关。和 OP 一样,我尝试了很多不同的东西来搞乱FocusScope,包括 call detach(),或者一直向下reparentIfNeeded()传递根目录,所以每次都不会创建一个新的,但这些都没有提供任何接近工作的东西。而且我还尝试了自定义过渡,但无济于事。FocusScopeFocusScope

调试输出在第一条路线上显示了这一点(当我选中复选框时):

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,这对我来说听起来是正确的。

我究竟做错了什么?

标签: dartflutter

解决方案


感谢卢卡斯上面的评论和这个其他的问题,我能够解决这个问题。

首先,我减少了FocusNodes 的数量:只有一个 per TextField,一个用于 parent ReminderPage。父级现在有一个blur()使所有TextFields 不聚焦的功能;这样,当我TextField在编辑另一个时单击复选框时,正在编辑的复选框没有焦点。

其次,我更改了我的reminder.updateText()函数(此处未显示),因此它仅在文本与现有文本不同时更新数据库。否则,我们将重建卡片,因为StreamBuilder,弄乱了TextField正在编辑的焦点。

第三,我现在正在听TextEditingController而不是对FocusNode数据库进行更改。但是我仍然只在FocusNode没有焦点时更新数据库,否则StreamBuilder会重建页面并再次混淆焦点。

但这仍然不能解释为什么当它ReminderPage是应用程序的主页时它工作得相当好,而不是当它被推送到路由顶部时。答案来自另一个遇到相同问题的 SO 问题:当放置在启动屏幕之后时,小部件会不断重建,但在用作应用程序主页时不会。我仍然不明白为什么这有什么不同,但同样的修复对我有用:将其更改为 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,
    );
  }
}

推荐阅读