Riverpod 'StreamProvider'流导致Flutter 'TextFormField'在输入第一个字符时完全丢失光标,尽管没有发出流事件

5gfr0r5j  于 2022-12-14  发布在  Flutter
关注(0)|答案(1)|浏览(80)

概述

该问题包括Flutter TextFormField在通过键盘输入第一个字符时完全失去光标-请参阅下面的完整复制步骤。
请注意,TextFormField正在监视一个流,该流不应(也不会)在下面的发布复制步骤2-3期间发出任何事件。在TextFormField中的onChanged上,currentValueProvider Riverpod StateProvider将使用TextFormField值进行更新。
下面的最小工作示例valueStreamProvider Riverpod StreamProvider为特色,这可能看起来有点奇怪-这是因为实际的valueStreamProvider Riverpod StreamProvider版本要复杂得多,但是下面的最小工作示例演示了我们旨在克服的问题。
对里弗波德大吼一声,非常棒的装备,非常感谢雷米·鲁塞莱。
请参阅以下问题的完整详细信息,我们感谢所有提前反馈。如果您需要更多示例的详细信息,请随时联系我们。

问题复制步骤:

  1. flutter run打开web应用程序。TextFormField随后将按预期使用文本“测试值”进行初始化; TextFormField的初始文本/值经由正被观看的valueStreamProvider Riverpod StreamProvider来填充。
    1.单击TextFormField(光标按预期 Flink )并输入1个字符(通过键盘)-然后出现光标自动从TextFormField消失的问题,因此用户不再(通过键盘)在TextFormField中键入任何其他字符。
    1.第二次单击TextFormField(光标按预期 Flink )并输入1个以上字符(通过键盘)-TextFormField中的光标未消失(按预期),用户能够按预期继续在TextFormField中键入字符(通过键盘)。

预期行为(未发生):

  1. flutter run打开web应用程序。然后,TextFormField将按预期使用文本“测试值”进行初始化; TextFormField的初始文本/值经由正被观看的valueStreamProvider Riverpod StreamProvider来填充。
    1.在TextFormField中单击(光标按预期 Flink )并输入1个以上的字符(通过键盘输入所需数量的字符)。光标在任何时候都不会从“TextFormField”中消失(除非发生单击事件,该事件在“TextFormField”之外发生)。
    基本上,只要currentValueProvider Riverpod StateProvider填充了1+个字符,我们就希望TextFormField忽略(不接收)来自valueStreamProvider Riverpod StreamProvider的事件(因此currentValueProvider Riverpod StateProviderTextFormField更新以实现这一点)。

通过调试,我们发现:

  • final TextEditingController controller = useTextEditingController();会正确保存游标位置的状态(在上述所有“问题复制步骤”中)。FocusNode状态也会在上述所有“问题复制步骤”中保存,而且在任何步骤中都不会遗失此焦点。
  • 如果我们从TextFormField中省略valueStreamProvider Riverpod StreamProvider,则步骤1中的问题(上述问题复制步骤),因此,这很可能是由valueStreamProvider Riverpod StreamProvider引起的。我们已经记录了整个valueStreamProvider,没有发出任何流事件-也许StreamProvider正在发出加载事件?或者?
  • 当游标在上述发出复制步骤步骤1中丢失时,日志记录已确认TextformField已重建-因此可能是游标丢失的原因。

最小工作示例:

请创建一个新的flutter Web应用程序,然后粘贴到以下文件中:

主省道

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

void main() {
  runApp(const ProviderScope(child: MyApp() ));
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData( primarySwatch: Colors.blue ),
      home: const Material(child: TextWidget()),
    );
  }
}

/// Riverpod Provider - current value
final currentValueProvider = StateProvider<String?>((final ref) => null);

/// Riverpod Provider - Value Stream
final valueStreamProvider = StreamProvider.autoDispose<String?>((final ref) async* {

  // DB Document StreamProvider watched here - *** not in use in this minimal 
  // example *** but to illustrate why a StreamProvider is required 
  // for valueStreamProvider
  // final String? firebaseDocumentStream = await ref.watch(firebaseDocumentStreamProvider
  //  .selectAsync((final String? v) => v?.firstName),
  // );

  // Only send event if 'Current Value' is not populated
  final String? currentValue = ref.watch(currentValueProvider);
  if (currentValue == null) {
    yield* Stream<String?>.value('Test Value');
  }

});

/// Text Widget
class TextWidget extends HookConsumerWidget {
  const TextWidget({ final Key? key, }) : super(key: key);
  @override
  Widget build(final BuildContext context, final WidgetRef ref) {

    final TextEditingController controller = useTextEditingController();
    final FocusNode focusNode = useFocusNode();

    return Consumer(
        builder: (final BuildContext context, final WidgetRef ref, final Widget? child) {

          // Listen to Value Stream
          final String? value = ref.watch(valueStreamProvider).value;

          return TextFormField(
            key: UniqueKey(),
            focusNode: focusNode,
            controller: controller..text =  value ?? "",
            onChanged: (final String value) async {

              // Update 'Current Value' Provider
              ref.read(currentValueProvider.notifier).state = value;
            },
          );
        }
    );
  }
}

出版规范yaml

name: test_text_field_stream
description: A new Flutter project.
publish_to: 'none'
version: 1.0.0+1
environment:
  sdk: '>=2.18.5 <3.0.0'

