admin管理员组

文章数量:1026989

Edit: solved by adding a unique key to the widget constructor call:

Edit 2: as Randal Schwartz mentioned -> this is not a solution. Issue NOT solved and still open.

  @override
  SearchTermInput get searchTermInput => SearchTermInputOneString(
        key: UniqueKey(),
        currentSearchTermInputContent:
            widgetRef.read(apiEndpointUniversalSearchTermInputContentProvider),
        captionList: ["Statement"],
        // todo : allow umlauts! or better deny numbers?
        inputFormatterAllow: RegExp(".*"),
        onSubmit: onSubmit,
        onLostFocus: onLostFocus,
      );

I have an abstract class (ApiEndpoint) with two implementations (ApiEndpointName, ApiEndpointUniversal).

Both implementations have a StatefulWidget (SearchTermInputOneString) inherited from the same base class (SearchTermInput).

For both ApiEndpoint-objects the Widget part of SearchTermInputOneStringis created separately - what is correct. My problem is, that the State-part of SearchTermInputOneString is created only once - so ApiEndpointName and ApiEndpointUniversal own the same state.

I have to read ApiEndpoint-specific configuration during initState - that does not work because initState is called only for the first ApiEndpoint-instance :(

Any ideas how to solve that?

abstract class ApiEndpoint:

enum ApiEndpointType {
  today,
  unseen,
  universal,
  name,
  exactAge,
  ageBetween,
  ageGreaterThanOrEqual,
  ageLessThanOrEqual
}

abstract class ApiEndpoint {
  ApiEndpointType get type;
  String get title;
  SearchTermInput get searchTermInput;
  String get searchFunctionName;

  bool validateData(BuildContext context, WidgetRef ref);
  void onSubmit(
      BuildContext context, WidgetRef ref, List<String> searchTermInputContent);
  void onLostFocus(
      BuildContext context, WidgetRef ref, List<String> searchTermInputContent);
}

ApiEndpointName implementation:

class ApiEndpointName implements ApiEndpoint {
  final WidgetRef widgetRef;

  ApiEndpointName({required this.widgetRef});

  @override
  String get searchFunctionName => "getObituariesByName";

  @override
  SearchTermInput get searchTermInput => SearchTermInputOneString(
        currentSearchTermInputContent:
            widgetRef.read(apiEndpointNameSearchTermInputContentProvider),
        captionList: ["Name"],
        inputFormatterAllow: RegExp("[a-zA-Z% ]"),
        onSubmit: onSubmit,
        onLostFocus: onLostFocus,
      );

  @override
  String get title => "... by name";

  @override
  ApiEndpointType get type => ApiEndpointType.name;

  @override
  bool validateData(BuildContext context, WidgetRef ref) {
    ...
  }

  @override
  void onSubmit(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    ...
  }

  @override
  void onLostFocus(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    ...
  }

ApiEndpointUniversal implementation:

class ApiEndpointUniversal implements ApiEndpoint {
  final WidgetRef widgetRef;

  ApiEndpointUniversal({required this.widgetRef});

  @override
  String get searchFunctionName => "getObituariesUniversal";

  @override
  SearchTermInput get searchTermInput => SearchTermInputOneString(
        currentSearchTermInputContent:
            widgetRef.read(apiEndpointUniversalSearchTermInputContentProvider),
        captionList: ["Statement"],
        inputFormatterAllow: RegExp(".*"),
        onSubmit: onSubmit,
        onLostFocus: onLostFocus,
      );

  @override
  String get title => "... universal";

  @override
  ApiEndpointType get type => ApiEndpointType.universal;

  @override
  bool validateData(BuildContext context, WidgetRef ref) {
    ...
  }

  @override
  void onSubmit(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    ...
  }

  @override
  void onLostFocus(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    ...
  }
}

abstract StatefulWidget:

abstract class SearchTermInput extends ConsumerStatefulWidget {
  const SearchTermInput({super.key});

  List<String> get currentSearchTermInputContent;
  List<String>? get captionList;
  RegExp get inputFormatterAllow;
  void Function(BuildContext, WidgetRef, List<String>) get onSubmit;
  void Function(BuildContext, WidgetRef, List<String>) get onLostFocus;
}

Implementation:

class SearchTermInputOneString extends SearchTermInput {
  @override
  final List<String> currentSearchTermInputContent;

  @override
  final List<String> captionList;

  @override
  final RegExp inputFormatterAllow;

  @override
  final void Function(BuildContext, WidgetRef, List<String>) onSubmit;

  @override
  final void Function(BuildContext, WidgetRef, List<String>) onLostFocus;

  const SearchTermInputOneString({
    super.key,
    required this.currentSearchTermInputContent,
    required this.captionList,
    required this.inputFormatterAllow,
    required this.onSubmit,
    required this.onLostFocus,
  });

  @override
  ConsumerState<ConsumerStatefulWidget> createState() =>
      _SearchTermInputOneStringState();
}

class _SearchTermInputOneStringState
    extends ConsumerState<SearchTermInputOneString> {
  late TextEditingController textEditingController;

  @override
  void initState() {
    super.initState();
    textEditingController = TextEditingController();
    textEditingController.text = widget.currentSearchTermInputContent.isNotEmpty
        ? widget.currentSearchTermInputContent[0]
        : "";
  }

  @override
  void dispose() {
    textEditingController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Focus(
      child: TextFormField(
        controller: textEditingController,
        decoration: InputDecoration(
          border: const OutlineInputBorder(),
          labelText: widget.captionList.isNotEmpty ? widget.captionList[0] : "",
          suffixIcon: IconButton(
            onPressed: () {
              textEditingController.text = "";
            },
            icon: Icon(Icons.clear),
          ),
        ),
        inputFormatters: [
          FilteringTextInputFormatter.allow(widget.inputFormatterAllow)
        ],
        onFieldSubmitted: (_) {
          widget.onSubmit(context, ref, [textEditingController.text]);
        },
      ),
      onFocusChange: (hasFocus) {
        if (!hasFocus) {
          widget.onLostFocus(context, ref, [textEditingController.text]);
        }
      },
    );
  }
}

I create the List of ApiEndpoints this way:

List<ApiEndpoint> getApiEndPoints({required WidgetRef widgetRef}) {
  final List<ApiEndpoint> apiEndpoints = [
    ApiEndpointName(widgetRef: widgetRef),
    ApiEndpointUniversal(widgetRef: widgetRef),
  ];

  return apiEndpoints;
}

Edit: solved by adding a unique key to the widget constructor call:

Edit 2: as Randal Schwartz mentioned -> this is not a solution. Issue NOT solved and still open.

  @override
  SearchTermInput get searchTermInput => SearchTermInputOneString(
        key: UniqueKey(),
        currentSearchTermInputContent:
            widgetRef.read(apiEndpointUniversalSearchTermInputContentProvider),
        captionList: ["Statement"],
        // todo : allow umlauts! or better deny numbers?
        inputFormatterAllow: RegExp(".*"),
        onSubmit: onSubmit,
        onLostFocus: onLostFocus,
      );

I have an abstract class (ApiEndpoint) with two implementations (ApiEndpointName, ApiEndpointUniversal).

Both implementations have a StatefulWidget (SearchTermInputOneString) inherited from the same base class (SearchTermInput).

For both ApiEndpoint-objects the Widget part of SearchTermInputOneStringis created separately - what is correct. My problem is, that the State-part of SearchTermInputOneString is created only once - so ApiEndpointName and ApiEndpointUniversal own the same state.

I have to read ApiEndpoint-specific configuration during initState - that does not work because initState is called only for the first ApiEndpoint-instance :(

Any ideas how to solve that?

abstract class ApiEndpoint:

enum ApiEndpointType {
  today,
  unseen,
  universal,
  name,
  exactAge,
  ageBetween,
  ageGreaterThanOrEqual,
  ageLessThanOrEqual
}

abstract class ApiEndpoint {
  ApiEndpointType get type;
  String get title;
  SearchTermInput get searchTermInput;
  String get searchFunctionName;

  bool validateData(BuildContext context, WidgetRef ref);
  void onSubmit(
      BuildContext context, WidgetRef ref, List<String> searchTermInputContent);
  void onLostFocus(
      BuildContext context, WidgetRef ref, List<String> searchTermInputContent);
}

ApiEndpointName implementation:

class ApiEndpointName implements ApiEndpoint {
  final WidgetRef widgetRef;

  ApiEndpointName({required this.widgetRef});

  @override
  String get searchFunctionName => "getObituariesByName";

  @override
  SearchTermInput get searchTermInput => SearchTermInputOneString(
        currentSearchTermInputContent:
            widgetRef.read(apiEndpointNameSearchTermInputContentProvider),
        captionList: ["Name"],
        inputFormatterAllow: RegExp("[a-zA-Z% ]"),
        onSubmit: onSubmit,
        onLostFocus: onLostFocus,
      );

  @override
  String get title => "... by name";

  @override
  ApiEndpointType get type => ApiEndpointType.name;

  @override
  bool validateData(BuildContext context, WidgetRef ref) {
    ...
  }

  @override
  void onSubmit(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    ...
  }

  @override
  void onLostFocus(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    ...
  }

ApiEndpointUniversal implementation:

class ApiEndpointUniversal implements ApiEndpoint {
  final WidgetRef widgetRef;

  ApiEndpointUniversal({required this.widgetRef});

  @override
  String get searchFunctionName => "getObituariesUniversal";

  @override
  SearchTermInput get searchTermInput => SearchTermInputOneString(
        currentSearchTermInputContent:
            widgetRef.read(apiEndpointUniversalSearchTermInputContentProvider),
        captionList: ["Statement"],
        inputFormatterAllow: RegExp(".*"),
        onSubmit: onSubmit,
        onLostFocus: onLostFocus,
      );

  @override
  String get title => "... universal";

  @override
  ApiEndpointType get type => ApiEndpointType.universal;

  @override
  bool validateData(BuildContext context, WidgetRef ref) {
    ...
  }

  @override
  void onSubmit(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    ...
  }

  @override
  void onLostFocus(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    ...
  }
}

abstract StatefulWidget:

abstract class SearchTermInput extends ConsumerStatefulWidget {
  const SearchTermInput({super.key});

  List<String> get currentSearchTermInputContent;
  List<String>? get captionList;
  RegExp get inputFormatterAllow;
  void Function(BuildContext, WidgetRef, List<String>) get onSubmit;
  void Function(BuildContext, WidgetRef, List<String>) get onLostFocus;
}

Implementation:

class SearchTermInputOneString extends SearchTermInput {
  @override
  final List<String> currentSearchTermInputContent;

  @override
  final List<String> captionList;

  @override
  final RegExp inputFormatterAllow;

  @override
  final void Function(BuildContext, WidgetRef, List<String>) onSubmit;

  @override
  final void Function(BuildContext, WidgetRef, List<String>) onLostFocus;

  const SearchTermInputOneString({
    super.key,
    required this.currentSearchTermInputContent,
    required this.captionList,
    required this.inputFormatterAllow,
    required this.onSubmit,
    required this.onLostFocus,
  });

  @override
  ConsumerState<ConsumerStatefulWidget> createState() =>
      _SearchTermInputOneStringState();
}

class _SearchTermInputOneStringState
    extends ConsumerState<SearchTermInputOneString> {
  late TextEditingController textEditingController;

  @override
  void initState() {
    super.initState();
    textEditingController = TextEditingController();
    textEditingController.text = widget.currentSearchTermInputContent.isNotEmpty
        ? widget.currentSearchTermInputContent[0]
        : "";
  }

  @override
  void dispose() {
    textEditingController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Focus(
      child: TextFormField(
        controller: textEditingController,
        decoration: InputDecoration(
          border: const OutlineInputBorder(),
          labelText: widget.captionList.isNotEmpty ? widget.captionList[0] : "",
          suffixIcon: IconButton(
            onPressed: () {
              textEditingController.text = "";
            },
            icon: Icon(Icons.clear),
          ),
        ),
        inputFormatters: [
          FilteringTextInputFormatter.allow(widget.inputFormatterAllow)
        ],
        onFieldSubmitted: (_) {
          widget.onSubmit(context, ref, [textEditingController.text]);
        },
      ),
      onFocusChange: (hasFocus) {
        if (!hasFocus) {
          widget.onLostFocus(context, ref, [textEditingController.text]);
        }
      },
    );
  }
}

I create the List of ApiEndpoints this way:

List<ApiEndpoint> getApiEndPoints({required WidgetRef widgetRef}) {
  final List<ApiEndpoint> apiEndpoints = [
    ApiEndpointName(widgetRef: widgetRef),
    ApiEndpointUniversal(widgetRef: widgetRef),
  ];

  return apiEndpoints;
}
Share Improve this question edited Nov 18, 2024 at 7:46 user28298940 asked Nov 16, 2024 at 14:22 user28298940user28298940 112 bronze badges 3
  • "key: UniqueKey(),"... WTF. bye bye performance. It's keeping your widget alive by design. and you've just ruined that. You've hooked into the wrong State lifecycle. You probably want api.flutter.dev/flutter/widgets/State/… and not initState. – Randal Schwartz Commented Nov 16, 2024 at 21:58
  • I do not understand :( Everywhere I read "Do your initialization in initState" - what is wrong now? The initialization of the TextController? Or the Key? I read that a WidgetObject gets its own StateObject when I define a unique key for each StatelessWidget. When I do not set the key, neither initState is called nor createState in the WidgetObject - therefore didChangeDependencie would not be called, too - did it? – user28298940 Commented Nov 18, 2024 at 5:36
  • Okay - after thinking about your words a bit longer I suppose to understand what you mean. The widget (also it's state!) should live for the whole lifetime of the app (in best case) and only the state dependencies should be updated if needed. But with setting the unique keys I force destroying and recreating the widget - makes sense. But unfortunately I am not able to get this didChangeDependencies being called when I switch from the "Search by name" to the "Universal search" widget. Please have a look at my code below. – user28298940 Commented Nov 18, 2024 at 8:35
Add a comment  | 

2 Answers 2

Reset to default 1

After playing around a lot of time without getting didChangeDependencies to work I found out that didUpdateWidget did the trick. Do you think that this okay or is there another stumbling block that I overlooked?

  @override
  void didUpdateWidget(covariant SearchTermInputOneString oldWidget) {
    final log = getLogger();
    log.t("didUpdateWidget");

    // read apiEndpoint content
    currentContent = ref
        .read(widget.apiEndpointSearchTermInputContentProvider)
        .cast<String>();
    textEditingController.text =
        currentContent.isNotEmpty ? currentContent[0] : "";

    super.didUpdateWidget(oldWidget);
  }

Okay, I changed my code but unfortunately I now got the same problem - the state is not updated for the second Widget-Instance (only if the first instance is disposed before!) :(

This is how the code now looks:

Widget that changes the widgets (for "Search via Name" and "Universal Search"):

  @override
  Widget build(BuildContext context) {
    _runsAfterBuild(context, ref);

    final currentApiEndpoint = ref.watch(currentApiEndpointProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text("Search obituaries ..."),
        centerTitle: true,
        actions: [
          IconButton(
              tooltip: "Start search...",
              onPressed: () {
                doSearch(context, ref);
              },
              icon: const Icon(Icons.search)),
        ],
      ),
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Column(
            children: [
              DropdownButtonFormField<ApiEndpoint>(
                  items: dropDownMenuItems,
                  value: currentApiEndpoint,
                  onChanged: (apiEndpoint) {
                    doOnDropDownChanged(context, ref, apiEndpoint);
                  }),
              const SizedBox(
                height: 10,
              ),
              currentApiEndpoint.searchTermInput,
            ],
          ),
        ),
      ),
    );
  }

BaseClass for the ApiEndpoint implementations:

abstract class ApiEndpoint {
  ApiEndpointType get type;
  String get title;
  SearchTermInput get searchTermInput;
  String get searchFunctionName;

  bool validateData(BuildContext context, WidgetRef ref);
  void onSubmit(
      BuildContext context, WidgetRef ref, List<String> searchTermInputContent);
  void onLostFocus(
      BuildContext context, WidgetRef ref, List<String> searchTermInputContent);
}

ApiEndpoint-Class that owns the widget for the first instance of the widget (Search via name):

class ApiEndpointName implements ApiEndpoint {
  final log = getLogger();
  final NotifierProvider<ApiEndpointNameSearchTermInputContent, List<String>>
      apiEndpointSearchTermInputContentProvider =
      apiEndpointNameSearchTermInputContentProvider;

  ApiEndpointName();

  @override
  String get searchFunctionName => "getObituariesByName";

  @override
  SearchTermInput get searchTermInput => SearchTermInputOneString(
        // key: Key("ApiEndpointName"), // UniqueKey(),
        apiEndpointSearchTermInputContentProvider:
            apiEndpointSearchTermInputContentProvider,
        captionList: ["Name"],
        textInputFormatter: [
          FilteringTextInputFormatter.deny(RegExp(r"[0-9]"))
        ],
        onSubmit: onSubmit,
        onLostFocus: onLostFocus,
      );

  @override
  String get title => "... by name";

  @override
  ApiEndpointType get type => ApiEndpointType.name;

  @override
  bool validateData(BuildContext context, WidgetRef ref) {
    var contentList = ref.read(apiEndpointSearchTermInputContentProvider);
    return contentList.isNotEmpty && contentList[0].isNotEmpty;
  }

  @override
  void onSubmit(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    // set values
    ref
        .read(currentSearchTermInputContentProvider.notifier)
        .setValue(searchTermInputContent);
    ref
        .read(apiEndpointSearchTermInputContentProvider.notifier)
        .setValue(searchTermInputContent);

    // start search
    var doSearch = ref.read(doSearchFunctionProvider);
    if (doSearch != null) {
      doSearch(context, ref);
    } else {
      log.d("no doSearch method found!");
    }
  }

  @override
  void onLostFocus(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    // set values
    ref
        .read(currentSearchTermInputContentProvider.notifier)
        .setValue(searchTermInputContent);
    ref
        .read(apiEndpointSearchTermInputContentProvider.notifier)
        .setValue(searchTermInputContent);
  }
}

ApiEndpoint-Class that owns the widget for the second instance of the widget (Universal search):

class ApiEndpointUniversal implements ApiEndpoint {
  final log = getLogger();
  final NotifierProvider<ApiEndpointUniversalSearchTermInputContent,
          List<String>> apiEndpointSearchTermInputContentProvider =
      apiEndpointUniversalSearchTermInputContentProvider;

  ApiEndpointUniversal();

  @override
  String get searchFunctionName => "getObituariesUniversal";

  @override
  SearchTermInput get searchTermInput => SearchTermInputOneString(
        // key: Key("ApiEndpointUniversal"), // UniqueKey(),
        apiEndpointSearchTermInputContentProvider:
            apiEndpointSearchTermInputContentProvider,
        captionList: ["Statement"],
        textInputFormatter: [FilteringTextInputFormatter.allow(RegExp(r".*"))],
        onSubmit: onSubmit,
        onLostFocus: onLostFocus,
      );

  @override
  String get title => "... universal";

  @override
  ApiEndpointType get type => ApiEndpointType.universal;

  @override
  bool validateData(BuildContext context, WidgetRef ref) {
    var contentList = ref.read(apiEndpointSearchTermInputContentProvider);
    return contentList.isNotEmpty && contentList[0].isNotEmpty;
  }

  @override
  void onSubmit(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    // set values
    ref
        .read(currentSearchTermInputContentProvider.notifier)
        .setValue(searchTermInputContent);
    ref
        .read(apiEndpointSearchTermInputContentProvider.notifier)
        .setValue(searchTermInputContent);

    // start search
    var doSearch = ref.read(doSearchFunctionProvider);
    if (doSearch != null) {
      doSearch(context, ref);
    } else {
      log.d("no doSearch method found!");
    }
  }

  @override
  void onLostFocus(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    // set values
    ref
        .read(currentSearchTermInputContentProvider.notifier)
        .setValue(searchTermInputContent);
    ref
        .read(apiEndpointSearchTermInputContentProvider.notifier)
        .setValue(searchTermInputContent);
  }
}

BaseClass of the Widget:

abstract class SearchTermInput extends ConsumerStatefulWidget {
  const SearchTermInput({super.key});

  NotifierProvider? get apiEndpointSearchTermInputContentProvider;
  List<String>? get captionList;
  List<TextInputFormatter>? get textInputFormatter;
  void Function(BuildContext, WidgetRef, List<String>) get onSubmit;
  void Function(BuildContext, WidgetRef, List<String>) get onLostFocus;
}

Widget implementation for ApiEndpointName and ApiEndpointUniversal:

class SearchTermInputOneString extends SearchTermInput {
  @override
  final NotifierProvider apiEndpointSearchTermInputContentProvider;

  @override
  final List<String> captionList;

  @override
  final List<TextInputFormatter>? textInputFormatter;

  @override
  final void Function(BuildContext, WidgetRef, List<String>) onSubmit;

  @override
  final void Function(BuildContext, WidgetRef, List<String>) onLostFocus;

  const SearchTermInputOneString({
    super.key,
    required this.apiEndpointSearchTermInputContentProvider,
    required this.captionList,
    required this.textInputFormatter,
    required this.onSubmit,
    required this.onLostFocus,
  });

  @override
  // ignore: no_logic_in_create_state - this is temporarily for for logging!
  ConsumerState<ConsumerStatefulWidget> createState() {
    final log = getLogger();
    log.t("createState");
    return _SearchTermInputOneStringState();
  }
}

class _SearchTermInputOneStringState
    extends ConsumerState<SearchTermInputOneString> {
  late TextEditingController textEditingController;
  late List<String> currentContent;

  @override
  void initState() {
    super.initState();
    final log = getLogger();
    log.t("initstate");
    textEditingController = TextEditingController();
  }

  @override
  void didChangeDependencies() {
    _runsAfterInit(context, ref);
    final log = getLogger();
    log.t("didChangeDependencies");

    // read apiEndpoint content
    currentContent = ref
        .read(widget.apiEndpointSearchTermInputContentProvider)
        .cast<String>();
    textEditingController.text =
        currentContent.isNotEmpty ? currentContent[0] : "";

    super.didChangeDependencies();
  }

  @override
  void dispose() {
    textEditingController.dispose();
    final log = getLogger();
    log.t("dispose");
    super.dispose();
  }

  Future<void> _runsAfterInit(BuildContext context, WidgetRef ref) async {
    await Future.delayed(Duration.zero); // <-- Add a 0 dummy waiting time

    final log = getLogger();
    log.t("_runsAfterInit");

    // set current content
    ref
        .read(currentSearchTermInputContentProvider.notifier)
        .setValue(currentContent);
  }

  @override
  Widget build(BuildContext context) {
    return Focus(
      child: TextFormField(
        controller: textEditingController,
        decoration: InputDecoration(
          border: const OutlineInputBorder(),
          labelText: widget.captionList.isNotEmpty ? widget.captionList[0] : "",
          suffixIcon: IconButton(
            onPressed: () {
              textEditingController.text = "";
            },
            icon: Icon(Icons.clear),
          ),
        ),
        inputFormatters: widget.textInputFormatter,
        onFieldSubmitted: (_) {
          widget.onSubmit(context, ref, [textEditingController.text]);
        },
      ),
      onFocusChange: (hasFocus) {
        if (!hasFocus) {
          widget.onLostFocus(context, ref, [textEditingController.text]);
        }
      },
    );
  }
}

Logoutput after the first instance has been created:

────────────────────────────────────────────────────────────────────────────────────────
#0   SearchTermInputOneString.createState (package:obituary_viewer/features/obituaries/presentation/widgets/search_term_inputs/search_term_input_one_string.dart:39:9)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2024-11-18 08:26:03.262
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 createState
────────────────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────────────────
#0   _SearchTermInputOneStringState.initState (package:obituary_viewer/features/obituaries/presentation/widgets/search_term_inputs/search_term_input_one_string.dart:53:9)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2024-11-18 08:26:03.269
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 initstate
────────────────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────────────────
#0   _SearchTermInputOneStringState.didChangeDependencies (package:obituary_viewer/features/obituaries/presentation/widgets/search_term_inputs/search_term_input_one_string.dart:61:9)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2024-11-18 08:26:03.272
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 didChangeDependencies
────────────────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────────────────
#0   ApiEndpointNameSearchTermInputContent.build (package:obituary_viewer/features/obituaries/presentation/provider/search_term_input_content_provider.dart:26:9)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2024-11-18 08:26:03.275
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 ApiEndpointNameSearchTermInputContent provider build
────────────────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────────────────
#0   DoSearchFunction.build (package:obituary_viewer/features/obituaries/presentation/provider/search_screen_provider.dart:13:9)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2024-11-18 08:26:03.402
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 DoSearchFunction provider build
────────────────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────────────────
#0   DoSearchFunction.setValue (package:obituary_viewer/features/obituaries/presentation/provider/search_screen_provider.dart:19:9)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2024-11-18 08:26:03.404
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 DoSearchFunction set to Closure: (BuildContext, WidgetRef) => void from Function 'doSearch':.
────────────────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────────────────
#0   _SearchTermInputOneStringState._runsAfterInit (package:obituary_viewer/features/obituaries/presentation/widgets/search_term_inputs/search_term_input_one_string.dart:85:9)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2024-11-18 08:26:03.405
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 _runsAfterInit
────────────────────────────────────────────────────────────────────────────────────────

Logoutput after the second instance:

> flutter:
> ┌─────────────────────────────────────────────────────────────────────────────────────────
> flutter: │ #0   CurrentApiEndpoint.setValue
> (package:obituary_viewer/features/obituaries/presentation/provider/api_endpoint_provider.dart:30:9)
> flutter:
> ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
> flutter: │ 2024-11-18 08:27:41.773 flutter:
> ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
> flutter: │  CurrentApiEndpoints set to Instance of
> 'ApiEndpointUniversal' flutter:
> └─────────────────────────────────────────────────────────────────────────────────────────

That's it - no create, no initState (what seems to be correct after deleting the unique key), no didChangeDependencies :( - and the text of the texteditingcontroller is still the one of the first instance :(

I am at the very beginning of my “flutter career” and don't want to learn anything wrong. I am therefore very grateful for your advice!

Edit: solved by adding a unique key to the widget constructor call:

Edit 2: as Randal Schwartz mentioned -> this is not a solution. Issue NOT solved and still open.

  @override
  SearchTermInput get searchTermInput => SearchTermInputOneString(
        key: UniqueKey(),
        currentSearchTermInputContent:
            widgetRef.read(apiEndpointUniversalSearchTermInputContentProvider),
        captionList: ["Statement"],
        // todo : allow umlauts! or better deny numbers?
        inputFormatterAllow: RegExp(".*"),
        onSubmit: onSubmit,
        onLostFocus: onLostFocus,
      );

I have an abstract class (ApiEndpoint) with two implementations (ApiEndpointName, ApiEndpointUniversal).

Both implementations have a StatefulWidget (SearchTermInputOneString) inherited from the same base class (SearchTermInput).

For both ApiEndpoint-objects the Widget part of SearchTermInputOneStringis created separately - what is correct. My problem is, that the State-part of SearchTermInputOneString is created only once - so ApiEndpointName and ApiEndpointUniversal own the same state.

I have to read ApiEndpoint-specific configuration during initState - that does not work because initState is called only for the first ApiEndpoint-instance :(

Any ideas how to solve that?

abstract class ApiEndpoint:

enum ApiEndpointType {
  today,
  unseen,
  universal,
  name,
  exactAge,
  ageBetween,
  ageGreaterThanOrEqual,
  ageLessThanOrEqual
}

abstract class ApiEndpoint {
  ApiEndpointType get type;
  String get title;
  SearchTermInput get searchTermInput;
  String get searchFunctionName;

  bool validateData(BuildContext context, WidgetRef ref);
  void onSubmit(
      BuildContext context, WidgetRef ref, List<String> searchTermInputContent);
  void onLostFocus(
      BuildContext context, WidgetRef ref, List<String> searchTermInputContent);
}

ApiEndpointName implementation:

class ApiEndpointName implements ApiEndpoint {
  final WidgetRef widgetRef;

  ApiEndpointName({required this.widgetRef});

  @override
  String get searchFunctionName => "getObituariesByName";

  @override
  SearchTermInput get searchTermInput => SearchTermInputOneString(
        currentSearchTermInputContent:
            widgetRef.read(apiEndpointNameSearchTermInputContentProvider),
        captionList: ["Name"],
        inputFormatterAllow: RegExp("[a-zA-Z% ]"),
        onSubmit: onSubmit,
        onLostFocus: onLostFocus,
      );

  @override
  String get title => "... by name";

  @override
  ApiEndpointType get type => ApiEndpointType.name;

  @override
  bool validateData(BuildContext context, WidgetRef ref) {
    ...
  }

  @override
  void onSubmit(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    ...
  }

  @override
  void onLostFocus(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    ...
  }

ApiEndpointUniversal implementation:

class ApiEndpointUniversal implements ApiEndpoint {
  final WidgetRef widgetRef;

  ApiEndpointUniversal({required this.widgetRef});

  @override
  String get searchFunctionName => "getObituariesUniversal";

  @override
  SearchTermInput get searchTermInput => SearchTermInputOneString(
        currentSearchTermInputContent:
            widgetRef.read(apiEndpointUniversalSearchTermInputContentProvider),
        captionList: ["Statement"],
        inputFormatterAllow: RegExp(".*"),
        onSubmit: onSubmit,
        onLostFocus: onLostFocus,
      );

  @override
  String get title => "... universal";

  @override
  ApiEndpointType get type => ApiEndpointType.universal;

  @override
  bool validateData(BuildContext context, WidgetRef ref) {
    ...
  }

  @override
  void onSubmit(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    ...
  }

  @override
  void onLostFocus(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    ...
  }
}

abstract StatefulWidget:

abstract class SearchTermInput extends ConsumerStatefulWidget {
  const SearchTermInput({super.key});

  List<String> get currentSearchTermInputContent;
  List<String>? get captionList;
  RegExp get inputFormatterAllow;
  void Function(BuildContext, WidgetRef, List<String>) get onSubmit;
  void Function(BuildContext, WidgetRef, List<String>) get onLostFocus;
}

Implementation:

class SearchTermInputOneString extends SearchTermInput {
  @override
  final List<String> currentSearchTermInputContent;

  @override
  final List<String> captionList;

  @override
  final RegExp inputFormatterAllow;

  @override
  final void Function(BuildContext, WidgetRef, List<String>) onSubmit;

  @override
  final void Function(BuildContext, WidgetRef, List<String>) onLostFocus;

  const SearchTermInputOneString({
    super.key,
    required this.currentSearchTermInputContent,
    required this.captionList,
    required this.inputFormatterAllow,
    required this.onSubmit,
    required this.onLostFocus,
  });

  @override
  ConsumerState<ConsumerStatefulWidget> createState() =>
      _SearchTermInputOneStringState();
}

class _SearchTermInputOneStringState
    extends ConsumerState<SearchTermInputOneString> {
  late TextEditingController textEditingController;

  @override
  void initState() {
    super.initState();
    textEditingController = TextEditingController();
    textEditingController.text = widget.currentSearchTermInputContent.isNotEmpty
        ? widget.currentSearchTermInputContent[0]
        : "";
  }

  @override
  void dispose() {
    textEditingController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Focus(
      child: TextFormField(
        controller: textEditingController,
        decoration: InputDecoration(
          border: const OutlineInputBorder(),
          labelText: widget.captionList.isNotEmpty ? widget.captionList[0] : "",
          suffixIcon: IconButton(
            onPressed: () {
              textEditingController.text = "";
            },
            icon: Icon(Icons.clear),
          ),
        ),
        inputFormatters: [
          FilteringTextInputFormatter.allow(widget.inputFormatterAllow)
        ],
        onFieldSubmitted: (_) {
          widget.onSubmit(context, ref, [textEditingController.text]);
        },
      ),
      onFocusChange: (hasFocus) {
        if (!hasFocus) {
          widget.onLostFocus(context, ref, [textEditingController.text]);
        }
      },
    );
  }
}

I create the List of ApiEndpoints this way:

List<ApiEndpoint> getApiEndPoints({required WidgetRef widgetRef}) {
  final List<ApiEndpoint> apiEndpoints = [
    ApiEndpointName(widgetRef: widgetRef),
    ApiEndpointUniversal(widgetRef: widgetRef),
  ];

  return apiEndpoints;
}

Edit: solved by adding a unique key to the widget constructor call:

Edit 2: as Randal Schwartz mentioned -> this is not a solution. Issue NOT solved and still open.

  @override
  SearchTermInput get searchTermInput => SearchTermInputOneString(
        key: UniqueKey(),
        currentSearchTermInputContent:
            widgetRef.read(apiEndpointUniversalSearchTermInputContentProvider),
        captionList: ["Statement"],
        // todo : allow umlauts! or better deny numbers?
        inputFormatterAllow: RegExp(".*"),
        onSubmit: onSubmit,
        onLostFocus: onLostFocus,
      );

I have an abstract class (ApiEndpoint) with two implementations (ApiEndpointName, ApiEndpointUniversal).

Both implementations have a StatefulWidget (SearchTermInputOneString) inherited from the same base class (SearchTermInput).

For both ApiEndpoint-objects the Widget part of SearchTermInputOneStringis created separately - what is correct. My problem is, that the State-part of SearchTermInputOneString is created only once - so ApiEndpointName and ApiEndpointUniversal own the same state.

I have to read ApiEndpoint-specific configuration during initState - that does not work because initState is called only for the first ApiEndpoint-instance :(

Any ideas how to solve that?

abstract class ApiEndpoint:

enum ApiEndpointType {
  today,
  unseen,
  universal,
  name,
  exactAge,
  ageBetween,
  ageGreaterThanOrEqual,
  ageLessThanOrEqual
}

abstract class ApiEndpoint {
  ApiEndpointType get type;
  String get title;
  SearchTermInput get searchTermInput;
  String get searchFunctionName;

  bool validateData(BuildContext context, WidgetRef ref);
  void onSubmit(
      BuildContext context, WidgetRef ref, List<String> searchTermInputContent);
  void onLostFocus(
      BuildContext context, WidgetRef ref, List<String> searchTermInputContent);
}

ApiEndpointName implementation:

class ApiEndpointName implements ApiEndpoint {
  final WidgetRef widgetRef;

  ApiEndpointName({required this.widgetRef});

  @override
  String get searchFunctionName => "getObituariesByName";

  @override
  SearchTermInput get searchTermInput => SearchTermInputOneString(
        currentSearchTermInputContent:
            widgetRef.read(apiEndpointNameSearchTermInputContentProvider),
        captionList: ["Name"],
        inputFormatterAllow: RegExp("[a-zA-Z% ]"),
        onSubmit: onSubmit,
        onLostFocus: onLostFocus,
      );

  @override
  String get title => "... by name";

  @override
  ApiEndpointType get type => ApiEndpointType.name;

  @override
  bool validateData(BuildContext context, WidgetRef ref) {
    ...
  }

  @override
  void onSubmit(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    ...
  }

  @override
  void onLostFocus(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    ...
  }

ApiEndpointUniversal implementation:

class ApiEndpointUniversal implements ApiEndpoint {
  final WidgetRef widgetRef;

  ApiEndpointUniversal({required this.widgetRef});

  @override
  String get searchFunctionName => "getObituariesUniversal";

  @override
  SearchTermInput get searchTermInput => SearchTermInputOneString(
        currentSearchTermInputContent:
            widgetRef.read(apiEndpointUniversalSearchTermInputContentProvider),
        captionList: ["Statement"],
        inputFormatterAllow: RegExp(".*"),
        onSubmit: onSubmit,
        onLostFocus: onLostFocus,
      );

  @override
  String get title => "... universal";

  @override
  ApiEndpointType get type => ApiEndpointType.universal;

  @override
  bool validateData(BuildContext context, WidgetRef ref) {
    ...
  }

  @override
  void onSubmit(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    ...
  }

  @override
  void onLostFocus(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    ...
  }
}

abstract StatefulWidget:

abstract class SearchTermInput extends ConsumerStatefulWidget {
  const SearchTermInput({super.key});

  List<String> get currentSearchTermInputContent;
  List<String>? get captionList;
  RegExp get inputFormatterAllow;
  void Function(BuildContext, WidgetRef, List<String>) get onSubmit;
  void Function(BuildContext, WidgetRef, List<String>) get onLostFocus;
}

Implementation:

class SearchTermInputOneString extends SearchTermInput {
  @override
  final List<String> currentSearchTermInputContent;

  @override
  final List<String> captionList;

  @override
  final RegExp inputFormatterAllow;

  @override
  final void Function(BuildContext, WidgetRef, List<String>) onSubmit;

  @override
  final void Function(BuildContext, WidgetRef, List<String>) onLostFocus;

  const SearchTermInputOneString({
    super.key,
    required this.currentSearchTermInputContent,
    required this.captionList,
    required this.inputFormatterAllow,
    required this.onSubmit,
    required this.onLostFocus,
  });

  @override
  ConsumerState<ConsumerStatefulWidget> createState() =>
      _SearchTermInputOneStringState();
}

class _SearchTermInputOneStringState
    extends ConsumerState<SearchTermInputOneString> {
  late TextEditingController textEditingController;

  @override
  void initState() {
    super.initState();
    textEditingController = TextEditingController();
    textEditingController.text = widget.currentSearchTermInputContent.isNotEmpty
        ? widget.currentSearchTermInputContent[0]
        : "";
  }

  @override
  void dispose() {
    textEditingController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Focus(
      child: TextFormField(
        controller: textEditingController,
        decoration: InputDecoration(
          border: const OutlineInputBorder(),
          labelText: widget.captionList.isNotEmpty ? widget.captionList[0] : "",
          suffixIcon: IconButton(
            onPressed: () {
              textEditingController.text = "";
            },
            icon: Icon(Icons.clear),
          ),
        ),
        inputFormatters: [
          FilteringTextInputFormatter.allow(widget.inputFormatterAllow)
        ],
        onFieldSubmitted: (_) {
          widget.onSubmit(context, ref, [textEditingController.text]);
        },
      ),
      onFocusChange: (hasFocus) {
        if (!hasFocus) {
          widget.onLostFocus(context, ref, [textEditingController.text]);
        }
      },
    );
  }
}

I create the List of ApiEndpoints this way:

List<ApiEndpoint> getApiEndPoints({required WidgetRef widgetRef}) {
  final List<ApiEndpoint> apiEndpoints = [
    ApiEndpointName(widgetRef: widgetRef),
    ApiEndpointUniversal(widgetRef: widgetRef),
  ];

  return apiEndpoints;
}
Share Improve this question edited Nov 18, 2024 at 7:46 user28298940 asked Nov 16, 2024 at 14:22 user28298940user28298940 112 bronze badges 3
  • "key: UniqueKey(),"... WTF. bye bye performance. It's keeping your widget alive by design. and you've just ruined that. You've hooked into the wrong State lifecycle. You probably want api.flutter.dev/flutter/widgets/State/… and not initState. – Randal Schwartz Commented Nov 16, 2024 at 21:58
  • I do not understand :( Everywhere I read "Do your initialization in initState" - what is wrong now? The initialization of the TextController? Or the Key? I read that a WidgetObject gets its own StateObject when I define a unique key for each StatelessWidget. When I do not set the key, neither initState is called nor createState in the WidgetObject - therefore didChangeDependencie would not be called, too - did it? – user28298940 Commented Nov 18, 2024 at 5:36
  • Okay - after thinking about your words a bit longer I suppose to understand what you mean. The widget (also it's state!) should live for the whole lifetime of the app (in best case) and only the state dependencies should be updated if needed. But with setting the unique keys I force destroying and recreating the widget - makes sense. But unfortunately I am not able to get this didChangeDependencies being called when I switch from the "Search by name" to the "Universal search" widget. Please have a look at my code below. – user28298940 Commented Nov 18, 2024 at 8:35
Add a comment  | 

2 Answers 2

Reset to default 1

After playing around a lot of time without getting didChangeDependencies to work I found out that didUpdateWidget did the trick. Do you think that this okay or is there another stumbling block that I overlooked?

  @override
  void didUpdateWidget(covariant SearchTermInputOneString oldWidget) {
    final log = getLogger();
    log.t("didUpdateWidget");

    // read apiEndpoint content
    currentContent = ref
        .read(widget.apiEndpointSearchTermInputContentProvider)
        .cast<String>();
    textEditingController.text =
        currentContent.isNotEmpty ? currentContent[0] : "";

    super.didUpdateWidget(oldWidget);
  }

Okay, I changed my code but unfortunately I now got the same problem - the state is not updated for the second Widget-Instance (only if the first instance is disposed before!) :(

This is how the code now looks:

Widget that changes the widgets (for "Search via Name" and "Universal Search"):

  @override
  Widget build(BuildContext context) {
    _runsAfterBuild(context, ref);

    final currentApiEndpoint = ref.watch(currentApiEndpointProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text("Search obituaries ..."),
        centerTitle: true,
        actions: [
          IconButton(
              tooltip: "Start search...",
              onPressed: () {
                doSearch(context, ref);
              },
              icon: const Icon(Icons.search)),
        ],
      ),
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Column(
            children: [
              DropdownButtonFormField<ApiEndpoint>(
                  items: dropDownMenuItems,
                  value: currentApiEndpoint,
                  onChanged: (apiEndpoint) {
                    doOnDropDownChanged(context, ref, apiEndpoint);
                  }),
              const SizedBox(
                height: 10,
              ),
              currentApiEndpoint.searchTermInput,
            ],
          ),
        ),
      ),
    );
  }

BaseClass for the ApiEndpoint implementations:

abstract class ApiEndpoint {
  ApiEndpointType get type;
  String get title;
  SearchTermInput get searchTermInput;
  String get searchFunctionName;

  bool validateData(BuildContext context, WidgetRef ref);
  void onSubmit(
      BuildContext context, WidgetRef ref, List<String> searchTermInputContent);
  void onLostFocus(
      BuildContext context, WidgetRef ref, List<String> searchTermInputContent);
}

ApiEndpoint-Class that owns the widget for the first instance of the widget (Search via name):

class ApiEndpointName implements ApiEndpoint {
  final log = getLogger();
  final NotifierProvider<ApiEndpointNameSearchTermInputContent, List<String>>
      apiEndpointSearchTermInputContentProvider =
      apiEndpointNameSearchTermInputContentProvider;

  ApiEndpointName();

  @override
  String get searchFunctionName => "getObituariesByName";

  @override
  SearchTermInput get searchTermInput => SearchTermInputOneString(
        // key: Key("ApiEndpointName"), // UniqueKey(),
        apiEndpointSearchTermInputContentProvider:
            apiEndpointSearchTermInputContentProvider,
        captionList: ["Name"],
        textInputFormatter: [
          FilteringTextInputFormatter.deny(RegExp(r"[0-9]"))
        ],
        onSubmit: onSubmit,
        onLostFocus: onLostFocus,
      );

  @override
  String get title => "... by name";

  @override
  ApiEndpointType get type => ApiEndpointType.name;

  @override
  bool validateData(BuildContext context, WidgetRef ref) {
    var contentList = ref.read(apiEndpointSearchTermInputContentProvider);
    return contentList.isNotEmpty && contentList[0].isNotEmpty;
  }

  @override
  void onSubmit(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    // set values
    ref
        .read(currentSearchTermInputContentProvider.notifier)
        .setValue(searchTermInputContent);
    ref
        .read(apiEndpointSearchTermInputContentProvider.notifier)
        .setValue(searchTermInputContent);

    // start search
    var doSearch = ref.read(doSearchFunctionProvider);
    if (doSearch != null) {
      doSearch(context, ref);
    } else {
      log.d("no doSearch method found!");
    }
  }

  @override
  void onLostFocus(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    // set values
    ref
        .read(currentSearchTermInputContentProvider.notifier)
        .setValue(searchTermInputContent);
    ref
        .read(apiEndpointSearchTermInputContentProvider.notifier)
        .setValue(searchTermInputContent);
  }
}

ApiEndpoint-Class that owns the widget for the second instance of the widget (Universal search):

class ApiEndpointUniversal implements ApiEndpoint {
  final log = getLogger();
  final NotifierProvider<ApiEndpointUniversalSearchTermInputContent,
          List<String>> apiEndpointSearchTermInputContentProvider =
      apiEndpointUniversalSearchTermInputContentProvider;

  ApiEndpointUniversal();

  @override
  String get searchFunctionName => "getObituariesUniversal";

  @override
  SearchTermInput get searchTermInput => SearchTermInputOneString(
        // key: Key("ApiEndpointUniversal"), // UniqueKey(),
        apiEndpointSearchTermInputContentProvider:
            apiEndpointSearchTermInputContentProvider,
        captionList: ["Statement"],
        textInputFormatter: [FilteringTextInputFormatter.allow(RegExp(r".*"))],
        onSubmit: onSubmit,
        onLostFocus: onLostFocus,
      );

  @override
  String get title => "... universal";

  @override
  ApiEndpointType get type => ApiEndpointType.universal;

  @override
  bool validateData(BuildContext context, WidgetRef ref) {
    var contentList = ref.read(apiEndpointSearchTermInputContentProvider);
    return contentList.isNotEmpty && contentList[0].isNotEmpty;
  }

  @override
  void onSubmit(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    // set values
    ref
        .read(currentSearchTermInputContentProvider.notifier)
        .setValue(searchTermInputContent);
    ref
        .read(apiEndpointSearchTermInputContentProvider.notifier)
        .setValue(searchTermInputContent);

    // start search
    var doSearch = ref.read(doSearchFunctionProvider);
    if (doSearch != null) {
      doSearch(context, ref);
    } else {
      log.d("no doSearch method found!");
    }
  }

  @override
  void onLostFocus(BuildContext context, WidgetRef ref,
      List<String> searchTermInputContent) {
    // set values
    ref
        .read(currentSearchTermInputContentProvider.notifier)
        .setValue(searchTermInputContent);
    ref
        .read(apiEndpointSearchTermInputContentProvider.notifier)
        .setValue(searchTermInputContent);
  }
}

BaseClass of the Widget:

abstract class SearchTermInput extends ConsumerStatefulWidget {
  const SearchTermInput({super.key});

  NotifierProvider? get apiEndpointSearchTermInputContentProvider;
  List<String>? get captionList;
  List<TextInputFormatter>? get textInputFormatter;
  void Function(BuildContext, WidgetRef, List<String>) get onSubmit;
  void Function(BuildContext, WidgetRef, List<String>) get onLostFocus;
}

Widget implementation for ApiEndpointName and ApiEndpointUniversal:

class SearchTermInputOneString extends SearchTermInput {
  @override
  final NotifierProvider apiEndpointSearchTermInputContentProvider;

  @override
  final List<String> captionList;

  @override
  final List<TextInputFormatter>? textInputFormatter;

  @override
  final void Function(BuildContext, WidgetRef, List<String>) onSubmit;

  @override
  final void Function(BuildContext, WidgetRef, List<String>) onLostFocus;

  const SearchTermInputOneString({
    super.key,
    required this.apiEndpointSearchTermInputContentProvider,
    required this.captionList,
    required this.textInputFormatter,
    required this.onSubmit,
    required this.onLostFocus,
  });

  @override
  // ignore: no_logic_in_create_state - this is temporarily for for logging!
  ConsumerState<ConsumerStatefulWidget> createState() {
    final log = getLogger();
    log.t("createState");
    return _SearchTermInputOneStringState();
  }
}

class _SearchTermInputOneStringState
    extends ConsumerState<SearchTermInputOneString> {
  late TextEditingController textEditingController;
  late List<String> currentContent;

  @override
  void initState() {
    super.initState();
    final log = getLogger();
    log.t("initstate");
    textEditingController = TextEditingController();
  }

  @override
  void didChangeDependencies() {
    _runsAfterInit(context, ref);
    final log = getLogger();
    log.t("didChangeDependencies");

    // read apiEndpoint content
    currentContent = ref
        .read(widget.apiEndpointSearchTermInputContentProvider)
        .cast<String>();
    textEditingController.text =
        currentContent.isNotEmpty ? currentContent[0] : "";

    super.didChangeDependencies();
  }

  @override
  void dispose() {
    textEditingController.dispose();
    final log = getLogger();
    log.t("dispose");
    super.dispose();
  }

  Future<void> _runsAfterInit(BuildContext context, WidgetRef ref) async {
    await Future.delayed(Duration.zero); // <-- Add a 0 dummy waiting time

    final log = getLogger();
    log.t("_runsAfterInit");

    // set current content
    ref
        .read(currentSearchTermInputContentProvider.notifier)
        .setValue(currentContent);
  }

  @override
  Widget build(BuildContext context) {
    return Focus(
      child: TextFormField(
        controller: textEditingController,
        decoration: InputDecoration(
          border: const OutlineInputBorder(),
          labelText: widget.captionList.isNotEmpty ? widget.captionList[0] : "",
          suffixIcon: IconButton(
            onPressed: () {
              textEditingController.text = "";
            },
            icon: Icon(Icons.clear),
          ),
        ),
        inputFormatters: widget.textInputFormatter,
        onFieldSubmitted: (_) {
          widget.onSubmit(context, ref, [textEditingController.text]);
        },
      ),
      onFocusChange: (hasFocus) {
        if (!hasFocus) {
          widget.onLostFocus(context, ref, [textEditingController.text]);
        }
      },
    );
  }
}

Logoutput after the first instance has been created:

────────────────────────────────────────────────────────────────────────────────────────
#0   SearchTermInputOneString.createState (package:obituary_viewer/features/obituaries/presentation/widgets/search_term_inputs/search_term_input_one_string.dart:39:9)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2024-11-18 08:26:03.262
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 createState
────────────────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────────────────
#0   _SearchTermInputOneStringState.initState (package:obituary_viewer/features/obituaries/presentation/widgets/search_term_inputs/search_term_input_one_string.dart:53:9)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2024-11-18 08:26:03.269
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 initstate
────────────────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────────────────
#0   _SearchTermInputOneStringState.didChangeDependencies (package:obituary_viewer/features/obituaries/presentation/widgets/search_term_inputs/search_term_input_one_string.dart:61:9)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2024-11-18 08:26:03.272
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 didChangeDependencies
────────────────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────────────────
#0   ApiEndpointNameSearchTermInputContent.build (package:obituary_viewer/features/obituaries/presentation/provider/search_term_input_content_provider.dart:26:9)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2024-11-18 08:26:03.275
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 ApiEndpointNameSearchTermInputContent provider build
────────────────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────────────────
#0   DoSearchFunction.build (package:obituary_viewer/features/obituaries/presentation/provider/search_screen_provider.dart:13:9)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2024-11-18 08:26:03.402
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 DoSearchFunction provider build
────────────────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────────────────
#0   DoSearchFunction.setValue (package:obituary_viewer/features/obituaries/presentation/provider/search_screen_provider.dart:19:9)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2024-11-18 08:26:03.404
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 DoSearchFunction set to Closure: (BuildContext, WidgetRef) => void from Function 'doSearch':.
────────────────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────────────────
#0   _SearchTermInputOneStringState._runsAfterInit (package:obituary_viewer/features/obituaries/presentation/widgets/search_term_inputs/search_term_input_one_string.dart:85:9)
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2024-11-18 08:26:03.405
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 _runsAfterInit
────────────────────────────────────────────────────────────────────────────────────────

Logoutput after the second instance:

> flutter:
> ┌─────────────────────────────────────────────────────────────────────────────────────────
> flutter: │ #0   CurrentApiEndpoint.setValue
> (package:obituary_viewer/features/obituaries/presentation/provider/api_endpoint_provider.dart:30:9)
> flutter:
> ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
> flutter: │ 2024-11-18 08:27:41.773 flutter:
> ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
> flutter: │  CurrentApiEndpoints set to Instance of
> 'ApiEndpointUniversal' flutter:
> └─────────────────────────────────────────────────────────────────────────────────────────

That's it - no create, no initState (what seems to be correct after deleting the unique key), no didChangeDependencies :( - and the text of the texteditingcontroller is still the one of the first instance :(

I am at the very beginning of my “flutter career” and don't want to learn anything wrong. I am therefore very grateful for your advice!

本文标签: dartFlutterforce createState() for inherited StatefulWidgetStack Overflow