user987654
user987654

Reputation: 6011

flutter infinite loop when FutureBuilder inside LayoutBuilder

I have a PageView inside LayoutBuilder to get the widget size. Since it depends on the widget size, I don't know how many pages there will be until the widget is built. So I added FutureBuilder inside LayoutBuilder, so the number of pages can be calculated asynchronously. Here's my code but it's waiting indefinitely in ConnectionState.waiting. What's the problem in the code and how to solve it?

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: ScrollableTabsDemo());
  }
}

class _Page {
  const _Page({this.icon, this.text});
  final IconData icon;
  final String text;
}

const List<_Page> _allPages = <_Page>[
  _Page(text: 'tab 1'),
  _Page(text: 'tab 2'),
  _Page(text: 'tab 3'),
];

class ScrollableTabsDemo extends StatefulWidget {
  static const String routeName = '/material/scrollable-tabs';

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

class ScrollableTabsDemoState extends State<ScrollableTabsDemo>
    with SingleTickerProviderStateMixin {
  TabController _controller;

  @override
  void initState() {
    super.initState();
    _controller = TabController(vsync: this, length: _allPages.length);
  }

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

  @override
  Widget build(BuildContext context) {
    final Color iconColor = Theme.of(context).accentColor;
    return Scaffold(
      appBar: AppBar(
        title: const Text('Scrollable tabs'),
        bottom: TabBar(
          controller: _controller,
          isScrollable: true,
          tabs: _allPages.map<Tab>((_Page page) {
            return Tab(text: page.text);
          }).toList(),
        ),
      ),
      body: TabBarView(
        controller: _controller,
        children: _allPages.map<Widget>((_Page page) {
          return SafeArea(
            top: false,
            bottom: false,
            child: LayoutBuilder(builder: (context, constraints) {
              return FutureBuilder<int>(
                  future: getPageCount(),
                  builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
                    switch (snapshot.connectionState) {
                      case ConnectionState.none:
                        print("none ${snapshot.data}");
                        return Text('none');
                      case ConnectionState.active:
                        print("active ${snapshot.data}");
                        return Text('active');
                      case ConnectionState.waiting:
                        print("waiting ${snapshot.data}");
                        return Center(child: CircularProgressIndicator());
                      case ConnectionState.done:
                        print("done ${snapshot.data}");
                        return buildPageView(snapshot.data);
                    }
                  });
            }),
          );
        }).toList(),
      ),
    );
  }

  Future<int> getPageCount() => Future.delayed(Duration(seconds: 3), () => 5);

  Widget buildPageView(int pageCount) {
    return PageView.builder(
      itemBuilder: (context, position) {
        return Container(child: Center(child: Text(position.toString())));
      },
      itemCount: pageCount,
    );
  }
}

Upvotes: 1

Views: 3709

Answers (2)

szotp
szotp

Reputation: 2622

The documentation for FutureBuilder states that you should not create the future on every build: https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html

Either run getPageCount in initState, in a field initializer, or prepare a stateful widget similiar to FutureBuilder that takes a future creating function and calls this function in it's own initState.

EDIT: With this helper you can have the future cached automatically

class CachingFutureBuilder<T> extends StatefulWidget {
  final Future<T> Function() futureFactory;
  final AsyncWidgetBuilder<T> builder;

  const CachingFutureBuilder(
      {Key key, @required this.futureFactory, @required this.builder})
      : super(key: key);
  @override
  _CachingFutureBuilderState createState() => _CachingFutureBuilderState<T>();
}

class _CachingFutureBuilderState<T> extends State<CachingFutureBuilder<T>> {
  Future<T> _future;

  @override
  void initState() {
    _future = widget.futureFactory();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<T>(
      future: _future,
      builder: widget.builder,
    );
  }
}

If your getPageCount requires size as input, you could use that helper widget like in the code below. The trick is to use ValueKey so that Flutter knows to reinit the CachingFutureBuilder. Make sure you don't have any size changing animations because it would cause the Future to reload on every frame.

class ScrollableTabsDemo extends StatelessWidget {
  Future<int> getPageCount(Size size) {
    return Future.value(1);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: LayoutBuilder(
          builder: (context, constraints) {
            final size = Size(constraints.maxHeight, constraints.maxWidth);
            return CachingFutureBuilder<int>(
              key: ValueKey(size),
              futureFactory: () => getPageCount(size),
              builder: (context, snapshot) {
                return Text(snapshot.toString());
              },
            );
          },
        ),
      ),
    );
  }
}

Upvotes: 4

Sergio Bernal
Sergio Bernal

Reputation: 2327

You should not use the FutureBuilder inside a LayoutBuilder. The builder might be called several times and therefore the future gets called every single time. The builder in the LayoutBuilder, gets called when the UI its updated. For example, if you change from portrait to landscape, the builder it's called twice, so the future too.

Upvotes: 1

Related Questions