FLUTTER: How to use navigator in streambuilder? - firebase

I am trying to navigate inside a streambuilder but I have this error:"setState() or markNeedsBuild() called during build.". If I call navigate inside an onpressed button it works but not by just use it inside a condition. I am stuck. There is some code to show you.
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder(
stream:
Firestore.instance.collection('rooms').document(pinid).snapshots(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
if ((snapshot.data['Votes'][0] + snapshot.data['Votes'][1]) >=
snapshot.data['joueurs']) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Results(),
));
}
}
return Center(
child: Text('VOUS AVEZ VOTE'),
);
},
),
);
}

That's because Flutter is triggering a frame build when you are trying to navigate to another screen, thus, that's not possible.
You can schedule a post frame callback so you can navigate as soon as Flutter is done with tree rebuilding for that widget.
import 'package:flutter/foundation.dart';
WidgetsBinding.instance.addPostFrameCallback(
(_) => Navigator.push(context,
MaterialPageRoute(
builder: (context) => Results(),
),
),
);

If navigation is the only thing happening on a button press, I wouldn't use a Bloc at all because Navigation is not business logic and should be done by the UI layer.
If you have business logic on a button press and need to navigate based on some dynamic information then I would do the navigation again in the presentation layer (widget) in response to a success state like below. You can also change navigation logic as per your requirement.
Widget loginButton(LoginBloc loginBloc) =>
StreamBuilder<List<UserLoginResultElement>>(
stream: loginBloc.loginStream,
builder:
(context, AsyncSnapshot<List<UserLoginResultElement>> snapshot) {
print(snapshot.connectionState);
Widget children;
if (snapshot.hasError) {
children = Padding(
padding: const EdgeInsets.only(top: 16),
child: Text('Error: ${snapshot.error}'),
);
} else {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.done:
case ConnectionState.active:
children = BlockButtonWidget(
text: Text(
"LOGIN",
style: TextStyle(color: Theme.of(context).primaryColor),
),
color: Theme.of(context).accentColor,
onPressed: () async {
try {
bloc.submit(_userNameController.value.text,
_passwordController.value.text, context);
} catch (ex) {
print(ex.toString());
}
},
);
break;
}
}
if (snapshot.data != null && snapshot.hasData) {
if (snapshot.data[0].code == "1") {
SchedulerBinding.instance.addPostFrameCallback((_) {
Navigator.pushReplacementNamed(context, "/HomeScreen");
});
} else {
print(Login Failed');
}
}
return children;
});

Related

why my streamBuilder get stuck in the active Connection State even though i got all the data in my App?

Im getting data in my Flutter Web App from my Firebase Collection but my stream builder connection state stuck in the active state and not proceeding to the done state
static Stream<QuerySnapshot> getData({required String collectionName}) {
return firebaseFirestore.collection(collectionName).snapshots();
}
this is how im calling the stream
late final Stream<QuerySnapshot> myStream;
#override
void initState() {
super.initState();
myStream = FireStoreServices.getData(collectionName: selectedRadio!);
}
my build method
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: myStream,
builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return const Center(
child: Text('No Data'),
);
case ConnectionState.waiting:
return const Center(
child: CircularProgressIndicator(),
);
// **stream stuck here** as long as the application is running even though i **got all the data**
case ConnectionState.active:
return const Center(
child: Text('Connection is active'),
);
case ConnectionState.done:
return const Center(
child: Text('Connection is DOne'),
);
default:
return const Center(
child: Text(' default state '),
);
}
Any Help Would be Appreciated
It's normal when you use StreamBuilder unlike FutureBuilder. Actually you should display data while the state is active or if the state is done.
Bellow a small example on how to use StreamBuilder:
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.connectionState == ConnectionState.active
|| snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return const Text('Error');
} else if (snapshot.hasData) {
return Text(
snapshot.data.toString(),
style: const TextStyle(color: Colors.teal, fontSize: 36)
);
} else {
return const Text('Empty data');
}
} else {
return Text('State: ${snapshot.connectionState}');
}
You could find more info about StreamBuilder in this blog.

SetState is causing Futurebuilder to reload the data on every tap

