Flutter/Firebase: build page depending on database - firebase

I'm trying to build a simple card game app to play online with friends using Flutter and Firebase. The way it works (for now), is we have lobbies which players can join and set their state as ready or notReady. Joining rooms and setting the state is synchronized with Firebase.
So Firebase has a list of lobbies, which have a list of players which all have a name and a isReady parameter. Currently, we display the list of players in real time as well as their state in real time. What I can't figure out is how to check if all players are ready and display a countdown that will then take them to the game page.
Even if we forget the countdown idea, I have no idea how to check for readiness and automatically go to the game page after a slight delay.
Any help is greatly appreciated, here's the code just in case!
class _GameRoomPageState extends State<GameRoomPage> {
bool isReady = false;
late DatabaseReference ref;
#override
Widget build(BuildContext context) {
ref = FirebaseDatabase.instance.ref('rooms/${widget.roomId}/players');
return Scaffold(
appBar: AppBar(
title: Text(widget.roomName),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
deletePlayerFromRoom();
Navigator.pop(context);
},
)),
floatingActionButton: FloatingActionButton.extended(
label: isReady ? const Text("I'm not ready !") : const Text("I'm ready !"),
icon: isReady ? const Icon(Icons.cancel_outlined) : const Icon(Icons.check),
onPressed: () {
setState(() {
isReady = !isReady;
final postData = {'name': globals.username, 'isReady': isReady};
final Map<String, Map> updates = {};
updates[globals.userId] = postData;
ref.update(updates);
});
},
),
body: Column(
children: [
Flexible(
child: FirebaseAnimatedList(
query: ref,
itemBuilder: (BuildContext context, DataSnapshot snapshot_, Animation<double> animation, int index) {
Map players = snapshot_.value as Map;
players['key'] = snapshot_.key;
return FutureBuilder<DataSnapshot>(
builder: (BuildContext context, snapshot) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: Icon(players['isReady'] ? Icons.check_circle : Icons.person_outline),
title: Text(players['name']),
subtitle: Text(players['isReady'] ? 'is ready' : 'is not ready yet'),
),
],
);
},
);
}),
)
],
),
);
}

Related

I am trying to make a grocery app using flutter and firebase, everything is working but when I press the checkbox it Checks all of them

I made a floatingactionbutton and every time you press it it adds an item, and each item has a checkbox next to it but when I check off one item it checks all of them, I've spent a lot of time trying to figure out how to fix this but I can't. I could really use your help.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(FireApp());
}
class FireApp extends StatefulWidget {
#override
_FireAppState createState() => _FireAppState();
}
bool isChecked = false;
class _FireAppState extends State<FireApp> {
final TextController = TextEditingController();
#override
Widget build(BuildContext context) {
CollectionReference groceries =
FirebaseFirestore.instance.collection('groceries');
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: TextField(
controller: TextController,
),
),
body: Center(
child: StreamBuilder(
stream: groceries.orderBy('name').snapshots(),
builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) {
return ListView(
children: snapshot.data!.docs.map((grocery) {
return Center(
child: Row(
children: [
Container(color: Colors.red,height: 50,child: Text(grocery['name'])),
Checkbox(
materialTapTargetSize: MaterialTapTargetSize.padded,
value: isChecked,
activeColor: Colors.black,
checkColor: Colors.greenAccent,
onChanged: (bool) {
setState(() {
isChecked = !isChecked;
});
}
)],
),
);
}).toList(),
);
},
),
),
floatingActionButton: FloatingActionButton(onPressed: () {
groceries.add({
'name': TextController.text,
});
},),
),
);
}
}
You are using the same variable for all your checkboxes (isChecked) but you ougth to have one per data, you could add that attribute to your firebase document so its synced or you could create it locally but each time your stream updates you will need to compare what grocery correspond to a checkbox value which can be hard.
UPDATE
The easiest way is to have a bool parameter in your Firestore document
Then just push an update any time the user tap
return ListView(
children: snapshot.data!.docs.map((grocery) {
return Center(
child: Row(
children: [
Container(color: Colors.red,height: 50,child: Text(grocery['name'])),
Checkbox(
materialTapTargetSize: MaterialTapTargetSize.padded,
value: grocery['checked'],
activeColor: Colors.black,
checkColor: Colors.greenAccent,
onChanged: (val) async {
final data = grocery.data();
data['checked'] = val;
await grocery.reference.update(data);
}
)],
),
);
}).toList(),
);
For now this is sufficient to answer your question, you will see later that this incurs in more Firestore calls, unnecesary rebuild of all widgets in the list and so on and you will have to think another way to optimize resources, like watching the stream somewhere else to have a local List of bools that keeps in sync all values of the groceries so you only update locally with an setState and once in the cloud at the end (a save button perhaps)

