csells / go_router

The purpose of the go_router for Flutter is to use declarative routes to reduce complexity, regardless of the platform you're targeting (mobile, web, desktop), handling deep linking from Android, iOS and the web while still allowing an easy-to-use developer experience.
https://gorouter.dev
441 stars 97 forks source link

Combining context.goNamed with an AutofillGroup breaks TextField's "autofocus" on iOS #317

Closed Lootwig closed 2 years ago

Lootwig commented 2 years ago

Running the code below on flutter 2.8.1 in iOS 15.2 on the Simulator results in the keyboard closing immediately after navigating to the Second page.

The behavior does not occur

Minimum working example ```dart import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { MyApp({Key? key}) : super(key: key); final _router = GoRouter( routes: [ GoRoute( path: '/', builder: (_, __) => const Home(), routes: [ GoRoute( name: '2', path: '2', builder: (_, __) => const Second(), ), GoRoute( name: '1', path: '1', builder: (_, __) => const First(), ), ], ), ], ); @override Widget build(BuildContext context) { return MaterialApp.router( routeInformationParser: _router.routeInformationParser, routerDelegate: _router.routerDelegate, ); } } class Home extends StatefulWidget { const Home({Key? key}) : super(key: key); @override State createState() => _HomeState(); } class _HomeState extends State { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: ElevatedButton( onPressed: () => context.goNamed('1'), child: const Center(child: Text('To First')), ), ), ); } } class First extends StatelessWidget { const First({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: () => context.goNamed('2'), child: const Text('To Second'), ), const AutofillGroup(child: SizedBox()), ], ), ), ); } } class Second extends StatelessWidget { const Second({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: Center(child: TextFormField(autofocus: true)), ); } } ```

Video (doesn't show in browser for me, but can be downloaded when copying address):

https://user-images.githubusercontent.com/39827040/149756670-80fb21ba-77dd-4ce0-8681-4a8a33bc47eb.mov

csells commented 2 years ago

That's strange. @lulupointu can you think of anything that might be causing that behavior?

lulupointu commented 2 years ago

I did not but I investigated and I think this is because of AutofillGroup.dispose which calls TextInput.finishAutofillContext which removes the keyboard focus.

It's not that autofocus does not work (you can actually see the textfield being focus during a few frames), it's rather that autofocus acts before TextInput.finishAutofillContext.

Why replacing goNamed with Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const Second())) works ?

When you do Navigator.of(context).push it pushes on top of the First widget. Therefore the First widget is not disposed which means that TextInput.finishAutofillContext is not called so autofocus works.

Why removing the AutofillGroup on First works?

As I said, the problematic part is TextInput.finishAutofillContext which is called in AutofillGroup.dispose so removing AutofillGroup does the trick.

How to fix this?

The solution I found was to manually refocus the first time the TextField is unfocused. The issue is that if you navigate to Second from a screen which does not have a AutofillGroup.dispose, then the first manual keyboard focus will be ignore. Maybe a timer to remove the listener might be good enough. Definitely not perfect though

Here is my Second implementation:

class Second extends StatefulWidget {
  const Second({Key? key}) : super(key: key);

  @override
  State<Second> createState() => _SecondState();
}

class _SecondState extends State<Second> {
  final _focusNode = FocusNode();

  @override
  void initState() {
    super.initState();
    // Focus after un-focus
    _focusNode.addListener(_refocusPlease);
  }

  _refocusPlease() {
    if (!_focusNode.hasFocus) {
      _focusNode.nextFocus();
      _focusNode.removeListener(_refocusPlease);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: TextFormField(focusNode: _focusNode, autofocus: true),
      ),
    );
  }
}

Also I tried a few things like WidgetsBinding.instance.addPostFrameCallback but this did not work. I think this is because of how Navigator works (when a screen is removed, it is not removed directly but stays a few frames inside the widget tree to allow its out animation if any)

csells commented 2 years ago

@Lootwig does you confirm that this solves your problem?

@lulupointu thanks!

Lootwig commented 2 years ago

@lulupointu thanks for looking into it, but the suggested implementation for Second does not work very well:

@csells even if it's not a solution, I understand that this is not a GoRouter issue. Would appreciate your upvotes over at https://github.com/flutter/flutter/issues/96756!