Make FirebaseAnimatedList auto scroll when have new data - firebase

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.

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)

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/Firestore: How to add items to stream on scroll (preserve scrollposition when done fetching)?

I have a chat (ListView) with messages that I only want to load as needed.
So when the chat is initially loaded I want to load the last n messages and when the user scrolls up I want to fetch older messages also.
Whenever a new message arrives in the firebase collection it should be added to the ListView. I achieved this by using a StreamBuilder that takes the stream of the last n messages where n is a variable stored in the state that I can increase to load more messages (it is an argument to the function that gets the stream of the last n messages).
But with my current implementation the problem is that even though more messages are fetched and added to the listview when I scroll up, it then immediately jumps back to the bottom (because the listview is rebuilt and the scrollposition isn't preserved). How can I prevent this from happening?
This issue is not related to ListView or the scroll position. Those are kept with automatically. The issue must be somewhere else in your code. Check my example below to see how having a list, adding new items and then resetting it, will maintain the scroll position or move to the right place:
class ListViewStream60521383 extends StatefulWidget {
#override
_ListViewStream60521383State createState() => _ListViewStream60521383State();
}
class _ListViewStream60521383State extends State<ListViewStream60521383> {
List<String> _itemList;
#override
void initState() {
resetItems();
super.initState();
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(
child: ListView.builder(
reverse: true,
itemCount: _itemList.length,
itemBuilder: (context, index){
return Container(
height: 40,
child: Text(_itemList[index]),
);
},
),
),
Row(
children: <Widget>[
RaisedButton(
onPressed: addMoreItems,
child: Text('Add items'),
),
RaisedButton(
onPressed: resetItems,
child: Text('Reset items'),
)
],
)
],
);
}
void addMoreItems(){
int _currentListCount = _itemList.length;
setState(() {
_itemList.addAll(List.generate(60, (index) => 'item ${index + _currentListCount}'));
});
}
void resetItems(){
setState(() {
_itemList = List.generate(60, (index) => 'item $index');
});
}
}
Using FirestoreListView you do that easily.
Refer this for more info https://www.youtube.com/watch?v=si6sTuVZxtw

Using async methods in initState() flutter

I am using firebase and need to get the id of the user inside my initState() method as I am building a widget that requires the id of the user, from firebase. The currentUser method that firebase uses is a Future.
Currently, I have this as my init state and getUser function:
Future<FirebaseUser> getUser() async {
FirebaseUser user = await FirebaseAuth.instance.currentUser();
return user;
}
void initState() {
getUser().then((result) {
stream = Firestore.instance.collection('Lessons').where("teacher", isEqualTo: result.uid).limit(4).snapshots();
_AppBarOptions = <Widget>[
AppBar(
title: new Text('Dashboard', style: TextStyle(color: Colors.white)),
backgroundColor: Colors.white,
),
AppBar(
title: new Text('Lessons', style: TextStyle(color: Colors.white))),
AppBar(title: new Text('Tasks', style: TextStyle(color: Colors.white))),
AppBar(
title: new Text('Settings', style: TextStyle(color: Colors.white)))
];
_widgetOptions = <Widget>[
Text(
'Dashboard will go here',
style: optionStyle,
),
StreamBuilder(
stream: stream,
builder: (context, snapshot) {
if(snapshot.hasData) {
return ListView.builder(
itemExtent: 80.0,
itemCount: snapshot.data.documents.length,
itemBuilder: (context, index) =>
_buildListItem(context, snapshot.data.documents[index]),
);
} else {
return Text('You have no Lessons');
}
}),
Text(
'Tasks will go here',
style: optionStyle,
),
Container(
child: new RaisedButton(
child: new Text('Sign Out'),
onPressed: signOut,
),
)
];
});
}
The problem that I am facing is that the build method is executed before this finishes, which uses the widgetOptions to display the text, resulting in an error.
I was wondering if there was any way to improve this as to be able to get the id of the user and use it in stream so that it can fetch the documents?
Thanks very much in advance.
For such cases Flutter has FutureBuilder widget.
It "postpones" building of a UI piece to the moment when the provided Future completes.
Read the API docs of this class, view some examples and you will resolve your problem.
As a result you will unload the part of your initState method where you construct the list of options to the builder argument of the FutureBuilder.
To mitigate this problem, you can use a boolean and setState() to change its value. Once you have the id of the user, then you can build the widget that needs the user id if the boolean is true, and otherwise make the widget a progress indicator or simply "loading" text.

Resources