Stream data is not refreshing when logging out and logging in with different user id in flutter app

i have a list of events which i am pulling from firestore for a particular user id.
When user logouts and a different user logs in, i need stream to refresh and show appropriate events for the new user.
But list of events is not refreshed and it stills shows data of last user. I have to run "flutter run" again to show data for current user.
Please help.
String uida = FirebaseAuth.instance.currentUser!.uid;
class eventScreen extends StatelessWidget {
const eventScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final authService = Provider.of<AuthClass>(context);
final List storedocs = [];
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.logout),
tooltip: 'List of your activities',
onPressed: () {
authService.signout();
},
),
title: const Text('Activity list'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.add),
tooltip: 'List of your events',
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => eventadd()),
);
},
),
],
),
body: StreamBuilder(
stream: FirebaseFirestore.instance
.collection('events')
.doc(uida)
.collection('events')
.snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text("Loading");
}
return ListView(
children: snapshot.data!.docs.map((DocumentSnapshot document) {
// final trip = event.fromSnapshot(document);
Map a = document.data() as Map<String, dynamic>;
storedocs.add(a);
a['id'] = document.id;
return Container(
child: Card(
elevation: 5,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomRight: Radius.circular(10),
topRight: Radius.circular(10)),
side: BorderSide(width: 3, color: Colors.blueAccent)),
child: ListTile(
onTap: () {
var docId = event.fromSnapshot(document);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
editevent(data: docId)));
},
leading: const Icon(Icons.description),
/* onPressed: (){
var docId = event.fromSnapshot(document);
Navigator.push(context, MaterialPageRoute(
builder: (context) => editevent(data: docId)));}*/
title: Text(a['title']),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
var docId = event.fromSnapshot(document);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
editevent(data: docId)));
}),
/*ElevatedButton(
onPressed: (){
var docId = document.id;
Navigator.push(context, MaterialPageRoute(
builder: (context) => editevent(docId)));
}, child: Text('press'),
)*/
)));
}).toList(),
);
},
),
);
}
}
You should not do this:
StreamBuilder(stream: FirebaseFirestore.instance.collection('events').doc(uida).collection('events').snapshots()
because you make a request with every rebuild. Depending on your widget structure, you may end up making hundreds of unnecessary requests and may have to pay a large Firebase bill. Instead you should create a Stream _stream; field in your class, initialize it in initState() as such:
_stream = FirebaseFirestore.instance.collection('events').doc(uida).collection('events').snapshots();
and use it as such
StreamBuilder(stream: _stream
Then, this gives you the chance to do this whenever you want to change the stream, for example when you change users:
setState(() {
_stream = FirebaseFirestore.instance.collection('events').doc(uida).collection('events').snapshots();
});
This way the stream will be renewed and you will see your screen use the new user's stream.
Looks like you're storing the user id in a global variable which will only store the value you give it in the first instance unless you actually call the change again.
Solution:
You should put the code to get the current user id in your widget so that it always gets the current value when the widget is shown.
class eventScreen extends StatelessWidget {
const eventScreen({Key? key}) : super(key: key);
String uida = FirebaseAuth.instance.currentUser!.uid; // Move this here
#override
Widget build(BuildContext context) {
...
}
}

AutoDisposeStreamProvider is not being disposed at loggin out

Currently, we are using Firebase to implement a simple chat on our application.
We handle the application's launch and authentication with Riverpod.
Launching goes like as follows:
#override
Widget build(BuildContext context) {
LocalNotificationService()
.handleApplicationWasLaunchedFromNotification(_onSelectNotification);
LocalNotificationService().setOnSelectNotification(_onSelectNotification);
_configureDidReceiveLocalNotification();
// final navigator = useProvider(navigatorProvider);
final Settings? appSettings = useProvider(settingsNotifierProvider);
final bool darkTheme = appSettings?.darkTheme ?? false;
final LauncherState launcherState = useProvider(launcherProvider);
SystemChrome.setEnabledSystemUIOverlays(
<SystemUiOverlay>[SystemUiOverlay.bottom],
);
return MaterialApp(
title: 'Thesis Cancer',
theme: darkTheme ? ThemeData.dark() : ThemeData.light(),
navigatorKey: _navigatorKey,
debugShowCheckedModeBanner: false,
home: Builder(
builder: (BuildContext context) => launcherState.when(
loading: () => SplashScreen(),
needsProfile: () => LoginScreen(),
profileLoaded: () => MainScreen(),
),
),
);
}
Currently, we just enable logging out from main screen and rooms screen as follows:
ListTile(
leading: const Icon(Icons.exit_to_app),
title: const Text('Çıkış yap'),
onTap: () =>
context.read(launcherProvider.notifier).signOut(),
),
Where signOut does:
Future<void> signOut() async {
tokenController.state = '';
userController.state = User.empty;
await dataStore.removeUserProfile();
_auth.signOut();
state = const LauncherState.needsProfile();
}
The problem is, every time we goes to the RoomsPage and we do logout from it or from the main page (coming back from rooms), we get the same problem with firebase:
The caller does not have permission to execute the specified operation..
Of course, signout closes the Firebase, thence Firebase throws this error; but, it is supposed after coming out from the RoomsScreen (it happens even when go back to the main screen), this widget is disposed therefore the connection should be closed, disposed, but it seems it is still on memory.
The RoomPage screen is as follows:
class RoomsPage extends HookWidget {
#override
Widget build(BuildContext context) {
final AsyncValue<List<fc_types.Room>> rooms =
useProvider(roomsListProvider);
return Scaffold(
appBar: Header(
pageTitle: "Uzmanlar",
leading: const BackButton(),
),
endDrawer: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 275),
child: SideMenu(),
),
body: rooms.when(
data: (List<fc_types.Room> rooms) {
if (rooms.isEmpty) {
return Container(
alignment: Alignment.center,
margin: const EdgeInsets.only(
bottom: 200,
),
child: const Text('No rooms'),
);
}
return ListView.builder(
itemCount: rooms.length,
itemBuilder: (
BuildContext context,
int index,
) {
final fc_types.Room room = rooms[index];
return GestureDetector(
onTap: () => pushToPage(
context,
ChatPage(
room: room,
),
),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Row(
children: <Widget>[
Container(
height: 40,
margin: const EdgeInsets.only(
right: 16,
),
width: 40,
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
child: Image.network(room.imageUrl ?? ''),
),
),
Text(room.name ?? 'Room'),
],
),
),
);
},
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (Object error, StackTrace? stack) => ErrorScreen(
message: error.toString(),
actionLabel: 'Home',
onPressed: () => Navigator.of(context).pop(),
),
),
);
}
}
And the provider is simple:
final AutoDisposeStreamProvider<List<fc_types.Room>> roomsListProvider =
StreamProvider.autoDispose<List<fc_types.Room>>(
(_) async* {
final Stream<List<fc_types.Room>> rooms = FirebaseChatCore.instance.rooms();
await for (final List<fc_types.Room> value in rooms) {
yield value;
}
},
name: "List Rooms Provider",
);
I suppose the AutoDispose constructor makes this provider auto disposed when the widget is removed, so, it should close the connection with Firebase (as de documentation says).
WHat's the problem here?
What am i missing?
Should i open an issue about this?
In the documentation, the example is using a Stream based on a StreamController
final messageProvider = StreamProvider.autoDispose<String>((ref) async* {
// Open the connection
final channel = IOWebSocketChannel.connect('ws://echo.websocket.org');
// Close the connection when the stream is destroyed
ref.onDispose(() => channel.sink.close());
// Parse the value received and emit a Message instance
await for (final value in channel.stream) {
yield value.toString();
}
});
In your case, your method is returning a Stream. This changes the game rules. Just return the Stream.
final AutoDisposeStreamProvider<List<fc_types.Room>> roomsListProvider =
StreamProvider.autoDispose<List<fc_types.Room>>(
(_) => FirebaseChatCore.instance.rooms(),
name: "List Rooms Provider",
);
Edit:
As you cannot cancel a Stream directly, you could just forward the FirebaseCore.instance.rooms() and let the provider do the cleanup:
final AutoDisposeStreamProvider<List<fc_types.Room>> roomsListProvider =
StreamProvider.autoDispose<List<fc_types.Room>>(
(_) => FirebaseChatCore.instance.rooms(),
name: "List Rooms Provider",
);
Previous Answer:
autoDispose only closes the provided Stream itself (the one you create by using async*), but you will still need too close the Firebase stream yourself.
You can use onDispose() as shown in the Riverpod documentation
ref.onDispose(() => rooms.close());