I am using future builder and stream builder to fetch data from firebase and show them on screen.
I have favourite button as well. when I click on favourite_borderLine iconButton. It fetch data from firebase then change the state to favourite_border iconButton.
It also change the state of every other listview.Builder what I want is just to change the icon state on every click not fetching the whole data from database.
This is the initial state
when I tap on favourite icon, Suppose I tapped on first icon then it start loading.
and then all the icons are changed :(
I just want to change the clicked icon state not all icons and do not want the fetch data on click just change the state of button.Here is code.
class TalentScreen1 extends StatefulWidget {
#override
_TalentScreen1State createState() => _TalentScreen1State();
}
class _TalentScreen1State extends State<TalentScreen1> {
bool toggle = false;
#override
Widget build(BuildContext context) {
return BlocProvider<TalentFavCubit>(
create: (context) => TalentFavCubit(),
child: SafeArea(
child: Scaffold(
body: Padding(
padding: const EdgeInsets.all(20.0),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text('Talent Screen 1 home search'),
_retriveAllDocs,
],
),
),
),
),
),
);
}
Widget get _retriveAllDocs => FutureBuilder<QuerySnapshot>(
future: FirebaseRepo.instance.fetchWorkerFormFieldsData(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting)
return CircularProgressIndicator();
if (snapshot.hasError) {
return Text("Something went wrong");
}
if (!snapshot.hasData) {
return Text("Nothing to show");
}
if (snapshot.connectionState == ConnectionState.done) {
final List<DocumentSnapshot> data = snapshot.data.docs;
return theUserInfo(data);
}
return Text("loading");
});
Widget theUserInfo(List<DocumentSnapshot> data) {
return ListView.builder(
shrinkWrap: true,
itemCount: data.length,
itemBuilder: (context, index) {
return FutureBuilder<DocumentSnapshot>(
future: fetch(data[index]['uid']),
builder: (BuildContext context,
AsyncSnapshot<DocumentSnapshot> snapshot) {
if (snapshot.hasError) {
return Text("Something went wrong");
}
if (snapshot.connectionState == ConnectionState.done) {
TalentHireFavModel userData = TalentHireFavModel.fromMap(
data[index].data(), snapshot.data.data());
return Card(
child: Column(
children: <Widget>[
Text(userData.name),
Text(userData.categories),
Text(userData.skills),
Text(userData.country),
Text(userData.phoneNo),
Text(userData.hourlyRate),
Text(userData.professionalOverview),
Text(userData.skills),
Text(userData.expert),
Text(userData.createdAt),
IconButton(
icon: toggle
? Icon(Icons.favorite_border)
: Icon(
Icons.favorite,
),
onPressed: () {
setState(() {
// Here we changing the icon.
toggle = !toggle;
});
}),
],
),
);
}
return Container();
});
});
}
//TODO: Implementation Fix Error
Widget _iconButton(uid) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseRepo.instance.fetchCurrentUserFavourites().snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
var data = snapshot.data.docs;
// print(snapshot.data.get('uid'));
if (snapshot.hasError) {
return Text('Something went wrong');
}
return IconButton(
icon: data.isEmpty == uid
? Icon(Icons.favorite)
: Icon(Icons.favorite_border),
onPressed: () =>
BlocProvider.of<TalentFavCubit>(context).addTalentFav(uid));
},
);
}
Future<DocumentSnapshot> fetch(data) async =>
await FirebaseRepo.instance.fetchWorkerUserData(data);
}
This is your broken line of code:
future: FirebaseRepo.instance.fetchWorkerFormFieldsData(),
The FutureBuilder documentation starts with:
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, 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.
A general guideline is to assume that every build method could get called every frame, and to treat omitted calls as an optimization.
And you broke the contract. I have a video that illustrates this in detail. https://www.youtube.com/watch?v=sqE-J8YJnpg
Do what the docs say. TL;DR: Do not create the Future in the parameter to FutureBuilder.

Flutter/Firestore/Provider - Error shown for split second then stream displayed, how can I load stream values on startup?

