FutureBuilder running two times bug - dart

I added debug point for
future: _futureData
Future getRegister1() async{ first run getRegister1() and then future: _futureData and again two times run getRegister1() after
that screen comes _mainCategory State Widget
class _mainCategory extends State<mainCategory3> {
Future _futureData;
#override
void initState() {
super.initState();
_futureData = getRegister1();
}
#override
Widget build(BuildContext context) {
return Scaffold(
FutureBuilder inside Body
body: FutureBuilder(
future: _futureData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
getRegister1()
Future getRegister1() async {
//this data coming from cache (sqflite)
List _catLocal = (await HelperDatabase1().displayRegisterCat());
List _defCatLocal = (await HelperDatabase1().display());
...
}
full code https://github.com/flutter/flutter/issues/31838

Related

How to prevent FutureBuilder's rebuilds even its underlying future does not change

In Flutter, I am getting unwanted rebuilds. In my case I use FutureBuilder to show a list by fetching db result, which is a future and have a dependency on query parameter. I tried to make it that FutureBuilder's future does not change if the query parameter does not change, But still the FutureBuilder's builder block is called every time. How can I make the FutureBuilder does not rebuild itself where its future does not change.
Below is my codes, every time the MusicList2's parent widget build, MusicList2 get rebuild, its FutureBuilder get rebuild.
class MusicList2 extends StatefulWidget {
final MusicRowActionCallback onTapItem;
final MusicRowActionCallback onDoubleTap;
final MusicRowActionCallback onLongPressed;
final String facetName;
final String facetValue;
const MusicList2(
{Key key,
this.onTapItem,
this.onDoubleTap,
this.onLongPressed,
this.facetName,
this.facetValue}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _MusicList2State();
}
}
class _MusicList2State extends State<MusicList2> {
Future<List<Music>> loadMusicByFacet;
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Music>>(
future:
loadMusicByFacet,
builder: (context, snapshot) {
if (snapshot.data == null)
return Center(child: CircularProgressIndicator(),);
return ListView.builder(
shrinkWrap: true,
key: const ValueKey<String>('music-list'),
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
final random = Random();
var i = random.nextInt(5);
return MusicRow(
avatarBgColor: colors[i],
music: snapshot.data[index],
onTap: widget.onTapItem,
onDoubleTap: widget.onDoubleTap,
onLongPressed: widget.onLongPressed,
);
},
);
},
);
}
#override
void initState() {
super.initState();
loadMusicByFacet = MusicsDatabaseRepository.get.getMusicsByFacet(widget.facetName, widget.facetValue);
}
#override
void didUpdateWidget(MusicList2 oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.facetValue != widget.facetValue || oldWidget.facetName != widget.facetName) {
loadMusicByFacet = MusicsDatabaseRepository.get.getMusicsByFacet(widget.facetName, widget.facetValue);
}
}
}
For anyone else having this error, I solved it using AbdulRahmanAlHamali's solution on GitHub. This is his answer:
Hello, I believe the problem here is that didUpdateWidget of the
FutureBuilder state is being called every time a rebuild is issued.
This function checks if the old future object is different from the
new one, and if so, refires the FutureBuilder.
To get past this, we can call the Future somewhere other than in the
build function. For example, in the initState, and save it in a member
variable, and pass this variable to the FutureBuilder.
So instead of having:
FutureBuilder(
future: someFunction(),
....
We should have:
initState() {
super.initState();
_future = SomeFunction();
}
and then
FutureBuilder(
future: _future,
....
Find the original thread here.

How to fix FutureBuilder open multiple times error?

these my two classes(two pages). these two classes open multiple times.
I put debug point in futurebuilder in two classes.
debug point running,
MainCategory page and got to the next page
SubCategory page and again running MainCategory page(previous page) futurebuilder and again running MainCategory page futurebuilder
navigate subcategory page to third page running subcategory page and main category page
I upload my two classes to GitHub and please let me know what the issue is.
MainCategory code: https://github.com/bhanuka96/ios_login/blob/master/MainCategory.dart
SubCategory code: https://github.com/bhanuka96/ios_login/blob/master/subCategory.dart
As stated in the documentation, you should not fetch the Future for the Futurebuilder during the widget's build event.
https://docs.flutter.io/flutter/widgets/FutureBuilder-class.html
The future must have been obtained earlier, e.g. during
State.initState, State.didUpdateConfig, or
State.didChangeDependencies. It must not be created during the
State.build or StatelessWidget.build method call when constructing the
FutureBuilder. If the future is created at the same time as the
FutureBuilder, then every time the FutureBuilder's parent is rebuilt,
the asynchronous task will be restarted.
So, try to move your call to getRegister method outside the build method and replace it with the returned Future value.
For example, below I have a class that returns a Future value which will be consumed with the help of FutureBuilder.
class MyApiHelper{
static Future<List<String>> getMyList() async {
// your implementation to make server calls
return List<String>();
}
}
Now, inside your widget, you will have something like this:
class _MyHomePageState extends State<MyHomePage> {
Future<List<String>> _myList;
#override
void initState() {
super.initState();
_myList = MyApiHelper.getMyList();
}
#override
Widget build(BuildContext context) {
return Scaffold(body: FutureBuilder(
future: _myList,
builder: (_, AsyncSnapshot<List<String>> snapLs) {
if(!snapLs.hasData) return CircularProgressIndicator();
return ListView.builder(
itemCount: snapLs.data.length,
itemBuilder: (_, index) {
//show your list item row here...
},
);
},
));
}
}
As shown above, the Future is fetched in the initState function and used inside the build method and used by FutureBuilder.
I hope this was helpful.
Thanks.
If you happen to use Provider, here's (in my opinion) a clearer alternative based on your question:
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FutureProvider<List<String>>(
create: (_) => MyApiHelper.getMyList(),
child: Consumer<List<String>>(
builder: (_, list, __) {
if (list == null) return CircularProgressIndicator();
return ListView.builder(
itemCount: list.length,
itemBuilder: (_, index) {
//show your list item row here...
},
);
};
),
);
}
}
This can also be achieved of course as a StatefulWidget as suggested by the other answer, or even with flutter_hooks as explained in Why is my Future/Async Called Multiple Times?
You can create new Widget and pass Function to
returnFuture as
() {
return YourFuture;
}
import 'dart:developer';
import 'package:flutter/material.dart';
class MyFutureBuilder<T> extends StatefulWidget {
final Future<T> Function() returnFuture;
final AsyncWidgetBuilder<T> builder;
final T initialData;
MyFutureBuilder({
this.returnFuture,
#required this.builder,
this.initialData,
Key key,
}) : super(key: key);
#override
_MyFutureBuilderState<T> createState() => _MyFutureBuilderState<T>();
}
class _MyFutureBuilderState<T> extends State<MyFutureBuilder<T>> {
bool isLoading = false;
Future<T> future;
#override
void initState() {
super.initState();
future = widget.returnFuture();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
builder: widget.builder,
initialData: widget.initialData,
future: future,
);
}
}
Example
MyFutureBuilder<List<User>>(
returnFuture: () {
return moderatorUserProvider
.getExecutorsAsModeratorByIds(val.users,
save: true);
},
builder: (cont, asyncData) {
if (asyncData.connectionState !=
ConnectionState.done) {
return Center(
child: MyCircularProgressIndicator(
color: ModeratorColor.executors.color,
),
);
}
return Column(
children: asyncData.data
.map(
(singlExecutor) =>
ChooseInfoButton(
title:
'${singlExecutor.firstName} ${singlExecutor.secondName}',
subTitle: 'Business analyst',
middleText: '4.000 NOK',
subMiddleText: 'full time',
label: 'test period',
subLabel: '1.5 month',
imageUrl:
assetsUrl + 'download.jpeg',
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) =>
ModeratorExecutorEditPage(),
),
);
},
),
)
.toList());
},
)
```

From function Widget to StatelessWidget in Flutter

I'm working on firestore from Google Lab example. What I want to happen is convert _buildList() and _buildListItem() function Widget into StatelessWidget including parameters because I red an article that splitting into function Widget is performance antipattern. But I don't know where to start. Anyone who can give a shed of light to this problem. Thank You.
class _VideoListState extends State<VideoList> {
#override
Widget build(BuildContext context) {
...
body: StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection(widget.category).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
// I want StatelessWidget not function widget
return _buildList(context, snapshot.data.documents);
},
),
);
}
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
return ListView(
// I want StatelessWidget not function widget
children: snapshot.map((data) => _buildListItem(context, data)).toList(),
);
}
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
final record = Record.fromSnapshot(data);
return Column(
children: <Widget>[
Text(record.title),
YoutubePlayer(
source: record.videoId.toString(),
quality: YoutubeQuality.LOW,
autoPlay: false,
context: context
);
}
}
It's simple. Take a look at the source code and read the comments. The source is auto explained by itself. I have used your methods names as class names.
// the method buildList into a stateless widget
class BuildListWidget extends StatelessWidget{
final List<DocumentSnapshot> snapshotList;
BuildListWidget({this.snapshotList}){} // you can use this approach to initialize your snapshotList.
// Here there parameter is already the member of class snapshotList
#override
Widget build(BuildContext context) {
//building a listView in this way allows you build items on demand.
return ListView.builder(
itemCount: snapshotList.length, // number of items in list
itemBuilder: (BuildContext context, int index){
//creating list members. Each one with your DocumentSnapshot from list
return BuildListItemWidget(
dataSnapshot: snapshotList[index], // getting DocumentSnapshot from list
);
}
);
}
}
// the mehtod _buildListItem into a stateless widget
class BuildListItemWidget extends StatelessWidget {
final DocumentSnapshot _data; // just if you want to hold a snapshot...
final Record _record; // your record reference
//here another approach to inicialize class data using named parameters and
// initialization list in class contructor
BuildListItemWidget({#required DocumentSnapshot dataSnapshot}) :
_record = Record.fromSnapshot(dataSnapshot),
_data = dataSnapshot;
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text(record.title),
YoutubePlayer(source: _record.videoId.toString(),
quality: YoutubeQuality.LOW,
autoPlay: false,
context: context
);
}
}
// usage...
class _VideoListState extends State<VideoList> {
#override
Widget build(BuildContext context) {
...
body: StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection(widget.category).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
// so here you have a statelessWidget
return BuildListWidget( snapshotList: snapshot.data.documents );
},
),
}
}

How to prevent passing down BuildContext?

Currently I get the BuildContext from the build method in HomeScreen, and then I have to pass it down to _gridSliver then down to _storeCard.
How can I write the code so that I don't need to pass the context down?
Maybe I can create a new private StatelessWidget called _StoreCard that will have its own build method and thus its own BuildContext?
class HomeScreen extends StatelessWidget {
HomeScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return StoreConnector<AppState, List<MyStore.Store>>(
converter: (Store<AppState> store) => store.state.home.stores,
builder: (BuildContext context, List<MyStore.Store> stores) =>
CustomScrollView(slivers: <Widget>[_gridSliver(stores, context)]));
}
Widget _gridSliver(stores, context) {
return SliverGrid(
delegate: SliverChildListDelegate(List<Widget>.from(stores.map(_storeCard, context))));
}
Widget _storeCard(MyStore.Store store, BuildContext context) {
return InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => StoreScreen(storeId: store.id)),
);
},
child: Container(child: Text(store.name))
);
}
}
Another instance of this problem is I navigate on a child function.
#override
Widget build(BuildContext context) {
return Column(
children: [
WhiteButton(text: "Login with Facebook", onPressed: _loginWithFacebook),
WhiteButton(text: "Login with Google", onPressed: _loginWithGoogle),
])
)
}
_loginWithFacebook(context) async {
...
var user = User.fromFacebook(result.accessToken.token, json.decode(graphResponse.body));
await _login(user, context);
}
}
_loginWithGoogle(context) async {
...
GoogleSignInAccount googleUser = await _googleSignIn.signIn();
await _login(User.fromGoogle(googleUser), context);
}
_login(user, context) async {
var fetchedUser = await MeService.getUser(user);
if (fetchedUser != null) {
loginSuccess(fetchedUser);
Navigator.popUntil(context, ModalRoute.withName(MainRoutes.root));
} else {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => RegisterScreen(user: user)),
);
}
}
To get a new BuildContext, you have two main solutions:
Extract part of the subtree into a new widget, typically StatelessWidget. And then use it's BuildContext from the build method
Use Builder widget, which is basically a reusable widget made to obtain a BuildContext:
Example:
#override
Widget build(BuildContext context) {
return Builder(
builder: (context) {
// do something with this new context
},
);
}
You have to use a Bloc pattern that uses an Inherited Widget, but still you'll have to pass context, but in a more straight forward way. I recommend using this app by Stephen Grider, to figure out how the whole thing works. He explains in his tutorial how to put the whole thing together but I can't link you to that because that would be advertising.
The idea is, you first create a file Bloc.dart that is going to contain your logic, then you create what is called a Provider, in a Provider.dart.
Provider.dart:
class Provider extends InheritedWidget {
final bloc = Bloc();
Provider({Key key, Widget child}) : super(key: key, child: child);
bool updateShouldNotify(_) => true;
static Bloc of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(Provider) as Provider).bloc;
}
}
In your file that contains the Material App, you wrap the material App with the provider:
Widget build(BuildContext context) {
return Provider(
child: MaterialApp(
And then you use the provider in every other class down the three of widgets.
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final bloc = Provider.of(context); // this is where you insert the provider
return StoreConnector<AppState, List<MyStore.Store>>(
converter: (Store<AppState> store) => store.state.home.stores,
builder: (BuildContext context, List<MyStore.Store> stores) =>
CustomScrollView(slivers: <Widget>[_gridSliver(stores, context)]));
}
Widget _gridSliver(stores) {
final bloc = Provider.of(context);
return SliverGrid(
delegate: SliverChildListDelegate(List<Widget>.from(stores.map(_storeCard, context))));
}
Widget _storeCard(MyStore.Store store) {
final bloc = Provider.of(context);
return InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => StoreScreen(storeId: store.id)),
);
},
child: Container(child: Text(store.name))
);
}
}
I'm a total noob with flutter and take everything with grain of salt, but this is what I would use. Hope it helps.

How to properly feed data to StreamBuilder initialData parameter?

I am using a StreamBuilder widget to display some data. When the app is recently opened, I wanted to display some initial data from my json file and feed it into the initialData optional keyword parameter of StreamBuilder.
Here is how I feed it:
MyStorage m = new MyStorage(); // Using path_provider, I accessed the json file inside this class
int x;
#override
void initState(){
super.initState();
getData();
}
getData() async{
Map<String, dynamic> myMap = await m._getMap;
x = int.parse(myMap["total"]);
}
...
#override
Widget build(BuildContext context){
...
child: StreamBuilder(
stream: mystream, // coming from my BLoC class
initialData: x,
builder: (context, snapshot){
return new Text("${snapshot.data}");
}
...
The problem is it that the Text widget inside my StreamBuilder is displaying "null".
I tried to rewrite my code to this:
MyStorage m = new MyStorage();
Future<int> getData() async{
Map<String, dynamic> myMap = await m._getMap;
return int.parse(myMap["total"]);
}
...
#override
Widget build(BuildContext context){
...
child: StreamBuilder(
stream: mystream, // coming from my BLoC class
initialData: getData(),
builder: (context, snapshot){
return new Text("${snapshot.data}");
}
...
but it displayed on my Text widget as "Instance of Future:int"
I have no problem with my stream parameter in the StreamBuilder. It displays the correct value that I'm expecting from the BLoC class.
The only problem I had is the feeding of initialData from my json file.
What am I doing wrong? I would appreciate any kind of help. Thanks
[UPDATE]
After a long hours of thinking a solution, I gave up using initialData parameter since after I added int to StreamBuilder like this StreamBuilder<int>() it prompted me that it will only take a value of integer. I couldn't fed it with a type of Future or Stream so I decided not to use it. What I did was I nested a FutureBuilder inside the StreamBuilder through ConnectionState.
Here is what my code now:
MyStorage m = new MyStorage();
Future<int> getData() async{
Map<String, dynamic> myMap = await m._getMap;
return int.parse(myMap["total"]);
}
...
#override
Widget build(BuildContext context){
...
child: StreamBuilder<int>(
stream: mystream, // coming from my BLoC class
//initialData: getData(),
builder: (context, snapshot){
swith(snapshot.connectionState){
case ConnectionState.none:
return new FutureBuilder(
future: getData(),
builder: (context, snapshot){
return new Text('${snapshot.data}');
}
);
case ConnectionState.active:
case ConnectionState.waiting:
return new FutureBuilder(
future: getData(),
builder: (context, snapshot){
return new Text('${snapshot.data}');
}
);
case ConnectionState.done:
if (snapshot.hasData){
return new Text('${snapshot.data}');
}
return new FutureBuilder(
future: getData(),
builder: (context, snapshot){
return new Text('${snapshot.data}');
}
);
}
}
...
I know that this solution is very inefficient but I couldn't think of a better solution as of now.
initialData is supposed to be the data to show before your actual data is available.
place 0 as initialData.
StreamBuilder has an additional parameter stream which is responsible for fetching your stream data. here you can do stream: getData().asStream,
edit:
Also make sure to specify the type of data you are expecting in your StreamBuilder. StreamBuilder<int>()

Resources