StreamBuilder not updating after an item is removed Flutter

I am new to Flutter and this is my first time asking a question on Stackoverflow. I apologize for any misunderstanding. I will try my best to make it clear.
I am using sqflite for storing user's favorites and populating a list from the DB on a page, named Favorites screen. This Favorites page is one of the items on my bottom navbar.
My issue is that when I tap on an item from the favorites list which takes me to a screen where I can unfavorite that item. I double-checked that it is really removed from the DB by logging the rows count. But when I go back to the Favorites page, that item is still on the list. If I go to one of the pages from the bottom navbar and go back to the Favorites screen, the item isn't there. I understand that the page is being rebuilt again this time but my intention was the Stream will constantly listen for a change.
I have also implemented a slide to dismiss feature on the fav screen, which works as intended. But I am using the same logic on both.
StreamBuilder code in Favorite screen
StreamBuilder<List<WeekMezmurList>>(
stream: favBloc.favStream,
builder: (context, AsyncSnapshot<List<WeekMezmurList>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: Text(
"Loading Favorites...",
style: TextStyle(fontSize: 20),
),
);
} else if (snapshot.data == null) {
return Center(
child: Text(
"No Favorites yet!",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
);
} else {
return ListView.builder(
physics: BouncingScrollPhysics(),
padding: const EdgeInsets.fromLTRB(5.0, 10.0, 5.0, 10.0),
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return new GestureDetector(
onTap: () =>
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
AudioPlayerScreen(
mezmurName: snapshot.data[index].mezmurName,
),
),
),
child: Slidable(
key: new Key(snapshot.data[index].mezmurName),
actionPane: SlidableDrawerActionPane(),
actionExtentRatio: 0.25,
// closes other active slidable if there is any
controller: slidableController,
secondaryActions: <Widget>[
IconSlideAction(
caption: 'Share',
color: Colors.indigo,
icon: Icons.share,
onTap: () =>
_share(snapshot
.data[index]),
),
IconSlideAction(
caption: 'Delete',
color: Colors.red,
icon: Icons.delete,
onTap: () =>
_swipeDelete(
context, snapshot.data[index].mezmurName),
),
],
child: Card(
color: Colors.white,
child: Padding(
padding: EdgeInsets.symmetric(
vertical: 15.0,
horizontal: 10.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Row(
children: <Widget>[
_misbakChapter(
snapshot.data[index].misbakChapters),
SizedBox(width: 15),
_displayFavoritesMisbakLines(
snapshot.data[index], index),
],
)
],
),
),
),
),
);
},
);
}
},
);
slide to delete code in Favorites screen
// deletes the specific favorite from the sqflite db
Future<void> _swipeDelete(BuildContext context, String mezmurName) async {
try {
favBloc.delete(mezmurName);
} catch (e) {
CupertinoAlertDialog(
content: Text("Something went wrong. Please try again."),
actions: <Widget>[
CupertinoDialogAction(
child: Text(
"Ok",
),
onPressed: () => Navigator.of(context).pop(),
),
],
);
}
}
I have the same logic in the second screen, the screen I get when I tap on one of the items from the Fav list.
favBloc.delete(widget.mezmurName);
BLoC code, I got the concepts from this Medium article
class FavoritesBloc{
FavoritesBloc(){
getFavorites();
}
final databaseHelper = DatabaseHelper.instance;
// broadcast makes it to start listening to events
final _controller = StreamController<List<WeekMezmurList>>.broadcast();
get favStream => _controller.stream;
void dispose() {
_controller.close();
}
getFavorites () async{
_controller.sink.add(await databaseHelper.getFavorites());
}
insert(WeekMezmurList fav){
databaseHelper.insertToDb(fav);
getFavorites();
}
delete(String mezmurName){
databaseHelper.delete(mezmurName: mezmurName);
getFavorites();
}
}
Delete method in the DB class
// deleting a value from the db
delete({String mezmurName}) async {
var dbClient = await getDb;
try {
await dbClient
.delete(TABLE, where: '$MEZMUR_NAME = ?', whereArgs: [mezmurName]);
} catch (e) {
}
}
I have tried to research this issue but all I have found were for remote databases.
Just to make it more clear, I took a screen record.
Thank you in advance!
The reason why StreamBuilder on the first screen doesn't update with the changes made is because it uses a different instance of FavoritesBloc(). If you'd like for the bloc to be globally accessible with a single instance, you can declare it as
final favBloc = FavoritesBloc();
Otherwise, you can follow what has been suggested in the comments and pass FavoritesBloc as an argument between screens.