I am using a Stream Provider to access Firestore data and pass it around my app. The problem I am facing starts when I first run the app. Everything starts as normal but as I navigate to the screen where I am using the Stream values in a list view, I initially get an error before the UI rebuilds and the list items appear after a split second. This is the error I get:
════════ Exception caught by widgets library ═══════════════════════════════════
The following NoSuchMethodError was thrown building OurInboxPage(dirty, dependencies: [_InheritedProviderScope<List<InboxItem>>]):
The getter 'length' was called on null.
Receiver: null
Tried calling: length
I'm guessing this has something to do with the load time to access the values and add them to the screen? How can I load all stream values when the app starts up to avoid this?
Here is my Stream code:
Stream<List<InboxItem>> get inboxitems {
return orderCollection
.where("sendTo", isEqualTo: FirebaseAuth.instance.currentUser.email)
.snapshots()
.map(
(QuerySnapshot querySnapshot) => querySnapshot.docs
.map(
(document) => InboxItem.fromFirestore(document),
)
.toList(),
);
}
I then add this to my list of Providers:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
MultiProvider(
providers: [
StreamProvider<List<InboxItem>>.value(value: OurDatabase().inboxitems),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<OurUser>(
builder: (_, user, __) {
return MaterialApp(
title: 'My App',
theme: OurTheme().buildTheme(),
home: HomepageNavigator(),
);
},
);
}
}
And finally the page I want to display the stream items:
class OurInboxPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
List<InboxItem> inboxList = Provider.of<List<InboxItem>>(context);
return Scaffold(
body: Center(
child: ListView.builder(
itemCount: inboxList.length,
itemBuilder: (context, index) {
final InboxItem document = inboxList[index];
return Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(document.event),
Icon(Icons.arrow_forward_ios)
],
),
);
},
),
),
);
}
}
Thanks
Yeah its trying to build before the data is populated, hence the null error.
Wrap your ListView.builder in a StreamBuilder and having it show a loading indicator if there's no data.
StreamBuilder<List<InboxItem>>(
stream: // your stream here
builder: (context, snapshot) {
if (snapshot.hasData) {
return // your ListView here
} else {
return CircularProgressIndicator();
}
},
);
I'm assuming your not using the latest version of provider because the latest version requires StreamProvider to set initialData.
If you really want to use StreamProvider and don't want a null value, just set its initialData property.
FROM:
StreamProvider<List<InboxItem>>.value(value: OurDatabase().inboxitems),
TO:
StreamProvider<List<InboxItem>>.value(
value: OurDatabase().inboxitems,
initialData: <InboxItem>[], // <<<<< THIS ONE
),
If you want to display some progress indicator while getter function inboxitems is executed initially. You don't need to modify the StreamProvider, and just add a null checking in your OurInboxPage widget.
class OurInboxPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
final List<InboxItem>? inboxList =
Provider.of<List<InboxItem>?>(context, listen: false);
return Scaffold(
body: inboxList == null
? const CircularProgressIndicator()
: ListView.builder(
itemCount: inboxList.length,
itemBuilder: (_, __) => Container(
height: 100,
color: Colors.red,
),
),
);
}
}
There are 2 ways to solve the issue.
Use the progress bar while the data is loading.
StreamBuilder<int>(
stream: getStream(),
builder: (_, snapshot) {
if (snapshot.hasError) {
return Text('${snapshot.error}');
} else if (snapshot.hasData) {
return Text('${snapshot.data}');
}
return Center(child: CircularProgressIndicator()); // <-- Use Progress bar
},
)
Provide dummy data initially.
StreamBuilder<int>(
initialData: 0, // <-- Give dummy data
stream: getStream(),
builder: (_, snapshot) {
if (snapshot.hasError) return Text('${snapshot.error}');
return Text('${snapshot.data}');
},
)
Here, getStream() return Stream<int>.

Flutter rendering list from firebase based on condition in dropdown menu

