Why Won't This StreamBuilder Display Data without Restarting the App? - flutter

I'm trying to get a Flutter project to display a simple list of items returned from the Firebase Realtime Database. The code mostly works, but I have to restart the app each time I log out and log back in as a different user, which isn't what I'm looking for. I need the user's data to appear when they log in. I don't quite understand what all is happening here (I stumbled across a functional solution after several days of trial and error and googling), but I thought a Stream was more or less a 'live' stream of data from a particular source.
EDIT: kPAYEES_NODE is a constant stored elsewhere that resolves to 'users/uid/payees' in the RTDB:
import 'package:firebase_database/firebase_database.dart';
import 'auth_service.dart';
final DatabaseReference kUSER_NODE =
FirebaseDatabase.instance.ref('users/${AuthService.getUid()}');
final DatabaseReference kPAYEES_NODE = kUSER_NODE.child('payees');
Here's the code in question:
class DashboardPage extends StatelessWidget {
const DashboardPage({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(...),
body: StreamBuilder(
stream: kPAYEES_NODE.onValue,
builder: (context, snapshot) {
final payees = <Payee>[];
if (!snapshot.hasData) {
return Center(child: Column(children: const [Text('No Data')]));
} else {
final payeeData =
(snapshot.data!).snapshot.value as Map<Object?, dynamic>;
payeeData.forEach((key, value) {
final dataLast = Map<String, dynamic>.from(value);
final payee = Payee(
id: dataLast['id'],
name: dataLast['name'],
note: dataLast['note'],
);
payees.add(payee);
});
return ListView.builder(
shrinkWrap: true,
itemCount: payees.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
payees[index].name,
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
payees[index].id,
style: const TextStyle(color: Colors.white),
),
);
});
}
},
),
floatingActionButton: (...),
);
}
}

That's because you only get the UID of the current once in your code, here:
FirebaseDatabase.instance.ref('users/${AuthService.getUid()}')
And by the time this line runs, the AuthService.getUid() has a single value. Instead, the code needs to be reevaluated any time a users signs in or out of the app.
In an app where the user can sign in and out, the UID values are a stream like the one exposed by FirebaseAuth.instance.authStateChanges() as shown in the Firebase documentation on getting the current user. You can wrap the stream that is returned by authStateChanges() in a StreamBuilder, and then create the database reference inside of that builder, to have it respond to changes in the authentication state.

Related

Accessing all docs in subbcollecions

I'm having struggles for the last couple of days, so any help highly appreciates it.
I have an app where everyday users take photos of themself ( I set the date of that day as docId), then in UI, every day has a page ( a carousel) where users can swipe and see the photos belonging to every day.
I attached a screenshot of the Firstore database.
But having a problem reading images , tried every method.
P.s : When I set the DocId for instance: 2023-01-11 it works but it just show the photos of one day , I need to fetch all images from all days.
Method adding data to Firestore:
final photoToDb = db
.collection('photos')
.doc(DateFormat('yyyy-MM-dd').format(newDate))
.collection('Today Photos')
.withConverter(
fromFirestore: PhotoModel.fromFirestore,
toFirestore: ((PhotoModel photoModel, options) =>
photoModel.toFirestore()),
);
photoToDb.add(photo);
} catch (e) {
return ('errro');
}
}
Page where I'm trying to display images ,
lass SugarPhotoPage extends StatefulWidget {
const SugarPhotoPage({
super.key,
});
#override
State<SugarPhotoPage> createState() => _SugarPhotoPageState();
}
class _SugarPhotoPageState extends State<SugarPhotoPage> {
final Stream<QuerySnapshot> _photoStream = FirebaseFirestore.instance
.collection('photos')
.doc()
.collection('Today Photos')
.snapshots();
#override
void initState() {
print('${AppData.userSelectedData}');
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
),
body: StreamBuilder<QuerySnapshot>(
stream: _photoStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("Loading");
}
if (snapshot.hasData) {
return SafeArea(
child: Center(
child: ListView(
children: snapshot.data!.docs
.map((DocumentSnapshot documentSnapshot) {
Map<String, dynamic> data =
documentSnapshot.data()! as Map<String, dynamic>;
return Container(
height: 200,
width: 100,
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage('${data['ImgUrl']}'),
fit: BoxFit.contain,
),
),
);
}).toList(),
),
),
);
}
return const Text('Loading');
}),
);
}
}
You can't do that with this data structure. Firebase queries are shallow, meaning that you can't query a document together with the documents in sub collections.
In StreamBuilder you can get snapshots of either one specific document by setting :
FirebaseFirestore.instance
.collection(...)
.withConverter<...>(...)
.doc(...)
.snapshots()
or multiple documents:
FirebaseFirestore.instance
.collection(...)
.withConverter<...>(...)
.where(...)
.orderBy(...)
.limit(...)
.snapshots()
In both cases, you will get the data of one or more documents, but if you need the documents in a sub collection, you need to perform another query. For example if you have one document in doc variable, and you need data in its Today Photos sub collection, you need another stream:
doc.collection('Today Photos')
.withConverter<...>(...)
.snapshots()
So with the current data structure you can query into a StreamBuilder all documents in the user's photos collection, but the contents of Today Photos sub collection must be queried separately for each retrieved document of photos collection.
The other option is to change your data structure. You can add the daily photos to the photos collection, let Firebase assign an id to them and add the date as a field. This way you can have one stream for the photos, order them by date, add a limit etc.