dependencies:
  flutter:
    sdk: flutter
  flutter_hooks: ^0.18.0
  hooks_riverpod: ^2.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true

在此之后编辑

回复Remi的友好反馈
请看下面的“解决方案,与雷米的帮助(谢谢!)”回答这个帖子。

进一步查询

关于该解决方案,与雷米的帮助(谢谢!)”回答下面这个帖子,我们有以下进一步的问题,如果我们可以:
1.如果skipLoadingOnReload未设置为true,则ref.watch(valueStreamProvider).when会卡住loading-这是为什么?如果我们设置skipLoadingOnReload = true,则不会出现此问题,并且data会按预期返回(即按预期构建/填充TextFormField)...
1.在TextFormField中,key: UniqueKey()实际上应该是一个key: GlobalKey(),这样我们就可以编写集成测试,它与TextFormField's交互/测试TextFormField's-我想建议不要在TextFormField中包含key: GlobalKey(),而是添加一个带有key: GlobalKey()的父Container,并从有没有好的文章解释“何时”和“何时不”使用keys,特别是关于TextFormField's,因为完全理解它会很好?

1.如果我们使用controllerTextEditingController),而不是将initialValue替换为TextFormField(在我们上面的“回复Remi的友好反馈”示例中),当第一个字符输入到TextFormField**(i)光标跳到TextFormField的开头,并且(ii)**输入的第一个字符从未出现在TextFormField中,但从第二个字符开始出现......这是为什么,也许这超出了这个帖子/问题的范围。2请看下面第三点问题的完整例子(pubspec.yaml和原来的例子是一样的):

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

void main() {
  runApp(const ProviderScope(child: MyApp() ));
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData( primarySwatch: Colors.blue ),
      home: const Material(child: TextWidget()),
    );
  }
}

/// Riverpod Provider - current value
final currentValueProvider = StateProvider<String?>((final ref) => null);

/// Riverpod Provider - Value Stream
final valueStreamProvider = StreamProvider.autoDispose<String?>((final ref) async* {

  // DB Document StreamProvider watched here - *** not in use in this minimal
  // example *** but to illustrate why a StreamProvider is required
  // for valueStreamProvider
  // final String? firebaseDocumentStream = await ref.watch(firebaseDocumentStreamProvider
  //  .selectAsync((final String? v) => v?.firstName),
  // );

  // Only send event if 'Current Value' is not populated
  final String? currentValue = ref.watch(currentValueProvider);
  if (currentValue == null) {
    yield* Stream<String?>.value('Test Value');
  }

});

/// Text Widget
class TextWidget extends HookConsumerWidget {
  const TextWidget({ final Key? key, }) : super(key: key);
  @override
  Widget build(final BuildContext context, final WidgetRef ref) {

    final controller = useTextEditingController();

    return ref.watch(valueStreamProvider).when(
      skipLoadingOnReload: true,
      data: (final String? value) => TextFormField(
        controller: controller..text = value ?? "",
        onChanged: (final String value) async {

          // Update 'Current Value' Provider
          ref.read(currentValueProvider.notifier).state = value;
        },
      ),
      error: (final Object err, final StackTrace stack)  =>
        throw Exception("Deal with error TODO: $err"),
      loading: () => const Text("Loading TODO"),
    );
  }
}
tp5buhyn

tp5buhyn1#

在Remi的帮助下,解决方案(谢谢!)

感谢Remi为我们指明了正确的方向--现在它可以按预期的方式工作了,并进行了以下更改(下面是完整的示例,pubspec.yaml与上面的原始示例相同):
1.如Remi所建议的那样,省略key: UniqueKey()(谢谢!!)
1.在观察valueStreamProviderStreamProvider(即ref.watch(valueStreamProvider).when)时实现.when,并设置skipLoadingOnReload = true
1.在TextFormField中,将controller属性取代为initialValue属性。

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

void main() {
  runApp(const ProviderScope(child: MyApp() ));
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData( primarySwatch: Colors.blue ),
      home: const Material(child: TextWidget()),
    );
  }
}

/// Riverpod Provider - current value
final currentValueProvider = StateProvider<String?>((final ref) => null);

/// Riverpod Provider - Value Stream
final valueStreamProvider = StreamProvider.autoDispose<String?>((final ref) async* {

  // DB Document StreamProvider watched here - *** not in use in this minimal
  // example *** but to illustrate why a StreamProvider is required
  // for valueStreamProvider
  // final String? firebaseDocumentStream = await ref.watch(firebaseDocumentStreamProvider
  //  .selectAsync((final String? v) => v?.firstName),
  // );

  // Only send event if 'Current Value' is not populated
  final String? currentValue = ref.watch(currentValueProvider);
  if (currentValue == null) {
    yield* Stream<String?>.value('Test Value');
  }

});

/// Text Widget
class TextWidget extends HookConsumerWidget {
  const TextWidget({ final Key? key, }) : super(key: key);
  @override
  Widget build(final BuildContext context, final WidgetRef ref) {

    return ref.watch(valueStreamProvider).when(
      skipLoadingOnReload: true,
      data: (final String? value) => TextFormField(
        initialValue: value,
        onChanged: (final String value) async {

          // Update 'Current Value' Provider
          ref.read(currentValueProvider.notifier).state = value;
        },
      ),
      error: (final Object err, final StackTrace stack)  =>
        throw Exception("Deal with error TODO: $err"),
      loading: () => const Text("Loading TODO"),
    );
  }
}

相关问题