I'm trying to figure out how to render a list from a specific collection in firebase and to change that list when selecting options from dropdown menu. I could get the list rendered on 1 collection, but when I add my dropdown menu, with the default value being 'lost', nothing is displayed. Here's what I have so far that works, but not entirely what I want.
class _ListPageState extends State<ListPage>{
List<String> _type = ['lost', 'found'];
String _selectedView = 'lost';
//this getData pulls from 'lost' collection, since I set _selectedView to lost by default
Future getData() async{
var firestore = Firestore.instance;
QuerySnapshot qn = await firestore.collection(_selectedView).getDocuments();
return qn.documents;
}
navigateToDetail(DocumentSnapshot post){
Navigator.push(context, MaterialPageRoute(builder: (context) => DetailPage(post: post,)));
}
Widget _viewType() {
return new DropdownButtonFormField(
value: _selectedView,
onChanged: (newValue) {
setState(() {
_selectedView = newValue;
});
},
items: _type.map((view) {
return new DropdownMenuItem(
child: new Text(view),
value: view,
);
}).toList(),
);
}
#override
Widget build(BuildContext context){
return ListView(
children: <Widget>[
_viewType(),
FutureBuilder(//it's not rendering any of this when adding the dropdown above it
future: getData(),
builder: (_, snapshot){
if(snapshot.connectionState == ConnectionState.waiting){
return Center(
child: Text("Loading"),
);
}
else{
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (_, index){
return ListTile(
title: Text(snapshot.data[index].data["Title"]),
onTap: () => navigateToDetail(snapshot.data[index]),
);
});
}
}),]
);
}
}
Thanks in advance for any help.
Please let me know if there's any more code you'd like to see.
I this I have to wrap part of it with setState(), but I'm not quite sure where.
Thanks for the fast clarification.
What is happening here is that you have put a ListView inside a ListView. You should use a Column.
By default (as mentioned in the documentation):
The Column widget does not scroll (and in general it is considered an error to have more children in a Column than will fit in the available room). If you have a line of widgets and want them to be able to scroll if there is insufficient room, consider using a ListView.
In your case, you want to place a ListView that will overflow the Column that can't scroll. To avoid that, consider using an Expanded
to take the remaining space so that the height is somehow constrained and the ListView knows its limits and work properly.
class _ListPageState extends State<ListPage> {
List<String> _type = ['lost', 'found'];
String _selectedView = 'lost';
//this getData pulls from 'lost' collection, since I set _selectedView to lost by default
Future getData() async {
var firestore = Firestore.instance;
QuerySnapshot qn = await firestore.collection(_selectedView).getDocuments();
return qn.documents;
}
navigateToDetail(DocumentSnapshot post) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(
post: post,
)));
}
Widget _viewType() {
return new DropdownButtonFormField(
value: _selectedView,
onChanged: (newValue) {
setState(() {
_selectedView = newValue;
});
},
items: _type.map((view) {
return new DropdownMenuItem(
child: new Text(view),
value: view,
);
}).toList(),
);
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
_viewType(),
Expanded(
child: FutureBuilder(
//it's not rendering any of this when adding the dropdown above it
future: getData(),
builder: (_, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: Text("Loading"),
);
} else {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (_, index) {
return ListTile(
title: Text(snapshot.data[index].data["Title"]),
onTap: () => navigateToDetail(snapshot.data[index]),
);
},
);
}
},
),
),
],
);
}
}

How to show a dialog with FutureBuilder when press a button?

I upload an image FirebaseStorage with FutureBuilder and when I press upload button I want to show a waiting dialog. Image upload successfully on FireStorage but nothing shows up. Whats wrong my codes? I think FutureBuilder return Widget and press button void. Maybe it is the problem. Or I am calling FutureBuilder wrong way. Do you have any tips or suggestions for my wrong code?
Here is my code;
Widget buildBody(BuildContext context) {
return new SingleChildScrollView(
child: new Column(
children: [
new SizedBox(
width: double.infinity,
child: new RaisedButton(
child: new Text('Upload'),
onPressed: () {
futureBuilder();
},
),
),
],
),
);
}
Future mediaUpload() async {
final fileName = DateTime.now().millisecondsSinceEpoch.toString() + '.jpg';
final StorageReference storage = FirebaseStorage.instance.ref().child(fileName);
final StorageUploadTask task = storage.putFile(_image);
return task.future;
}
Widget futureBuilder() {
return new FutureBuilder(
future: mediaUpload(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
print('ConnectionState.none');
return new Text('Press button to start.');
case ConnectionState.active:
print('ConnectionState.active');
return new Text('');
case ConnectionState.waiting:
waitingDialog(context);
return waitingDialog(context);
case ConnectionState.done:
print('ConnectionState.done');
if (snapshot.hasError) return new Text('Error: ${snapshot.error}');
print(snapshot.data.downloadUrl);
Navigator.of(context).pushReplacementNamed('home');
return new Text('Result: ${snapshot.data.downloadUrl}');
}
},
);
}
waitingDialog(BuildContext context) {
return showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return new Center(
child: new SizedBox(
width: 40.0,
height: 40.0,
child: const CircularProgressIndicator(
value: null,
strokeWidth: 2.0,
),
),
);
},
);
}
I am not 100% sure why FutureBuilder is not working. But I have a guess. In the above example, FutureBuilder is not attached to the widgetTree(screen). Not attached means value returned from FutureBuilder is not displayed in screen. In this case, Text is returned based on snapshot.connectionState which is not attached to screen.
Work around: (I tested and it worked, can you please verify)
futureBuilder(BuildContext context) {
mediaUpload().then((task) { // fire the upload
Navigator.of(context).maybePop(); // remove the dialog on success upload
}); // we can use task(which returned from
// mediaUpload()) to get the values like downloadUrl.
waitingDialog(context); // show the spinner dialog
}

Resources