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());
Related
When a user logs into my flutter app, they have to log in, then they are brought to a screen with a feed of posts. I use a ListView.builder to take a list of posts from my database and create the feed of posts. My issue is that when the feed screen is initially launched, the ListView doesn't load. As soon as I hot-reload the app the list does load. I imagine there's a very obvious minor mistake in my code but I just can't find it. I will put all of the code from the feed screen below, please take a look and let me know if you see the mistake.
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
static const String id = "home_screen";
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// List allposts = [(post: Post, owner: String)];
Color _likeButtonColor = Colors.black;
Widget _buildPost(String username, String imageUrl, String caption) {
return Container(
color: Colors.white,
child: Column(
children: [
Container(
height: 50,
color: Colors.deepOrangeAccent[100],
child: Row(
children: [
SizedBox(width: 5),
CircleAvatar(),
SizedBox(width: 5),
Text(username, style: TextStyle(fontSize: 15)),
SizedBox(width: 225),
Icon(Icons.more_horiz)
],
),
),
Stack(
children: [
Image.asset("images/post_background.jpg"),
Padding(
padding: const EdgeInsets.all(20.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: Image.network(imageUrl, fit: BoxFit.cover)),
),
],
),
Container(
height: 100,
child: Column(
children: [
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
onPressed: () {
setState(() {
HapticFeedback.lightImpact();
});
},
icon: Icon(Icons.thumb_up_alt_outlined, size: 30)),
Text("l", style: TextStyle(fontSize: 30)),
Icon(Icons.ios_share, size: 30)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(caption, style: const TextStyle(fontSize: 15))
],
)
],
),
)
],
),
);
}
List<Post> listPosts = [];
fetchPosts() async {
final userRef = FirebaseFirestore.instance.collection('users');
final QuerySnapshot result = await userRef.get();
result.docs.forEach((res) async {
print(res.id);
QuerySnapshot posts = await userRef.doc(res.id).collection("posts").get();
posts.docs.forEach((res) {
listPosts.add(Post.fromJson(res.data() as Map<String, dynamic>));
});
});
}
#override
void initState() {
fetchPosts();
print(listPosts);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: listPosts.length,
itemBuilder: (BuildContext context, int index) {
// We retrieve the post at index « index »
final post = listPosts[index];
// Replace with your actual implementation of _buildPost
return _buildPost(post.id, post.postUrlString, post.caption);
}),
);
}
}
The reason is that you need to rebuild your screen to show the reflected changes after performing an async operation (use setState to rebuild the UI). And secondly .forEach loop is not built to carry async stuff and are less efficient then a normal for loop so its better to change it.
fetchPosts() async {
final userRef = FirebaseFirestore.instance.collection('users');
final QuerySnapshot result = await userRef.get();
for(var res in result.docs)async{
print(res.id);
QuerySnapshot posts = await userRef.doc(res.id).collection("posts").get();
posts.docs.forEach((res) {
listPosts.add(Post.fromJson(res.data() as Map<String, dynamic>));
});
}
setState((){});//call it after end of your function
}
Ps:- You can use a variable named loading to show progress indicator and set it to false after fetching data in setState.
I have an admin sign in page where admins can add an id and password to gain access to the Admin area. There is an admin collection in the Firebase Database that stores the id and password. The admin collection is separate from the user sign in page and separate from the user collection which uses Firebase Authentication before allowing access. The user access continues to work correctly. When I fill in the two input boxes on the admin sign in screen and click the button to gain access my error dialog message appears indicating that there isn't any data in the two input fields even though there is data. If I do nothing to the code and then hot reload and click the button again I am able to access the admin but I get the following error message in the console.
The following _TypeError was thrown building ShoppingAdminSignInPage(dirty, dependencies: [_LocalizationsScope-[GlobalKey#2c797]], state: _ShoppingAdminSignInPageState#e3b3d):
type 'Null' is not a subtype of type '() => void'
I have obviously written something or several things incorrectly in my code. It appears the error is in the ShoppingAdminSignInButton. Thank you in advance for any help.
class ShoppingAdminSignInPage extends StatefulWidget {
const ShoppingAdminSignInPage({Key? key}) : super(key: key);
#override
State<ShoppingAdminSignInPage> createState() =>
_ShoppingAdminSignInPageState();
}
class _ShoppingAdminSignInPageState extends State<ShoppingAdminSignInPage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _adminIDController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
#override
Widget build(BuildContext context) {
return AdaptiveLayoutScaffold(
appBar: const ShoppingAdminSignInPageAppBar(),
landscapeBodyWidget: Container(),
portraitBodyWidget: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
const ShoppingAdminSignInHeader(),
Form(
key: _formKey,
child: Column(
children: [
const SizedBox(
height: 50.0,
),
AdminSignInTextField(
controller: _adminIDController,
labelText: TextFieldLabel.adminID,
prefixIcon: Icons.person,
textInputAction: TextInputAction.next,
),
AdminSignInTextField(
controller: _passwordController,
labelText: TextFieldLabel.password,
prefixIcon: Icons.password,
textInputAction: TextInputAction.done,
),
ShoppingAdminSignInButton(
onPressed: _adminIDController.text.isNotEmpty &&
_passwordController.text.isNotEmpty
? logInAdmin()
: () => showDialog(
context: context,
builder: (ctx) {
return const ErrorAlertDialog(
message: DialogString.addAdminIDAndPassword,
);
}),
),
const NotAnAdminButton(),
],
),
),
],
),
),
),
);
}
logInAdmin() {
FirebaseFirestore.instance.collection('admins').get().then((snapshot) {
snapshot.docs.forEach((result) {
if (result.data()['id'] != _adminIDController.text.trim()) {
SnackBarUtil.showSnackBar(
context,
SnackBarString.idNotCorrect,
);
} else if (result.data()['password'] !=
_passwordController.text.trim()) {
SnackBarUtil.showSnackBar(
context,
SnackBarString.passwordNotCorrect,
);
} else {
SnackBarUtil.showSnackBar(
context,
'Welcome ${result.data()['name']}',
);
setState(() {
_adminIDController.text = '';
_passwordController.text = '';
});
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const UploadItemsPage(),
),
);
}
});
});
}
}
onPressed: _adminIDController.text.isNotEmpty && _passwordController.text.isNotEmpty
? logInAdmin()
: () => showDialog(
above, what you are saying is if a condition is true (in this case the condition is both _adminIDController and _passwordController to not be empty) then it should run logInAdmin and wait for it to finish and then run whatever logInAdmin returned.
Dart thinks logInAdmin will return a function and it should run that function. This is not the case, you want the button to directly run logInAdmin.
To fix this, remove the parenthesis:
onPressed: _adminIDController.text.isNotEmpty && _passwordController.text.isNotEmpty
? logInAdmin
: () => showDialog(
This way, you are not assigning the result of the function, you are assigning the function itself.
Also as a general recommendation, you should always declare a return type on your functions so dart can tell you whenever this happens
void logInAdmin() {
...
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)
I've try to build Chat example on Flutter, but I have problem, how I can make FirebaseAnimatedFlutter auto scroll when have new data populate ?
Example: When I submit new chat message for my friend, from my side, I can call this method to auto scroll:
Timer(Duration(milliseconds: 100), () {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 100),
curve: Curves.easeOut);
});
But at my friend side, he still need manual scroll to end to see new message
So, there are anyway to detect and auto scroll to end of FirebaseAnimatedList when we receive new data ?
Thank you
I can't see all your code, but there is a trick you can do that will avoid having to add extra code. It involves reversing the data in the list of messages and setting to true the reverse property of the ListView. This will make the messages move up as new messages come in.
You reverse the original list, you set to true the reverse property of the ListView, and when you add messages to your List you use messages.insert(0, newMessage) to add it to the top (now bottom because of inversion), instead of of messages.add.
class Issue65846722 extends StatefulWidget {
#override
_Issue65846722State createState() => _Issue65846722State();
}
class _Issue65846722State extends State<Issue65846722> {
List<String> messages = [
'message 1',
'message 2',
'message 3',
].reversed.toList();
TextEditingController textEditingController = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('StackOverflow'),
),
floatingActionButton: Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(top: 100.0),
child: FloatingActionButton(
// To simulate an incoming message from another source that is not
// the local TextField
child: Icon(Icons.message),
onPressed: () => newMessage('new message'),
),
),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
reverse: true,
itemCount: messages.length,
itemBuilder: (context, index){
return Container(
child: Text(messages[index]),
);
},
),
),
Divider(color: Colors.black,),
TextFormField(
controller: textEditingController,
onFieldSubmitted: (_) => submitMessage()
),
],
),
);
}
void submitMessage(){
newMessage(textEditingController.text);
textEditingController.clear();
}
void newMessage(String newMessage){
setState(() {
messages.insert(0, newMessage);
});
}
}
thank for useful answer of João Soares, i already solve this problem by 2 step
Reverse data from Firebase by use 'sort' property of FirebaseAnimatedList
Set 'reverse' property to 'true' in FirebaseAnimatedList
And work like a charm
FirebaseAnimatedList(
query: loadChatContent(context, app),
sort: (DataSnapshot a,DataSnapshot b) => b.key.compareTo(a.key), //fixed
reverse: true, //fixed
Just wrap your FirebaseAnimatedList with Flexible Widget & thats it.
This worked for me.
I am trying to add chatting to my app.
When my user starts a chat with a new user, I create a chatroom with a unique id in my Firebase database. I want my user to be notified with any updates of the chatroom document( eg: new messages) so when I create a chatroom, I also create a new stream to that chatroom document. Is there a problem with constantly listening to many documents with different streams without disposing the streams (because I want to get the latest results of any chatroom that user is a member of.)
This is my chatroom_screen code:
import 'package:flutter/material.dart';
import '../models/databse_management.dart';
class ChatroomScreen extends StatefulWidget {
static const String routeName = "/chatroom_screen";
#override
_ChatroomScreenState createState() => _ChatroomScreenState();
}
class _ChatroomScreenState extends State<ChatroomScreen> {
TextEditingController _messageTextEditingController = TextEditingController();
bool isChatroomExists;
Stream chatroomDocStream; //my initial stream variable which is null
#override
Widget build(BuildContext context) {
final Map<String, dynamic> passedArguments =
ModalRoute.of(context).settings.arguments;
//sedner details can be retrieved from currentUserDetails
final String senderId = passedArguments["senderId"];
final List<String> receiversIds = passedArguments["receiverIds"];
final String senderUsername = passedArguments["senderUsername"];
final List<String> receiverUsernames = passedArguments["receiverUsernames"];
final String chatroomId = passedArguments["chatroomId"];
if(chatroomDocStream == null){
chatroomDocStream = Firestore.instance.collection("chatrooms").document(chatroomId).snapshots(); //if no stream was created before (first time we build the widget), we connect a stream to this chatroom documentspecified with chatroomId
}
isChatroomExists = isChatroomExists == null
? passedArguments["isChatroomExists"]
: isChatroomExists;
final Image senderProfilePictureUrl =
passedArguments["senderProfilePicture"];
final List<Image> receiverProfilePictures =
passedArguments["receiverProfilePictures"];
final mediaQuery = MediaQuery.of(context).size;
final ThemeData theme = Theme.of(context);
//we get the values from the passed argument map
if (isChatroomExists) {
//load the previous chats
}
return Scaffold(
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
CircleAvatar(
backgroundImage: receiverProfilePictures[0]
.image, //right now only for 1 receiver but change it for multi members later
),
Container(
child: Text(receiverUsernames[0]),
margin: const EdgeInsets.only(left: 10),
),
],
),
),
body: //to do=> create a stream that is listening to the chatroom document for any changes
Stack(
children: <Widget>[
StreamBuilder(
//updates the chats whenever data of the chatroom document changes
stream: chatroomDocStream,
builder: (context, snapShot) {
return Column(
children: <Widget>[
Center(child: const Text("My chats"))
//message widgets go here
],
);
},
),
Positioned(
//positioned is used for positioning the widgets inside a stack. bottom: 10 means 10 pixel from bottom
bottom: 0,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
width: mediaQuery.width, //takes the total width of the screen
decoration: BoxDecoration(
color: theme.primaryColor.withOpacity(0.3),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
//always takes the remaining space (textField widget use this as its size https://stackoverflow.com/questions/45986093/textfield-inside-of-row-causes-layout-exception-unable-to-calculate-size)
child: Container(
padding: const EdgeInsets.symmetric(
horizontal:
10), //horizontal padding for the textfield widget
decoration: BoxDecoration(
color: theme.accentColor,
borderRadius: BorderRadius.circular(25),
border: Border.all(width: 2, color: theme.primaryColor),
),
child: TextField(
minLines: 1,
maxLines: 5,
controller: _messageTextEditingController,
decoration: const InputDecoration(
hintText: "Type something here...",
border: InputBorder
.none //removes all the border for textfield widget
),
),
),
),
Container(
child: Row(
//another row for buttons to be treated all toghether as a container in the parent row
children: <Widget>[
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
//add media like image or pictures
},
),
IconButton(
icon: const Icon(Icons.camera_alt),
onPressed: () {
//take picture or video
},
),
IconButton(
icon: const Icon(Icons.send),
onPressed: () async {
if (_messageTextEditingController.text
.trim()
.isEmpty) {
return;
}
try {
await DatabaseManagement().sendMessage(
membersIds: [
senderId,
...receiversIds
], //extracts all the members of the list as seperate String items
//to do=>later on we have to get a list of the members as well
isChatroomExists: isChatroomExists,
chatroomId: chatroomId,
messageContent:
_messageTextEditingController.text,
senderId: senderId,
timestamp: Timestamp
.now(), //it's from firebase and records the time stamp of the sending message
);
if (isChatroomExists != true) {
isChatroomExists =
true; //after we sent a messsage, we 100% created the chatroom so it becomes true
}
} catch (e) {
print(
e.toString(),
);
return;
}
},
)
],
),
)
],
),
),
),
],
),
);
}
}
The idea is to have something like WhatsApp which you receive a notification of any updates in any chatroom you are a member of.
You can create a lot of snapshot listeners, but Google recommends a limit of 100 snapshot listeners per client:
Source: https://cloud.google.com/firestore/docs/best-practices?hl=en#realtime_updates