导航器弄乱了FocusScope

时间:2019-02-24 01:29:19

标签: dart flutter

我正在编写一个简单的提醒应用程序,该应用程序本质上是ListView中的TextField,当它们模糊或提交后,便会更新数据库。当用户点击复选框或在GestureDetector外部时,我使用FocusNodeTextField来模糊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,这对我来说是正确的。

我在做什么错了?

1 个答案:

答案 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);
})));