How Should I Deal with Null Values in a StreamBuilder?

I'm trying to build a flutter view that loads a list of items ('cost codes' in the code snippet) from a database call. This code works elsewhere in my project where I already have data in the database, but it fails when it tries to read data from an empty node. I can provide dummy data or sample data for my users on first run, but they might delete the data before adding their own, which would cause the app to crash the next time this view loads.
What's the proper way to deal with a potentially empty list in a StreamBuilder?
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: dbPathRef.onValue,
builder: (context, snapshot) {
final costCodes = <CostCode>[];
if (!snapshot.hasData) {
return Center(
child: Column(
children: const [
Text(
'No Data',
style: TextStyle(
color: Colors.white,
),
)
],
),
);
} else {
final costCodeData =
// code fails on the following line with the error
// 'type "Null" is not a subtype of type "Map<Object?, dynamic>" in type cast'
(snapshot.data!).snapshot.value as Map<Object?, dynamic>;
costCodeData.forEach(
(key, value) {
final dataLast = Map<String, dynamic>.from(value);
final account = CostCode(
id: dataLast['id'],
name: dataLast['name'],
);
costCodes.add(account);
},
);
return ListView.builder(
shrinkWrap: false,
itemCount: costCodes.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
costCodes[index].name,
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
costCodes[index].id,
style: const TextStyle(color: Colors.white),
),
);
},
);
}
},
);
}
Personally I tend to avoid handling raw data from a database in the UI code and handle all of this in a repository/bloc layer.
However, to solve your issue you can simply add a ? to the end of the cast like so:
final costCodeData = (snapshot.data!).snapshot.value as Map<Object?, dynamic>?;
You will no longer get the cast exception - however you still have to test costCodeData for null.
This block of code may help:
final data = snapshot.data;
final Map<Object?, dynamic>? costCodeData
if (data == null) {
costCodeData = null;
} else {
costCodeData = (snapshot.data!).snapshot.value as Map<Object?, dynamic>?;
}
if (costCodeData == null){
// Show noData
} else {
// Show data
}
final dataLast = Map<String, dynamic>.from(value);
final account = CostCode(
id: dataLast['id'],
name: dataLast['name'],
);
costCodes.add(account);
},
you declaired dataLast with a Map having key as String, but inside the account variable the id and name are not in the string format, keep those inside "" || '' even after modiying these, if you still face other issue try putting question mark at the end of the line
(snapshot.data!).snapshot.value as Map<Object, dynamic>?

Flutter Fire Store - Assigning individual document fields to variables

