flutter X firebase: how to correctly initialize fields with data in different branches in firebase realtime database - flutter

I have a piece of code trying to initialize the fields with data from two different users in the firebase realtime database, I tried various ways but all of them don't work and the field is not initialized error keeps popping up.
Here is the code:
class _PartnerProfilePageState extends State<PartnerProfilePage> {
final userUID = FirebaseAuth.instance.currentUser!.uid;
final database = FirebaseDatabase.instance.ref();
late final partnerUID;
late final profilePath;
late final partnerName;
late final birthday;
late final en;
#override
initState() {
super.initState();
initInfo();
}
initInfo() async {
database.child(userUID).onValue.listen((event) {
final data = Map<dynamic, dynamic>.from(event.snapshot.value as Map);
setState(() {
partnerUID = data['partner'];
en = data['language'].toString().startsWith('en');
initPartnerInfo();
});
});
}
Future initPartnerInfo() async {
final ppsnapshot =
await database.child(partnerUID).child('profilePath').get();
profilePath = ppsnapshot.value.toString();
final nsnapshot = await database.child(partnerUID).child('username').get();
partnerName = nsnapshot.value.toString();
final bsnapshot = await database.child(partnerUID).child('birtday').get();
birthday = bsnapshot.value.toString();
}
//rest of other unrelated stuff like build down there
}
(My firebase realtime database has no 'user' branch but directly save every user in the root with their userid).

I think there is a problem with the async initializing. The build method can try to build before you initialize the last variable(Because initState method can not async). You can easily check my theory.Delete the 'method call'(the initInfo) inside the initState(). Just create a button in the screen, give it a anonymous async function, inside the function call your init method and try to call your variables like this:
() async{
await initInfo();
print('partnerUID');
}
Easy way to checking out initialization process. Hope it helps.

Related

How to provide Future to inside Providers using Riverpod?

I'm trying to learn Riverpod with clean architecture.
I have following set/chain of providers:
final databaseFutureProvider = FutureProvider<Database>((ref) async {
final db = await DatabaseHelper().createDatabase(); // this is async because 'openDatabase' of sqflite is async
return db;
});
final toDoDatasourceProvider = Provider<ToDoDatasource>((ref) {
final db = ref.watch(databaseFutureProvider); // problem is here!!
return ToDoDatasourceImpl(db: db);
});
final toDoRepositoryProvider = Provider<ToDoRepository>((ref) {
final ds = ref.watch(toDoDatasourceProvider);
return ToDoRepositoryImpl(ds);
});
I am probably missing some small things or doing it completely wrong. How to properly provide DB (that is async in its nature)?
You don't need multiple providers, in your case since you would need ToDoRepository always you can just Initialized before running the app and use it later without worrying about the database connection state
Future<void> main(List<String> args) async {
// Initialization the db
final db = await DatabaseHelper().createDatabase();
ProviderScope(
overrides: [
// pass the db
toDoRepositoryProvider.overrideWithValue(db),
],
child: RootApp(),
);
}
final toDoRepositoryProvider = Provider<ToDoRepository>((ref) {
throw UnimplementedError();
});
I totally agree with Mohammed Alfateh's decision. In addition, you can use ProviderContainer()..read(toDoDatasourceProvider) and UncontrolledProviderScope, to asynchronously assign values in main method. And in ToDoDatasourceImpl call the async method init() to assign a value in the field late final db.

Future<dynamic> is not a subtype of List<dynamic>

So I am trying to pass a list of String values from firestore table, but I am getting an exception type 'Future<dynamic>' is not a subtype of type 'List<dynamic>'
This is the function
getLectureList(String userId) async {
var collection = FirebaseFirestore.instance.collection('students');
var docSnapshot = await collection.doc(userId).get();
Map<String, dynamic>? data = docSnapshot.data();
List<String> _lectureList =
await data!['attendance']; //This line is kinda giving me trouble
userInfo = FirestoreWrapper()
.getStudentFromData(docId: currentUser(), rawData: data);
return _lectureList;
}
And this is the function where I am getting the exception thrown
#override
void initState() {
lectureList = getLectureList(currentUser()); // Getting an exception here
NearbyConn(context).searchDevices(devices: deviceList);
super.initState();
}
tried using await in the getLectureList() method but still getting the same problem
Why do you await your data? You already got it.
List<String> _lectureList = data!['attendance'];
Please note that I don't know what your data structure looks like, so I cannot tell you if this is correct, I can only tell you that it is more correct than before, because the await did not belong there.
You are getting an exception here lectureList = getLectureList(currentUser()); because the the parameter required by the getLectureList() method is the userId which is a string. I do not know what currentUser() return but I'm assuming it's the userId that you need when calling the getLectureList() method. Based on the error, it looks like currentUser() is an async method that returns a future after some time.
You're not awaiting that future. You shouldn't make the initState() method async so move the code block out of it into a separate method and then call it from initState().
Something like this,
#override
void initState() {
super.initState();
_getData();
}
void _getData() async {
lectureList =
getLectureList(await currentUser());
NearbyConn(context).searchDevices(devices: deviceList);
}
or
#override
void initState() {
super.initState();
_getData();
}
void _getData() async {
String _userID = await currentUser();
lectureList = getLectureList(_userID);
NearbyConn(context).searchDevices(devices: deviceList);
}
Which I recommend so you can see all the parts.
Making your method parameters required named parameters also help you to easily see what is needed to pass to a function/class/.
Eg.
getLectureList({required String userId}){
...
}
Your IDE will alert you on the type of object the function requires and it makes things clearer.
Ultimately, I think typing your classes makes it so much more easier to fetch data from fireStore Typing CollectionReference and DocumentReference
This way you can easily do this,
final moviesRef = FirebaseFirestore.instance.collection('movies').withConverter<Movie>(
fromFirestore: (snapshot, _) => Movie.fromJson(snapshot.data()!),
toFirestore: (movie, _) => movie.toJson(),
);
and get your data this way,
Future<void> main() async {
// Obtain science-fiction movies
List<QueryDocumentSnapshot<Movie>> movies = await moviesRef
.where('genre', isEqualTo: 'Sci-fi')
.get()
.then((snapshot) => snapshot.docs);
// Add a movie
await moviesRef.add(
Movie(
title: 'Star Wars: A New Hope (Episode IV)',
genre: 'Sci-fi'
),
);
// Get a movie with the id 42
Movie movie42 = await moviesRef.doc('42').get().then((snapshot) => snapshot.data()!);
}
Keeps everything dry and tidy.
< The data comes to list format thats why showing the exception of datatype >
List<String> lectureList = await getLectureList(currentUser()); // use
Future<List<String>> getLectureList(String userId) async {
- your code -
}
Instead of
List _lectureList =
await data!['attendance'];
Try this
_lectureList = await data![] As List

Riverpod StateNotifierProvider depend on a FutureProvider

