rrousselGit / flutter_hooks

React hooks for Flutter. Hooks are a new kind of object that manages a Widget life-cycles. They are used to increase code sharing between widgets and as a complete replacement for StatefulWidget.
MIT License
3.13k stars 179 forks source link

Assertion Error with useExpansionTileController #414

Closed kovaccc closed 8 months ago

kovaccc commented 8 months ago

An assertion error is occurring in the CustomExpansionTile widget when using the useExpansionTileController hook from the flutter_hooks package. The issue seems to manifest when accessing the isExpanded property of ExpansionTileController. The assertion error suggests that the controller's state is null at the time of access.

Steps to reproduce the behavior:

  1. Create a CustomExpansionTile widget as a HookWidget.
  2. Use useExpansionTileController to get an instance of ExpansionTileController.
  3. Try to access expansionTileController.isExpanded within the widget's build method.
  4. See error on runtime.
class CustomExpansionTile extends HookWidget {
  // ... [other properties]

  @override
  Widget build(BuildContext context) {
    final expansionTileController = useExpansionTileController();
    return Column(
      // ... [other widget code]
      child: ExpansionTile(
        controller: expansionTileController,
        // ... [other properties]
        trailing: expansionTileController.isExpanded
            ? Assets.icons.minus.svg()
            : Assets.icons.plus.svg(),
        // ... [other properties]
      ),
    );
  }
}

Error message:

======== Exception caught by widgets library =======================================================
The following assertion was thrown building CustomExpansionTile:
'package:flutter/src/material/expansion_tile.dart': Failed assertion: line 49 pos 12: '_state != null': is not true.

The expected behavior is that the CustomExpansionTile should correctly show the expanded or collapsed state based on the isExpanded property of the ExpansionTileController without causing any runtime errors or assertions. The useExpansionTileController hook should properly initialize and manage the state of the ExpansionTileController.

rrousselGit commented 8 months ago

The issue is most definitely unrelated to hooks. The controller was correctly created, and the error isn't thrown by flutter_hooks but Flutter.
My guess is that you're trying to use the controller before it is associated with a widget, and therefore trigger this assert.

Also note that your controller isn't listened. So your UI won't update when the value changes.

Two things:

kovaccc commented 8 months ago

Firstly, thank you for the prompt response and your suggestions. I understand your point that the issue might be due to the ExpansionTileController being used before it's associated with a widget and the controller not being listened to.

Following your advice, I attempted to use HookBuilder along with useListenable for the ExpansionTileController. However, I've run into a type inference issue. When I use useListenable with ExpansionTileController, I encounter the following error:

Couldn't infer type parameter 'T'.
Tried to infer 'ExpansionTileController' for 'T' which doesn't work:
  Type parameter 'T' is declared to extend 'Listenable?' producing 'Listenable?'.
  The type 'ExpansionTileController' was inferred from:
  Parameter 'listenable' declared as 'T' but argument is 'ExpansionTileController'.
Consider passing explicit type argument(s) to the generic.

This seems to suggest a type mismatch between ExpansionTileController and Listenable?, which useListenable expects.

Is there a workaround or a different approach you would recommend for ensuring that the ExpansionTileController is correctly used and listened to within the context of flutter_hooks? Any further guidance would be greatly appreciated. Thank you for your time and help.

rrousselGit commented 8 months ago

My bad, it's looking like ExpansionTileController isn't a Listenable.

Looks like you have to use ExpansionTileController.of instead;

leading: Builder(
  builder: (context) {
    final controller = ExpansionTileController.of(context);
    return  expansionTileController.isExpanded
          ? Assets.icons.minus.svg()
          : Assets.icons.plus.svg()
  },
kovaccc commented 8 months ago

No worries. I tried do it your way, it does not throw the error but also not changing the icon

class CustomExpansionTile extends HookWidget {
  final String title;
  final List<Widget> children;
  final EdgeInsets? tilePadding;
  final bool showDivider;

  const CustomExpansionTile({
    required this.title,
    required this.children,
    this.tilePadding,
    this.showDivider = true,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    final expansionTileController = useExpansionTileController();
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Theme(
          data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
          child: ExpansionTile(
            controller: expansionTileController,
            tilePadding: tilePadding,
            expandedAlignment: Alignment.topLeft,
            expandedCrossAxisAlignment: CrossAxisAlignment.start,
            trailing: HookBuilder(
              builder: (context) {
                return expansionTileController.isExpanded
                    ? Assets.icons.minus.svg()
                    : Assets.icons.plus.svg();
              },
            ),
            title: Text(
              title,
              style: context.appTextStyles.paragraph2SemiBold,
            ),
            children: children,
          ),
        ),
        if (showDivider) const CustomDivider(),
      ],
    );
  }
}

I will stick to useState instead.

  @override
  Widget build(BuildContext context) {
    final isExpanded = useState(false);
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Theme(
          data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
          child: ExpansionTile(
            tilePadding: tilePadding,
            expandedAlignment: Alignment.topLeft,
            expandedCrossAxisAlignment: CrossAxisAlignment.start,
            trailing: isExpanded.value
                ? Assets.icons.minus.svg()
                : Assets.icons.plus.svg(),
            title: Text(
              title,
              style: context.appTextStyles.paragraph2SemiBold,
            ),
            onExpansionChanged: (bool expanded) => isExpanded.value = expanded,
            children: children,
          ),
        ),
        if (showDivider) const CustomDivider(),
      ],
    );
  }
rrousselGit commented 8 months ago

It's ultimately still unrelated to flutter_hooks. So I'd redirect you to Reddit/StackOverflow/Discord for further help requests :)