Force a widget tree build using a Hook Widget in Flutter

Issue

I have a page that dynamically accepts a future list and a callback to get the future list to receive data and be able to refresh it through on refresh. a simplified version looks like this:

class ListSearchPage<T> extends StatefulWidget {

  final Future<List<T>> itemsFuture;
  final ValueGetter<Future<List<T>>> getItemsFuture;

  const ListSearchPage({Key key, this.getItemsFuture, this.itemsFuture})
      : super(key: key);

  @override
  _ListSearchPageState createState() => _ListSearchPageState();
}


class _ListSearchPageState<T> extends State<ListSearchPage> {
  Future<List<T>> itemsFuture;
  TextEditingController _controller;

  @override
  void initState() {
    itemsFuture = widget.itemsFuture;
    _controller = TextEditingController();
    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
              future:
                  itemsFuture != null ? itemsFuture : widget.getItemsFuture(),
              builder: (context, snapshot) {

                   return RefreshIndicator(
                     onRefresh: () async {
                        setState(() {
                          itemsFuture = null;
                          _controller.text = '';
                        });
                      },
                     child: ...
                   );
                });
    }
}

So the first time, the page loads with the future already loaded. when the user refreshes, I mark the future as null so the callback gets called and the data can be re-fetched.

I’m trying to implement flutter_hooks throughout the app now and I’ve refactored this widget to be like this (simplified version):


class ListSearchPage<T> extends HookWidget {
  
  final Future<List<T>> itemsFuture;
  final ValueGetter<Future<List<T>>> getItemsFuture;

  const ListSearchPage({Key key, this.getItemsFuture, this.itemsFuture})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    final itemsFutureNotifier = useState(this.itemsFuture);
    final TextEditingController _controller = useTextEditingController();
    return FutureBuilder(
              future:
              itemsFutureNotifier.value != null ? itemsFutureNotifier.value : getItemsFuture(),
              builder: (context, snapshot) {
                   return RefreshIndicator(
                     onRefresh: () async {
                        itemsFutureNotifier.value = null;
                        _controller.text = '';
                      },
                     child: ...
                   );
                });
    }

}

This works the first time, however after that the value keeps on getting assigned to null, and therefore the value notifier does not get notified about the change. How can I force the widget to rebuild in this case like before? and as a bonus, do you see a better solution for this?

Thanks in advance.

update

This is itemsFuture

final future = useMemoized(() => repository.fetchData());

This is getItemsFuture

() => repository.fetchData()

The idea behind it is to fetch the data before the search page is opened. In my use case works.

I’ve found a solution to my problem, but I won’t post it as an answer because I don’t believe is clean and I rather see if someone finds the proper way of doing it.

current solution

@override
  Widget build(BuildContext context) {
    // feels like a dirty solution for rebuilding on refresh
    final counterNotifier = useState(0);
    final itemsFutureNotifier = useState(this.itemsFuture);
    final TextEditingController _controller = useTextEditingController();

 return ValueListenableBuilder(
            valueListenable: counterNotifier,
            builder: (context, value, child) {
return FutureBuilder(
              future:
              itemsFutureNotifier.value != null ? itemsFutureNotifier.value : getItemsFuture(),
              builder: (context, snapshot) {
                   return RefreshIndicator(
                     onRefresh: () async {
                        counterNotifier.value++;
                        itemsFutureNotifier.value = null;
                        _controller.text = '';
                      },
                     child: ...
                   );
                });
});

As you can see I now have a counter notifier that will actually rebuild the ValueListenableBuilder and will make the FutureBuilder fetch the data

Solution

I think itemsFuture is not necessary to set to null (because it can be a initial statement inside useState).

@override
Widget build(BuildContext context) {
  final fetchData = useState(itemsFuture ?? getItemsFuture());

  return Scaffold(
    body: FutureBuilder(
      future: fetchData.value,
      builder: (context, snapshot) {
        return RefreshIndicator(
          onRefresh: () async {
            fetchData.value = getItemsFuture();
          },
          child: ...
        );
      },
    ),
  );
}

Answered By – yellowgray

Answer Checked By – Willingham (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *