Reputation: 6011
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
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
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