Flutter: Why doesn't this streambuilder work?

So, I have just began working on a flutter project and am quite new to the whole experience. I just managed to integrate firebase firestore into my project by creating a few buttons that update, remove, and add documents. However, I also wanted to add a Streambuilder with the list that is being updated on the same page. I tried each task seperately, and they all work fine and dandy, however when I combine the two, the streambuilder shows no data and the buttons won't click. How do I incorporate both buttons and a Streambuilder in one body, or one page? What can I do to combine both of these elements onto one page in the widget build method? Again, the two elements seem to be working okay by themselves if I use the Streambuilder in the body and not a children widget tag.
A picture of what the not working page looks like. Notice how the buttons are not being selected when hovered over and the streambuilder is loading infinitely: https://i.stack.imgur.com/XnfVJ.png
Firebase data screenshot (security settings allow users to access the data): https://i.stack.imgur.com/oSsOL.png
Here is my code for main.dart:
final databaseReference = Firestore.instance;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('FireStore Demo'),
),
body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
RaisedButton(
child: Text('Create Record'),
onPressed: () {
createRecord();
},
),
RaisedButton(
child: Text('View Record'),
onPressed: () {
getData();
},
),
RaisedButton(
child: Text('Update Record'),
onPressed: () {
updateData();
},
),
RaisedButton(
child: Text('Delete Record'),
onPressed: () {
deleteData();
},
),
StreamBuilder<QuerySnapshot>(
stream: databaseReference.collection('books').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return new Text('Loading...');
return new ListView(
children: snapshot.data.documents.map((DocumentSnapshot document) {
return new ListTile(
title: new Text(document['title']),
subtitle: new Text('${document['description']} description'),
);
}).toList(),
);
},
)
],
)),
//center
);
}
void createRecord() async {
await databaseReference.collection("books")
.document("1")
.setData({
'title': 'Mastering Flutter',
'description': 'Programming Guide for Dart'
});
DocumentReference ref = await databaseReference.collection("books")
.add({
'title': 'Flutter in Action',
'description': 'Complete Programming Guide to learn Flutter'
});
print(ref.documentID);
}
void getData() {
databaseReference
.collection("books")
.getDocuments()
.then((QuerySnapshot snapshot) {
snapshot.documents.forEach((f) => print('${f.data}}'));
});
}
void updateData() {
try {
databaseReference
.collection('books')
.document('1')
.updateData({'description': 'Head First Flutter'});
} catch (e) {
print(e.toString());
}
}
void deleteData() {
try {
databaseReference
.collection('books')
.document('1')
.delete();
} catch (e) {
print(e.toString());
}
}
}
Still don't know why the code above didn't work, but putting the streambuilder within an Expanded block seemed to do the trick! Both widgets are working fine as of now.
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
void main() => runApp(new MediaQuery(
data: new MediaQueryData(), child: new MaterialApp(home: new MyApp())));
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
RaisedButton(
child: Text('Create Record'),
onPressed: () {
createRecord();
},
),
RaisedButton(
child: Text('View Record'),
onPressed: () {
getData();
},
),
RaisedButton(
child: Text('Update Record'),
onPressed: () {
updateData();
},
),
RaisedButton(
child: Text('Delete Record'),
onPressed: () {
deleteData();
},
),
new Expanded(child:
new StreamBuilder<QuerySnapshot>(
stream: databaseReference.collection('books').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return new Text('Loading...');
return new ListView(
children: snapshot.data.documents.map((DocumentSnapshot document) {
return new ListTile(
title: new Text(document['title']),
subtitle: new Text('${document['description']} description'),
);
}).toList(),
);
},
)
)
],
)),
//center
);

Resources