I have a StateNotifierProvider that depends on a FutureProvider. Currently they look like below.
final catalogProvider = StateNotifierProvider<CatalogNotifier, CatalogState>((ref) {
final network = ref.watch(networkProvider.future); // future provider
return CatalogNotifier(network: network);
});
this makes my CatalogNotifier accept a Future<NetworkProvider> instead of NetworkProvider and requires me to do things like below.
await (await network).doGet(...)
What's the best way to avoid having to await multiple and allow CatalogNotifier to accept a bare NetworkProvider so I can write like await network.doGet(...) ?
for completeness as requested, below is the other related providers
final networkProvider = FutureProvider<Network>((ref) async {
final cache = await ref.watch(cacheProvider.future);
return Network(cacheManager: cache);
});
final cacheProvider = FutureProvider<CacheManager>((ref) async {
final info = await ref.watch(packageInfoProvider.future);
final key = 'cache-${info.buildNumber}';
return CacheManager(Config(
key,
stalePeriod: const Duration(days: 30),
maxNrOfCacheObjects: 100,
));
I'm sure I can take my cache provider as a future into the network provider, so it doesn't have to be a FutureProvider, but I'm interested in how to solve the issue above, since in another scenario, if I depend on say 3 or 4 FutureProviders, this may not be an option.
this makes my CatalogNotifier accept a Future instead of >NetworkProvider and requires me to do things like below.
I can't think of a way to get your desired result.
Could you not just accept an AsyncValue and handle it in the statenotifier?
final catalogProvider = StateNotifierProvider<CatalogNotifier, CatalogState>((ref) {
final network = ref.watch(networkProvider); // future provider
return CatalogNotifier(network: network);
});
Then you can:
void someFunction() async {
network.maybeWhen(
data: (network) => AsyncData(await network.doGet(...)),
orElse: () => state = AsyncLoading(),
);
}
with riverpod v2 and its codegen features this has become much easier since you no longer have to decide the type of the provider. (unless you want to)
StateNotifier in riverpod 2
#riverpod
Future<CatalogController> catalog(CatalogRef ref) async {
final network = await ref.watch(networkProvider.future);
return CatalogController(network: network);
}
Alternative approch in Riverpod 2
Quite often you want to have a value calculated and have a way to explicitely redo that calculation from UI. Like a list from network, but with a refresh button in UI. This can be modelled as below in riverpod 2.
#riverpod
Future<CatalogState> myFeed(MyFeedRef ref) async {
final json = await loadData('url');
return CatalogState(json);
}
// and when you want to refresh this from your UI, or from another provider
ref.invalidate(myFeedProvider);
// if you want to also get the new value in that location right after refreshing
final newValue = await ref.refresh(myFeedProvider);
Riverpod 2 also has loading and error properties for the providers. You can use these to show the UI accordingly. Though if you want to show the last result from the provider while your feed is loading or in an error state, you have to model this yourself with a provider that returns a stream/BehaviorSubject, caches the last value .etc.
you can make AsyncValue a subtype of StateNotifier, I use the Todo list as an example.
as follows:
class TodoNotifier extends StateNotifier<AsyncValue<List<Todo>>> {
TodoNotifier(this._ref) : super(const AsyncValue.loading()) {
_fetchData();
}
final Ref _ref;
Future<void> _fetchData() async {
state = const AsyncValue.loading();
// todoListProvider is of type FutureProvider
_ref.read(todoListProvider).when(data: (data) {
state = AsyncValue.data(data);
}, error: (err, stackTrace) {
state = AsyncValue.error(err, stackTrace: stackTrace);
}, loading: () {
state = const AsyncValue.loading();
});
}
void addTodo(Todo todo) {
if (state.hasValue) {
final todoList = state.value ?? [];
state = AsyncValue.data(List.from(todoList)..add(todo));
}
}
....
}

How can I read the string value from the _storageToken.read() call?

I have this construction in my homepage widget which is suppose to get the saved token. Now this returns a Future. So How can I read the token string? I tried storedToken.toString() but it returns Future<String?> not what I expected. I am expecting a String value like "fhsdjkfhs.djkfhsdjkfhsdjk"
void initState(){
const _storageToken = FlutterSecureStorage();
storedToken = _storageToken.read(key: 'jwt');
print(storedToken);
}
_storageToken.read() returns a Future, so you have to await it in order to get the value.
The problem is, you can't perform async/await action directly on the initState.
If you still want to call that _storageToken.read() on the initState, you can use this workaround.
void initState(){
/// Initialize Flutter Secure Storage
const _storageToken = FlutterSecureStorage();
/// Await your Future here (This function only called once after the layout is Complete)
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async {
final storedToken = await _storageToken.read(key: 'jwt');
print(storedToken);
});
}

How can i always setState when the page is opened?

My issue is, when the page is opened my variable isn't set. So when i'm querying from firestore i get the error The method '[]' was called on null. Receiver: null Tried calling: []("userbio")
I declared 2 variables
var userid;
var useremail;
Then i wrote this future
Future getUserData() async {
var firestore = Firestore.instance;
final FirebaseUser user = await FirebaseAuth.instance.currentUser();
final String uid = user.uid.toString();
final String email = user.email.toString();
DocumentReference qn = await firestore
.collection("users")
.document(email)
.collection('userdetails')
.document(uid);
void initState() {
setState(() {
userid = uid;
useremail = email;
});
}
return qn.documentID;
}
This is how i'm currently trying to setState but it's not working
void initState() {
setState(() {
userid = uid;
useremail = email;
});
}
Please help, thanks.
comment if you need more context
Seems like you're declaring the nested function initState() but you're not calling it. I would just remove the function and call setState() directly :
...
.document(uid);
setState(() {
...
EDIT : Call getUserData() inside initState :
// In a class extending State<T>
#override
void initState() {
super.initState();
// synchronous call if you don't care about the result
getUserData();
// anonymous function if you want the result
() async {
var result = await getUserData();
}();
}
PS : initState(), like build() or setState(), is a predefined name in Flutter. Try not to use those names for functions that are not overriding the original ones.
Try assign your variable an empty value instead of leaving it null.
var userid = '';
var useremail = '';