I'm attempting to show a user's profile image on their home page by pulling the user's 'imageUrl' from their Fire Store document. I already have the app setup to where the user can upload a new image which updates the 'imageUrl' in Fire Store, but I don't know how to have the 'imageUrl' as a variable so I can show it on the app screen.
I've been reading documentation online but It seems over simplified or out of date. I've tried using StreamBuilder, but it pulls the data from every user in the database instead of for a single user. I just need to know how to pull this one value and use it as a variable in my dart code using "getString()" with a document reference or the collection reference I already have, thank you.
class _UserPageState extends State<UserPage> {
User user = auth.currentUser!;
final CollectionReference collectionReference = FirebaseFirestore.instance.collection('users');
// Get profileImageUrl from users userDoc
String imageUrl = 'test'; // this should be the users imageUrl
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'${user.email}'), // this is being pulled from authentication not firestore
),
body: Center(
child: Column(
children: [
// --------------------------- I tried using a stream builder here ---------------------
StreamBuilder(
stream: collectionReference.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return const Text(
'Something went wrong.'); // A: use incase the data does not load
}
final data = snapshot.requireData;
return ListView.builder(
shrinkWrap: true,
itemCount: data.size,
itemBuilder: (context, index) {
return Text(
// A: Stream builder will update with all of the users email addresses, I want this for one user exclusively
'My email is ${data.docs[index]['email']}');
},
collection('users')
.where("uid", isEqualTo: uid)
.snapshots(),
To filter the data in firestore collection use "where". Store the user uid in offline and query it by where using the stored uid
You can use the following function to get single data from stream.
Stream<UserModel> getSingleStreamData({String? uId}) {
return ref!.where(CommonKeys.id, isEqualTo: uId).snapshots().map((value) => value.docs.first.data());}

Flutter_bloc: Is there a cleaner way to access the state of a flutter Cubit than a redundant if/else statement?

I'm using flutter_bloc and have created a sessionState that gets triggered when a user has successfully authenticated and the state saves the user object (did).
I use a Cubit to change the state which then results in showing different screens in the ui. But whenever I want to access the did object of the Verified sessionState which holds the user information, I have to create an if else statement in every file to check if the sessionState is Verified and if that is true I can access the did object with state.did.
I would be interested to know if it's possible to provide this did object to all underlying widgets without passing it down manually every widget.
I would like to be able to just access the did object from the context so that I can access it everywhere below where it is provided.
My SessionState:
abstract class SessionState {}
class UnkownSessionState extends SessionState {}
class Unverified extends SessionState {}
class Verified extends SessionState {
Verified({required this.did});
final Did did;
}
The SessionCubit launches states that I use to define the global state of the app and show different screens as a result.
class SessionCubit extends Cubit<SessionState> {
SessionCubit(this.commonBackendRepo) : super(UnkownSessionState()) {
attemptGettingDid();
}
final CommonBackendRepo commonBackendRepo;
final SecureStorage secureStorage = SecureStorage();
Future<void> attemptGettingDid() async {
try {
//more logic
emit(Unverified());
} catch (e) {
emit(Unverified());
}
}
void showUnverified() => emit(Unverified());
void showSession(Did did) {
emit(Verified(did: did));
}
}
This is how I currently access the did object in every file:
BlocBuilder<SessionCubit, SessionState>(builder: (context, state) {
if (state is Verified) {
return CustomScrollView(
slivers: <Widget>[
SliverAppBar(
elevation: 0.0,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
title: Text(
state.did.username,
style: Theme.of(context).textTheme.headline5,
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: CredentialDetailsView(),
))
],
);
} else {
return const Text("Unverified");
}
});
Edit:
That's how I would imagine the optimal solution to my scenario:
Create if/else to check if state is verfied in one parent widget.
If state is verfied create a Provider as a child with the 'did' Object so that the children of this Provider don't have to access the SessionState but can just access the provided did object.
Response to Robert Sandberg's answer
I also have an AppNavigator that either start the authNavigator or sessionNavigator. So the sessionNavigator is the parent widget of all widgets that get shown only when the state is Verified. So I think this would be a great place to wrap the sessionNavigator with the Provider.value, as you explained it.
I wrapped my SessionNavigator with the Provider.value widget and provided the state object:
class AppNavigator extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<SessionCubit, SessionState>(
builder: (context, state) {
return Navigator(
pages: [
if (state is UnkownSessionState)
MaterialPage(child: StartupScreen()),
//show auth flow
if (state is Unverified)
MaterialPage(
child: BlocProvider(
create: (context) => AuthCubit(context.read<SessionCubit()),
child: AuthNavigator(),
)),
//show session flow
if (state is Verified)
MaterialPage(
child: Provider.value(
// here I can access the state.did object
value: state,
child: SessionNavigator(),
))
],
onPopPage: (route, result) => route.didPop(result),
);
},
);
}
}
For testing I tried to watch the provided value inside of my Home screen which is a child of the SessionNavigator:
...
class _HomeState extends State<Home> {
#override
Widget build(BuildContext context) {
//
final sessionState = context.watch<SessionState>();
return CustomScrollView(
slivers: <Widget>[
SliverAppBar(
floating: true,
expandedHeight: 60.0,
backgroundColor: Colors.white,
flexibleSpace: FlexibleSpaceBar(
title: Text(
// I can't access state.did because value is of type SessionState
sessionState.did,
style: Theme.of(context).textTheme.headline6,
),
titlePadding:
const EdgeInsetsDirectional.only(start: 20, bottom: 16)),
)
],
);
}
}
But because the type of my value is SessionState I can't access the did object of the underlying Verified state. I though about providing the state.did object but I don't know how I would watch for that type.
I also got the error that my home screen couldn't find the Provider in the build context. Could that happen because my Home screen has a Navigator as parent?
This is a visualization of my app architecture:
I use this pattern sometimes, and it sounds like my solution very much fits your imagined optimal solution :)
In a parent:
BlocBuilder<MyBloc, MyBlocState>(
builder: (context, state) {
return state.when(
stateA: () => SomeWidgetA(),
stateB: () => SomeWidgetB(),
stateC: (myValue) {
return Provider.value(
value: myValue,
child: SomeWidgetC(),
);
}),
}
)
... and then in SomeWidgetC() and all it's children I do the following to get hold of myValue (where it is of type MyValueType)
final myValue = context.watch<MyValueType>();
If you want, you could also use regular InheritedWidget instead of using the Provider package but you get a lot of nifty features with Provider.
EDIT
Response to your further questions.
In your case, if you provide the entire state as you do in AppNavigator, then you should look for Verified state in SessionNavigator as such:
final sessionState = context.watch<Verified>();
Or, if you only need the Did object, then just provide that, so in AppNavigator:
if (state is Verified)
MaterialPage(
child: Provider.value(
value: state.did,
child: SessionNavigator(),
))
And then in SessionNavigator:
final did = context.watch<Did>();
Edit 3:
It might be that your problems arises with nested Navigators. Have a look at this SO answer for some guidance.
If you continue struggle, I suggest that you open new SO questions for the specific problems, as this is starting to get a bit wide :) I think that the original question is answered, even though you haven't reached a final solution.
You can use something like (state as Verified).did.username, but I would still recommend using the if/else pattern since you ensure that you are using the correct state.
In my personal projects, I use the Freezed package (https://pub.dev/packages/freezed) for such cases since there are some useful methods to cover this. For instance, by using freezed, your issue could be resolved as:
return state.maybeWhen(
verified: (did) => CustomScrollView(...),
orElse: () => const Text("Unverified"),
)

Flutter: How to delete item from listview?

DBHelper dbHelper = DBHelper();
List<Map<String, dynamic>> lists;
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Map<String, dynamic>>>(
future: dbHelper.selectMemo(userkey, 1),
builder: (context, snapshot){
if(snapshot.hasData){
if(snapshot.data.length != 0){
lists = List<Map<String, dynamic>>.from(snapshot.data);
return ListView.separated(
separatorBuilder: (context, index){
return Divider(
thickness: 0,
);
},
itemCount: lists.length,
itemBuilder: (context, index){
return ListTile(
title: Text(lists[index]["memo"]),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: (){
setState(() {
lists = List.from(lists)..removeAt(index);
});
},
),
);
},
);
}
}
},
);
}
This is my code. My lists come from sqlflite. And I want to delete my item from Listview. But this code doesn't work. I don't know where I made the mistake.
This behavior is normal. If you print some logs in the build statement, you will find that every time you click the delete button (setState), Widget will build again.
In addition, lists are re-assigned to DB data after each build
lists = List<Map<String, dynamic>>.from(snapshot.data);
So, it looks like the delete operation is not working.
This phenomenon if you've seen Flutter setState part of the source code will be well understood.
In setState, the callback is performed first, and then mark dirty
void setState(VoidCallback fn) {
final dynamic result = fn() as dynamic;
_element.markNeedsBuild();
}
So, there are two ways to solve this problem:
(1) Do not directly change the value of lists, but when the delete button is pressed, to delete the data in the database, so that when Widget build again, the data taken out of the database is correct.
(2) Add a flag to judge whether the data is initialized, and then add a judgment before assigning lists. If the data is initialized, assignment operation will not be carried out
I hope it worked for you. ^-^