Flutter: How can the position of buttons in a SingleChildScrollView be changed? - button

I would like to have a screen with a changeable amount of buttons. This is realized by a list of MaterialButtons. Because I don't know the amount of the buttons (maybe is too big to display all of them on the screen), I use a SingleChildScrollView to make the list of buttons scrollable. If there are less buttons, so that the screen is not filled completely, the buttons should always be displayed in the middle of the screen. At the moment, the button list always starts at the top of my screen.
Does anybody know how to implement this automatic position adjustment? And is it possible to change the space between the buttons?
This is my code (at the moment there is a fix number of 3 buttons but that will be changed soon):
class PlanState extends State<PlanForm> {
var namesList = new List<String>();
List<MaterialButton> buttonsList = new List<MaterialButton>();
#override
void initState() {
super.initState();
namesList.add("Button1");
namesList.add("Button2");
namesList.add("Button3");
List<Widget> _buildButtonsWithNames() {
int length = namesList.length;
for (int i = 0; i < length; i++) {
buttonsList
.add(new MaterialButton(
height: 40.0,
minWidth: 300.0,
color: Colors.blue,
textColor: Colors.white,
child: new Text(namesList[0], textScaleFactor: 2.0,),
onPressed: () {
Navigator.push(context, new MaterialPageRoute(builder: (context) => new viewPlanScreen()),);
}
));
namesList.removeAt(0);
}
return buttonsList;
}
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
child:Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: _buildButtonsWithNames()
)
)
);
}
}

Because the SilgheChildScrollView has an "infinite" size in the main axis direction you can not center things like in other scenarios. If you check the documentation [1] there are some examples on how to achieve this like centering if there is enough room. I have no tried but should be something like this:
LayoutBuilder(
builder: (BuildContext context, BoxConstraints viewportConstraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: _buildButtonsWithNames(),
)
)
)
}
)
[1] https://api.flutter.dev/flutter/widgets/SingleChildScrollView-class.html

Related

Flutter/Dart/Firebase - wait until data has loaded before displaying

I am trying to create a list of unique "events" in my app. I have created a couple of functions to extract the data from firebase:
// event list from snapshot
List<String> _eventsFromSnapshot(QuerySnapshot snapshot) {
return snapshot.docs.map(
(doc) {
return doc['event'].toString() ?? '';
},
).toList();
}
//get events data
Stream<List<String>> get events {
return productCollection.snapshots().map(_eventsFromSnapshot);
}
I then want to build my list view in another screen. I have implemented my StreamProvider in the root page of my homescreen:
class OurHomePage extends StatefulWidget {
#override
_OurHomePageState createState() => _OurHomePageState();
}
class _OurHomePageState extends State<OurHomePage> {
#override
Widget build(BuildContext context) {
return StreamProvider<List<Product>>.value(
value: OurDatabase().products.handleError((e) {
print(e.toString());
}),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Align(
alignment: Alignment.center,
child: Column(
children: [
OurHeadline(),
AllCards(),
HowItWorks(),
],
),
),
),
),
);
}
}
And then I create a function to return the list of Strings and use that in my stateless widget:
class AllCards extends StatelessWidget {
#override
Widget build(BuildContext context) {
final List<String> uniqueEventList = getListOfEvents(context);
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [Text('Browse all Cards'), Text('Shop All')],
mainAxisAlignment: MainAxisAlignment.spaceBetween,
),
SizedBox(
height: 125,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: uniqueEventList.length,
itemBuilder: (context, i) {
return Container(
decoration: BoxDecoration(
border: Border.all(),
),
width: 160.0,
child: Center(
child: Text(uniqueEventList[i]),
),
);
},
),
)
],
),
),
);
}
List<String> getListOfEvents(BuildContext context) {
final uniqueEvents = Provider.of<List<Product>>(context);
final List<String> list = [];
for (var item in uniqueEvents) {
list.add(item.event);
}
return list.toSet().toList();
}
}
The problem is that whenever I switch pages, for a split second I get this message and an error appears:
The getter 'iterator' was called on null.
Receiver: null
Tried calling: iterator
Which indicates to me that I need to use some sort of async functionality to wait for the events data to finish loading, but is there a simple way to do this without going for something like a Future builder?
Any help would be appreciated!

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

Multiple streams to firebase documents without disposing

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

Why does my Firestore StreamBuilder skip when scrolling up if it's put inside a CustomScrollView?

For some reason, my StreamBuilder skips a whole Container whenever I try scrolling up. However, it only starts skipping Containers when I start scrolling up from a certain point in my Firestore stream. Maybe like 2000 or so pixels down? Either way, the important aspect is that this does NOT happen when I put a normal container in the same scenario-only with StreamBuilders. This only issue only happens when I put it inside a CustomScrollView. (See video below)
This whole thing occurred after I was trying to wrap my StreamBuilder in a Column/ListView so that I could put other Containers above the Stream Containers. Wrapping it in a Column/ListView didn't work because, in a Column, the other container would not scroll down with the rest of the stream. In a ListView, the StreamBuilder had its own scrolling system that was separate.
Basically, my goal is to have one big list of widgets that scroll. There will be some at the top that will be separate from the ones that came from the Firestore (which will be at the bottom). Upon clicking on one of those Firestore Containers, I would be taken to another page.
This is the closest I've gotten to get my goal, but if there's another way, I'd love to hear it.
Video:
Only the GREEN Containers are from the Firestore StreamBuilder. NOT the red or blue ones.
https://youtu.be/jOo08oLhZP8
Code:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:firebase_auth/firebase_auth.dart';
class GeneralTopicsPage extends StatefulWidget {
GeneralTopicsPage({this.user, this.googleSignIn, this.index});
final FirebaseUser user;
final GoogleSignIn googleSignIn;
var index;
#override
_GeneralTopicsPageState createState() => new _GeneralTopicsPageState();
}
class _GeneralTopicsPageState extends State<GeneralTopicsPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
pinned: true,
title: Text(
"Testing",
),
),
SliverPadding(
//Works perfectly fine.
padding: EdgeInsets.all(16.0),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
margin: EdgeInsets.all(20.0),
color: Colors.red,
height: 400.0,
child: Center(
child: Text(
"This is a normal, hardcoded Container in red and it works perfectly fine. Notice the lack of skipping when scrolling up.",
style: TextStyle(
color: Colors.white
),
textAlign: TextAlign.center,
),
),
);
},
childCount: 7,
),
),
),
SliverPadding(
//This is where it doesn't work as intended. Its behavior is separate from the containers located above and below it because it suddenly includes a StreamBuilder.
padding: EdgeInsets.all(16.0),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return StreamBuilder(
stream: Firestore.instance.collection("content").snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) {
return Container(
child: Center(
child: CircularProgressIndicator(),
),
);
} else {
return Container(
margin: EdgeInsets.all(20.0),
color: Colors.green,
height: 400.0,
child: Center(
child: Text(
snapshot.data.documents[index]['title'],
style: TextStyle(
color: Colors.white
),
textAlign: TextAlign.center,
),
),
);
}
},
);
},
childCount: 8,
),
),
),
SliverPadding(
//Works perfectly fine.
padding: EdgeInsets.all(16.0),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
margin: EdgeInsets.all(20.0),
color: Colors.blue,
height: 400.0,
child: Center(
child: Text(
"This is a normal, hardcoded Container in blue and it works perfectly fine. Notice the lack of skipping when scrolling up.",
style: TextStyle(
color: Colors.white
),
textAlign: TextAlign.center,
),
),
);
},
childCount: 3,
),
),
),
],
),
);
}
}

Resources