I built an E-commerce app using Firebase/Firestore and I am using Stream Builder or Future Builder and facing high Firestore reads.
When I press on Hot-Reload all Stream Builders in the app rebuild (I am using the print method to track the rebuilding).
The hot-reload rebuilds the previous screen widget (Includes Stream Builder or Flutter Builder), after Navigator.push.
I need to reduce Firestore high reads and avoid many rebuilds.
Can someone help me to fix it?
Example:
class _SliderView extends StatefulWidget {
const _SliderView({
required this.slidersItems,
Key? key,
}) : super(key: key);
final Future<List<SliderModel>> slidersItems;
#override
State<_SliderView> createState() => _SliderViewState();
}
class _SliderViewState extends State<_SliderView> {
final CarouselController _carouselController = CarouselController();
int currentIndex = 0;
int nextPage = 0;
late int offersLength;
#override
void initState() {
super.initState();
}
#override
void dispose() {
_carouselController.stopAutoPlay();
super.dispose();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<List<SliderModel>>(
future: widget.slidersItems,
builder: (context, snapshot) {
print("## Home - Slider Viewer ##");
if (snapshot.data != null) {
return SizedBox(
width: double.infinity,
child: Stack(
children: [
NotificationListener<OverscrollIndicatorNotification>(
onNotification: (scroll) {
scroll.disallowIndicator();
return false;
},
child: CarouselSlider.builder(
carouselController: _carouselController,
options: CarouselOptions(
viewportFraction: 1.0,
height: ScreenSize.screenHeight,
autoPlay: snapshot.data!.length > 1 ? true : false,
onPageChanged: (val, reason) {
setState(() {
currentIndex = val;
});
},
),
itemCount: snapshot.data!.length,
itemBuilder: (context, index, eall) {
// ProductModel productModel =
// ProductModel.fromJson(fetched.data());
// SliderModel sliderModel = SliderModel.fromMap(fetched.data());
// sliderModel.itemPriceWithDiscount == 0.0
// ? sliderModel.prodPrice.toString()
// : sliderModel.itemPriceWithDiscount.toString();
return GestureDetector(
onTap: () {
// Navigator.pushNamed(
// context,
// Routes.productDetails,
// arguments: productModel,
// );
},
child: SizedBox(
width: ScreenSize.screenWidth!,
child: snapshot.data![index].itemImageUrl == null ||
snapshot.data![index].itemImageUrl!.isEmpty
? const Center(
child: CustomLogo(size: 133),
)
: CachedNetworkImage(
width: double.maxFinite,
imageUrl: snapshot.data![index].itemImageUrl
.toString(),
fit: BoxFit.cover,
placeholder: (context, url) {
return const CustomProgressIndicator();
},
errorWidget: (context, url, error) {
return const Center(
child: CustomLogo(),
);
},
),
),
);
},
),
),
Positioned(
bottom: 10,
child: BuildIndicators(
itemIndex: snapshot.data!.length,
currentIndex: currentIndex,
),
),
],
),
);
} else {
return const CustomProgressIndicator();
}
},
);
}
}
Related
I'm new to Flutter, I started making a Slider through bloc and writing data to the fireStore, but I ran into a problem - when I change the value in the Slider, the page refreshes and freezes at the Loading stage, this is profile_screen. How can I fix the problem so that the page is not updated in real time, but only by clicking on the Save button?
profile_screen
double _currentSliderValue = 1;
#override
Widget build(BuildContext context) {
// final ProfileBloc infoBloc = context.read<ProfileBloc>();
ProfileCubit cubit = ProfileCubit.get(context);
return Scaffold(
body: Container(
child: Padding(
padding: const EdgeInsets.all(50),
child: BlocBuilder<ProfileCubit, ProfileState>(
builder: (context, state) {
if (state is ProfileInitial) {
return const Center(
child: Text('No Data'),
);
}
if (state is ProfileLoadingState) {
return CircularProgressIndicator();
}
if (state is ProfileLoadedState) {
return FutureBuilder<DocumentSnapshot>(
future: cubit.getInfo(),
builder: (BuildContext context,
AsyncSnapshot<DocumentSnapshot> snapshot) {
if (snapshot.hasError) {
return Text("Something went wrong");
}
if (snapshot.hasData && !snapshot.data!.exists) {
return _userNoInfo(cubit);
}
if (snapshot.connectionState == ConnectionState.done) {
Map<String, dynamic> data =
snapshot.data!.data() as Map<String, dynamic>;
return _userInfo(data, cubit);
}
return Text('Loading');
},
);
}
if (state is ProfileErrorState) {
return Center(
child: Text(state.error.toString()),
);
}
return Container();
})),
));
}
Widget _userInfo(data, ProfileCubit cubit) {
return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Form(
key: _formKeyTwo,
child: Column(children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.2,
child: Slider(
value: _currentSliderValue,
max: 100,
divisions: 100,
label: _currentSliderValue.toString(),
onChanged: (value) {
setState(() {
_currentSliderValue = value;
});
}),
),
SizedBox(width: 100),
_cardData(),
],
),
const SizedBox(height: 50.0),
Center(
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.2,
child: ElevatedButton(
onPressed: () {
cubit.addAndUpdateInfo(
_bisNameContr.text,
_bisAddressContr.text,
_contactNameContr.text,
_contactEmailContr.text,
_phoneNumberContr.text,
_progNameContr.text,
_currentSliderValue.toString());
profile_cubit
class ProfileCubit extends Cubit<ProfileState> {
final Database _firestoreRepo;
ProfileCubit(this._firestoreRepo) : super(ProfileInitial()) {
getInfo();
}
static ProfileCubit get(context) => BlocProvider.of(context);
Future<DocumentSnapshot<Object?>> getInfo() {
try {
emit(ProfileLoadingState());
final Future<DocumentSnapshot<Object?>> infoData =
_firestoreRepo.getData();
emit(ProfileLoadedState(infoData));
return infoData;
} catch (e) {
emit(ProfileErrorState(e.toString()));
throw Exception(e);
}
}
Future<void> addAndUpdateInfo(
final String bisName,
final String bisAddress,
final String contactName,
final String contactEmail,
final String phoneNumber,
final String progName,
final String progYears,
) async {
await _firestoreRepo.addAndUpdateInfo(bisName, bisAddress, contactName,
contactEmail, phoneNumber, progName, progYears);
}
}
cubit_state
abstract class ProfileState extends Equatable {
const ProfileState();
#override
List<Object> get props => [];
}
class ProfileInitial extends ProfileState {}
class ProfileLoadingState extends ProfileState {}
class ProfileLoadedState extends ProfileState {
final Future<DocumentSnapshot<Object?>> dataInfo;
const ProfileLoadedState(this.dataInfo);
}
class ProfileErrorState extends ProfileState {
final String error;
const ProfileErrorState(this.error);
#override
List<Object> get props => [error];
}
If you don't want to build everytime, you can use buildWhen which is available inside BlocBuilder. if the value is true, it will rebuild everytime and if it's false, it will never rebuild. So you can keep a condition over there based on your requirements.
I am trying to load more data once I reach the end of the stream builder, everytime I reach the end, the stream builder reloads and bounces back to the beginning, and it does not allow me to scroll down ( keep reload and bounce back to the top).
What I tried is once I reach the end, increase the limit from the firebase, but I am not sure what the problem is.. anyone can help me with this?
Here is the sample code for what I want to do
_onEndScroll(ScrollMetrics metrics) {
//loadToTrue();
setState(() {
documentLimit = documentLimit + 10;
});
}
StreamBuilder(
initialData: cache,
stream: FirebaseFirestore.instance
.collection("timeline")
.doc(widget.currentUser.id)
.collection('timelinePosts')
.orderBy('timestamp', descending: true)
.limit(documentLimit)
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> streamSnapshot) {
items = streamSnapshot.data != null &&
streamSnapshot.data.docs != null
? streamSnapshot.data.docs
: [];
List<Post> posts =
items.map((doc) => Post.fromDocument(doc)).toList();
cache = streamSnapshot.data;
return !streamSnapshot.hasData ||
streamSnapshot.connectionState ==
ConnectionState.waiting
? Center(
child: CircularProgressIndicator(),
)
: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollEndNotification) {
_onEndScroll(scrollNotification.metrics);
}
},
child: Container(
height: MediaQuery.of(context).size.height - 200,
margin: EdgeInsets.only(bottom: 1),
child: ListView.builder(
physics: BouncingScrollPhysics(
parent: ScrollPhysics()),
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (_, i) =>
timelineDecision == "follow"
? posts[i]
: postsLocal[i])));
})
and this is code for all
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:fluttershare/pages/home.dart';
import 'package:fluttershare/models/user.dart';
import 'package:fluttershare/pages/search.dart';
import 'package:fluttershare/pages/upload.dart';
import 'package:fluttershare/pages/upload_limit.dart';
import 'package:fluttershare/widgets/post.dart';
import 'package:latlong/latlong.dart';
final usersRef = FirebaseFirestore.instance.collection('users');
class Timeline extends StatefulWidget {
final User currentUser;
Timeline({this.currentUser});
#override
_TimelineState createState() => _TimelineState();
}
class _TimelineState extends State<Timeline> {
QuerySnapshot cache;
List<Post> posts;
List<Post> postsLocal;
List<DocumentSnapshot> items;
List<String> followingList = [];
String timelineDecision = "local";
String address = "Norman";
ScrollController listScrollController;
int documentLimit = 5;
void initState() {
super.initState();
getFollowing();
}
getFollowing() async {
QuerySnapshot snapshot = await followingRef
.doc(currentUser.id)
.collection('userFollowing')
.get();
if (mounted) {
setState(() {
followingList = snapshot.docs.map((doc) => doc.id).toList();
});
}
}
setTimeline(String timelineDecision) {
setState(() {
this.timelineDecision = timelineDecision;
});
}
buildToggleTimeline() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
TextButton(
onPressed: () => setTimeline("follow"),
child: Text('Following',
style: TextStyle(
color: timelineDecision == 'follow'
? Theme.of(context).primaryColor
: Colors.grey)),
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 20),
),
),
TextButton(
onPressed: () => setTimeline("local"),
child: Text('Local',
style: TextStyle(
color: timelineDecision == 'local'
? Theme.of(context).primaryColor
: Colors.grey)),
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 20),
)),
],
);
}
_onEndScroll(ScrollMetrics metrics) {
//loadToTrue();
setState(() {
documentLimit = documentLimit + 10;
});
}
#override
Widget build(context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text('Entango'),
actions: <Widget>[
PopupMenuButton(
icon: const Icon(Icons.add),
onSelected: choiceAction,
itemBuilder: (context) => [
PopupMenuItem(
child: Text("Timelimit post"),
value: 1,
),
PopupMenuItem(
child: Text("Normal post"),
value: 2,
)
]),
IconButton(
icon: const Icon(Icons.search),
tooltip: 'Show Snackbar',
onPressed: () => Navigator.push(
context, MaterialPageRoute(builder: (context) => Search())),
)
],
),
body: ListView(
children: <Widget>[
buildToggleTimeline(),
Divider(
height: 0.0,
),
//buildSlider(),
StreamBuilder(
initialData: cache,
stream: timelineDecision == "follow"
? FirebaseFirestore.instance
.collection("timeline")
.doc(widget.currentUser.id)
.collection('timelinePosts')
.orderBy('timestamp', descending: true)
.limit(documentLimit)
.snapshots()
: FirebaseFirestore.instance
.collection("postsLocalRef")
.doc(address)
.collection("userPosts")
.orderBy('timestamp', descending: true)
.limit(documentLimit)
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> streamSnapshot) {
items = streamSnapshot.data != null &&
streamSnapshot.data.docs != null
? streamSnapshot.data.docs
: [];
List<Post> posts =
items.map((doc) => Post.fromDocument(doc)).toList();
List<Post> postsLocal =
items.map((doc) => Post.fromDocument(doc)).toList();
cache = streamSnapshot.data;
return !streamSnapshot.hasData ||
streamSnapshot.connectionState ==
ConnectionState.waiting
? Center(
child: CircularProgressIndicator(),
)
: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollEndNotification) {
_onEndScroll(scrollNotification.metrics);
}
},
child: Container(
height: MediaQuery.of(context).size.height - 200,
margin: EdgeInsets.only(bottom: 1),
child: ListView.builder(
physics: BouncingScrollPhysics(
parent: ScrollPhysics()),
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (_, i) =>
timelineDecision == "follow"
? posts[i]
: postsLocal[i])));
})
],
),
);
}
void choiceAction(int value) {
if (value == 1) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Upload_limit(
currentUser: currentUser,
)));
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Upload(
currentUser: currentUser,
)));
}
}
}
There are few issues I could find. One of them is using StreamBuilder to fetch paged data. While it might be great in theory but it won't work in case of Firebase as Firebase is providing Stream. So every-time, setState is called, a new steam will be created. I have wrote sample app for fetching data from firestore in paginated way.
//main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
const MyHomePage({Key? key, required this.title}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const PAGE_SIZE = 30;
bool _allFetched = false;
bool _isLoading = false;
List<ColorDetails> _data = [];
DocumentSnapshot? _lastDocument;
#override
void initState() {
super.initState();
_fetchFirebaseData();
}
Future<void> _fetchFirebaseData() async {
if (_isLoading) {
return;
}
setState(() {
_isLoading = true;
});
Query _query = FirebaseFirestore.instance
.collection("sample_data")
.orderBy('color_label');
if (_lastDocument != null) {
_query = _query.startAfterDocument(_lastDocument!).limit(PAGE_SIZE);
} else {
_query = _query.limit(PAGE_SIZE);
}
final List<ColorDetails> pagedData = await _query.get().then((value) {
if (value.docs.isNotEmpty) {
_lastDocument = value.docs.last;
} else {
_lastDocument = null;
}
return value.docs
.map((e) => ColorDetails.fromMap(e.data() as Map<String, dynamic>))
.toList();
});
setState(() {
_data.addAll(pagedData);
if (pagedData.length < PAGE_SIZE) {
_allFetched = true;
}
_isLoading = false;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: NotificationListener<ScrollEndNotification>(
child: ListView.builder(
itemBuilder: (context, index) {
if (index == _data.length) {
return Container(
key: ValueKey('Loader'),
width: double.infinity,
height: 60,
child: Center(
child: CircularProgressIndicator(),
),
);
}
final item = _data[index];
return ListTile(
key: ValueKey(
item,
),
tileColor: Color(item.code | 0xFF000000),
title: Text(
item.label,
style: TextStyle(color: Colors.white),
),
);
},
itemCount: _data.length + (_allFetched ? 0 : 1),
),
onNotification: (scrollEnd) {
if (scrollEnd.metrics.atEdge && scrollEnd.metrics.pixels > 0) {
_fetchFirebaseData();
}
return true;
},
),
);
}
}
class ColorDetails {
final String label;
final int code;
ColorDetails(this.code, this.label);
factory ColorDetails.fromMap(Map<String, dynamic> json) {
return ColorDetails(json['color_code'], json['color_label']);
}
Map toJson() {
return {
'color_code': code,
'color_label': label,
};
}
}
In case you are interested, you can checkout the article I have written as well at https://blog.litedevs.com/infinite-scroll-list-using-flutter-firebase-firestore
I want to run a function to check Firestore data everytime a screen is loaded.
Here is my code :
class PlaceTile extends StatefulWidget {
//String flag='inactive';
final Place place;
PlaceTile({ this.place });
#override
_PlaceTileState createState() => _PlaceTileState(place);
}
class _PlaceTileState extends State<PlaceTile> {
final Place place;
_PlaceTileState(this.place);
String flag = 'inactive';
void getUserById(String id) {
DatabaseService().placesCollection.document(id).get().then((DocumentSnapshot doc) {
print(doc.data);
});
}
checkUserStatus() async {
final FirebaseAuth auth = FirebaseAuth.instance;
final FirebaseUser user = await auth.currentUser();
String uid = user.uid;
UserDatabaseService().userCollection.getDocuments().then((QuerySnapshot snapshot) {
snapshot.documents.forEach((DocumentSnapshot doc) {
if(doc.documentID == uid)
{
if(doc.data['status']=='true')
{
setState(() {
flag = 'active';
});
}
else
{
setState(() {
flag = 'inactive';
});
}
}
});
});
return flag;
}
void showQueueDetailsPanel() {
showModalBottomSheet(context: context, builder: (context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 20.0, horizontal: 60.0),
child: QueueDetails(value: place.name),
);
});
}
String value;
#override
initState() {
super.initState();
checkUserStatus();
}
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(top: 8.0),
child: Card(
margin: EdgeInsets.fromLTRB(20.0, 6.0, 20.0, 0.0),
child: ListTile(
isThreeLine: true,
leading: CircleAvatar(
radius: 25.0,
backgroundColor: Colors.white,
),
title: Text(place.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
//Text("People "+place.totalPeople.toString()),
Text(""),
Text("Token "+place.tokenAvailable.toString()),
],
),
trailing: FlatButton.icon(
icon: Icon(Icons.add_box_rounded),
label: Text('Join'),
onPressed: checkUserStatus() =='inactive' ? showQueueDetailsPanel : null,
),
),
)
);
}
}
The code in its current state doesn't perform the way I expect. It s a fallacy actually. Once flag is set to active, the Join buttons get disabled and for them to get re-enabled they need to be pressed for condition to be checked. But that's not possible since the buttons can't be pressed as they are disabled.
I want to run checkUserStatus() everytime this page with PlaceTile is loaded. How can I achieve that? A code snippet would be helpful.
Add your function to initState:
#override
void initState() {
super.initState();
checkUserStatus();
}
Widget build(BuildContext context) {
return FutureBuilder(
future: futurePackageListModel,
builder: (BuildContext context,
AsyncSnapshot<List<Model>> snapshot) {
if (snapshot.hasData) {
return YourWidget();
} else if (snapshot.hasError) {
return ApiStatusFail();
} else {
return Center(child: CircularProgressIndicator());
}
},
);
}
I'm trying to arrange some data streamed from Firebase with an ExpansionPanellist. The panelList is placed inside a StreamBuilder, and above the StreamBuilder i have a SingleChildScrollView.
I am able to get the list showing with the headers, but i can't get the expand/collapse function to work, so I am not able to see the body-text.
screenshot of the list
The expanding/collapinsg function worked outside the Streambuilder, but I was not able to access the data from Firebase then.
Any help will be much appreciated! If this is the wrong way of doing this, I will also be grateful for any pointers to alternative ways of achieving this. (There won't be any data added to the server while looking at past climbs and graphs, so a streambuilder might not be necessary if there are easier/better ways).
-Kristian
class Graphs extends StatefulWidget {
static String id = 'graphs_screen';
#override
_GraphsState createState() => _GraphsState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(tabs: [
Tab(text: 'Graphs'),
Tab(text: 'Stats'),
Tab(text: 'Climbs'),
]),
),
body: TabBarView(
children: [
//Image.asset('assets/images/line_graph.png'),
Expanded(child: NumericComboLinePointChart.withSampleData()),
Container(
child: Text(''),
),
SingleChildScrollView(
child: DataStream(),
),
],
)),
),
);
}
}
class DataStream extends StatefulWidget {
#override
_DataStreamState createState() => _DataStreamState();
}
class _DataStreamState extends State<DataStream> {
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _firestore
.collection('climbs')
.orderBy('Date', descending: true)
.snapshots(),
builder: (context, snapshot) {
List<ExpansionItem> expansionList = <ExpansionItem>[];
if (snapshot.hasData) {
final alldata = snapshot.data.docs;
for (var data in alldata) {
final dataFunction = data.data();
final grades = dataFunction['gradeScore'];
final climbDate = dataFunction['Date'];
final climbDateT = DateTime.fromMicrosecondsSinceEpoch(
climbDate.microsecondsSinceEpoch);
String climbDateString =
"${climbDateT.year.toString()}-${climbDateT.month.toString().padLeft(2, '0')}-${climbDateT.day.toString().padLeft(2, '0')} ${climbDateT.hour.toString()}-${climbDateT.minute.toString()}";
final climber = dataFunction['sender'];
final currentUSer = loggedInUser.email;
if (climber == loggedInUser.email) {
expansionList.add(ExpansionItem(
dateTimeHeader: climbDateString,
climbs: grades.toString()));
}
}
}
return ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
print('tap registered');
expansionList[index].isExpanded = !isExpanded;
});
},
children: expansionList.map((ExpansionItem item) {
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return Container(
child: Text(item.dateTimeHeader),
);
},
body: Container(
child: Text(item.climbs),
),
isExpanded: item.isExpanded,
);
}).toList(),
);
});
}
}
class ExpansionItem {
ExpansionItem({this.isExpanded: false, this.dateTimeHeader, this.climbs});
bool isExpanded;
final String dateTimeHeader;
final String climbs;
}
I also ran into this issue and managed to develop a "work-around" (sorry in advance, it's a bit messy).
The reason your expansion tiles are not expanding is due to the nature of expansionCallback function. Once you press the expand button it also causes your StreamBuilder to rebuild. Therefore, since you're initializing "expansionList" within the StreamBuilder it will reset "isExpanded" back to false no matter how many times you press it. So your best option is to initialize the expansionList outside of the StreamBuilder and modify it from within. Check below for my solution but I welcome anyone to optimize it and/or share a better one.
class ExpansionItem {
String headerValue;
bool isExpanded;
SplitObject item;
ExpansionItem({this.item, this.headerValue, this.isExpanded = false});
}
class MyExample extends StatefulWidget{
#override
_MyExampleState createState() => _MyExampleState();
}
class _MyExampleState extends State<MyExample> {
//Pick a number as large as you see fit to always be more than necessary.
List<ExpansionItem> expansionItems = List<ExpansionItem>.generate('anyNumber', (int index)=> ExpansionItem(isExpanded: false,));
#override
Widget build(BuildContext context) {
return GestureDetector(
child: Scaffold(
appBar: AppBar(
title: Text('Split',style: Theme.of(context).textTheme.headline3,),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Container(
child: StreamBuilder(
builder: (context, streamData){
if(streamData.hasData){
List<SplitObject> items = streamData.data;
//Save data to Expansion list by iterating through it.
for (var i = 0; i < items.length; i++){
try {
expansionItems[i].item =items[i];
expansionItems[i].headerValue =items[i].itemName;
} catch (e) {
// Catch any range errors after trimming list.
if(e.toString().contains('RangeError')) {
expansionItems.add(ExpansionItem(
item: items[i], headerValue: items[i].itemName));
}
}
}
// Trim list
expansionItems = expansionItems.getRange(0, items.length).toList();
return _buildListPanel(expansionItems);
} else {
return ListTile(
title: Text('No items to split.'),
);
}
},
stream: DatabaseService().splitItemData,
),
),
),
)
);
}
Widget _buildListPanel(List<ExpansionItem> expansionItems){
// print(expansionItems[0].isExpanded);
return ExpansionPanelList(
expansionCallback: (int index, bool isExpanded){
setState(() {
expansionItems[index].isExpanded = !isExpanded;
// print(expansionItems[index].isExpanded);
});
},
children: expansionItems.map<ExpansionPanel>((ExpansionItem item){
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded){
print(item.isExpanded);
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(),
);
},
body: Container(),
isExpanded: item.isExpanded,
);
}).toList(),
);
}
}
You should create a class where your Expansion List will be, then your Stream builder must call it. Doing it this way Expansion Panel List callback will function just normal.
Look:
class _ExpansionPanelClass extends State<ExpansionPanelClass> {
#override
Widget build(BuildContext context) {
return ExpansionPanelList(
elevation: 3,
expansionCallback: (index, isExpanded) {
setState(() {
widget.product[index]['isExpanded'] = !isExpanded;
});
},
animationDuration: const Duration(milliseconds: 600),
children: widget.product
.map(
(item) => ExpansionPanel(
canTapOnHeader: true,
backgroundColor:
item['isExpanded'] == true ? Colors.cyan[100] : Colors.white,
headerBuilder: (_, isExpanded) => Container(
padding:
const EdgeInsets.symmetric(vertical: 15, horizontal: 30),
child: Text(
item['title'],
style: const TextStyle(fontSize: 20),
)),
body: Container(
padding:
const EdgeInsets.symmetric(vertical: 15, horizontal: 30),
child: Text(item['description']),
),
isExpanded: item['isExpanded'],
),
)
.toList(),
);
}
}
Then from your StreamBuilder:
StreamBuilder<QuerySnapshot>(
stream: dbProducts
.collection(ids[i])
.orderBy('order', descending: false)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<Map<String, dynamic>> _items = [];
for (var document in snapshot.data!.docs) {
Map data = Map.from(document.data() as Map);
if (data['hide'] == false) {
Map<String, dynamic> map = {
'id': _items.length,
'title': data['name'],
'description': data['ingredients'],
'isExpanded': false
};
_items.add(map);
}
}
return ExpansionPanelClass(product: _items);
} else {
return const Center(
child: CircularProgressIndicator(
color: Colors.brown,
),
);
}
},
),
That's all.
I am having some troubles using firestore data in two diferent classes.
I have the class HomePage and the FirestoreSliceShow.
In the class FirestoreSliceShow I query the firestore, and I am able to use the data in _buildStoryPage method. However, I need to use the data of the same photo slide in the class HomePage <---- PHOTO TITLE HERE ---->.
How can I share the data from slideList[currentIdx - 1] with the class HomePage?
HomePage class:
class HomePage extends StatefulWidget
{
HomePage
({
this.auth,
this.onSignedOut,
});
#override
State<StatefulWidget> createState() {
return _HomePageState();
}
}
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
{
List<Posts> postsList = [];
#override
void initState(){
// defining the animation, removed to reduce the number of lines
super.initState();
}
onBottomPartTap(){
}
// Deteils box --- some information from firebase db go to here
Widget getWidget(){
return Stack(
fit: StackFit.expand,
children: <Widget>[
FractionallySizedBox(
alignment: Alignment.topCenter,
heightFactor: _heightFactorAnimation.value,
child: FirestoreSlideShow(),
),
GestureDetector(
onTap: onBottomPartTap,
onVerticalDragUpdate: _handleVerticalUpdate,
onVerticalDragEnd: _handleVerticalEnd,
child: FractionallySizedBox(
alignment: Alignment.bottomCenter,
heightFactor: 1.05 - _heightFactorAnimation.value,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
new BoxShadow(
color: Colors.black12,
)
],
),
child: Row(
children: <Widget>[
Expanded(
child: Text(
"<---- TAGS HERE ---->", textAlign: TextAlign.center),
),
Expanded(
child: Text(
"<---- PHOTO TITLE HERE ---->", textAlign: TextAlign.center),
),
],
),
),
),
),
],
);
}
#override
Widget build(BuildContext context) {
screenHeight = MediaQuery.of(context).size.height;
return MaterialApp
(
home: Scaffold
(
backgroundColor: Color(0xFFEEEEEE),
bottomNavigationBar: AppBottomBar(),
body: AnimatedBuilder(
animation: _controller,
builder: (context, widget){
return getWidget();
},
),
),
);
}
}
and the class FirestoreSlideShow:
class FirestoreSlideShow extends StatefulWidget{
createState() => FirestoreSlideshowState();
}
class FirestoreSlideshowState extends State<FirestoreSlideShow> {
//width of the image
final PageController ctrl = PageController(viewportFraction: 0.95);
// the fraction is to the previous and current image overflow
final Firestore db = Firestore.instance;
// data from the db
Stream slides;
String activeTag = 'favorites';
//keep track of the current page to avoid unnecessary renders
int currentPage = 0;
#override
void initState() {
_queryDb();
//set state when page changes
ctrl.addListener(() {
int next = ctrl.page.round();
if (currentPage != next) {
setState(() {
currentPage = next;
});
}
});
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: slides,
initialData: [],
builder: (context, AsyncSnapshot snap) {
List slideList = snap.data.toList();
return PageView.builder(
controller: ctrl,
itemCount: slideList.length + 1,
itemBuilder: (context, int currentIdx) {
if (currentIdx == 0) {
return _buildTagPage(); // First slide. to be redesigned
} else if (slideList.length >= currentIdx) {
// Active page
bool active = currentIdx == currentPage;
//ProfileDetails(index: currentPage,);
return _buildStoryPage(slideList[currentIdx - 1], active, currentPage);
}
}
);
}
);
}
// Query the DB
Stream _queryDb({String tag = 'favorites' }) {
// make a query
Query query = db.collection('stories').where('tags', arrayContains: tag);
// Map the documents to the date payload
slides =
query.snapshots().map((list) => list.documents.map((doc) => doc.data));
// update the active tag
setState(() {
activeTag = tag;
});
}
static _buildStoryPage(Map data, bool active, int currentPage) {
// Animated properties
final double blur = active ? 0 : 0;
final double offset = active ? 0 : 0;
//height of the image
final double top = active ? 5 : 5;
return AnimatedContainer(
duration: Duration(milliseconds: 500),
curve: Curves.easeOutQuint,
margin: EdgeInsets.only(top: top, bottom: 0, right: 3),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
image: DecorationImage(
fit: BoxFit.cover,
image: NetworkImage(data['img']),
),
boxShadow: [BoxShadow(color: Colors.black87,
blurRadius: blur,
offset: Offset(offset, offset)),
]
),
child: Column(
children: <Widget>[
Container(
child: Text(data['title'],
style: TextStyle(fontSize: 40, color: Colors.white))
),
Container(
child: Text(currentPage.toString()),
),
],
),
);
}
}
Can you try this;
class FirestoreSlideShow extends StatefulWidget{
createState() => FirestoreSlideshowState();
}
class FirestoreSlideshowState extends State<FirestoreSlideShow> {
//width of the image
final PageController ctrl = PageController(viewportFraction: 0.95);
// the fraction is to the previous and current image overflow
final Firestore db = Firestore.instance;
// data from the db
Stream slides;
String activeTag = 'favorites';
//keep track of the current page to avoid unnecessary renders
int currentPage = 0;
#override
void initState() {
_queryDb();
//set state when page changes
ctrl.addListener(() {
int next = ctrl.page.round();
if (currentPage != next) {
setState(() {
currentPage = next;
});
}
});
}
#override
Widget build(BuildContext context) {
Widget _buildStoryPage(Map data, bool active, int currentPage) {
// Animated properties
final double blur = active ? 0 : 0;
final double offset = active ? 0 : 0;
//height of the image
final double top = active ? 5 : 5;
return AnimatedContainer(
duration: Duration(milliseconds: 500),
curve: Curves.easeOutQuint,
margin: EdgeInsets.only(top: top, bottom: 0, right: 3),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
image: DecorationImage(
fit: BoxFit.cover,
image: NetworkImage(data['img']),
),
boxShadow: [BoxShadow(color: Colors.black87,
blurRadius: blur,
offset: Offset(offset, offset)),
]
),
child: Column(
children: <Widget>[
Container(
child: Text(data['title'],
style: TextStyle(fontSize: 40, color: Colors.white))
),
Container(
child: Text(currentPage.toString()),
),
],
),
);
}
return StreamBuilder(
stream: slides,
initialData: [],
builder: (context, AsyncSnapshot snap) {
List slideList = snap.data.toList();
return PageView.builder(
controller: ctrl,
itemCount: slideList.length + 1,
itemBuilder: (context, int currentIdx) {
if (currentIdx == 0) {
return _buildTagPage(); // First slide. to be redesigned
} else if (slideList.length >= currentIdx) {
// Active page
bool active = currentIdx == currentPage;
//ProfileDetails(index: currentPage,);
return _buildStoryPage(slideList[currentIdx - 1], active, currentPage);
}
}
);
}
);
}
// Query the DB
Stream _queryDb({String tag = 'favorites' }) {
// make a query
Query query = db.collection('stories').where('tags', arrayContains: tag);
// Map the documents to the date payload
slides =
query.snapshots().map((list) => list.documents.map((doc) => doc.data));
// update the active tag
setState(() {
activeTag = tag;
});
}
}