onFieldSubmitted of TextFormFeild repetitive call upon calling future function - Flutter - flutter

Scenario:
I have a TextFormFeild in which user write product name to search. If the searched product is not available in the database then I want to store that searched product in the database. To do that I have the function storeUserSearchedValue() which is calling future function inside it.
After looking googling this problem I have found that calling future during flutter's build phase causes repetitive invocations.
How can I store the value of the searched product in the database on the onFieldSubmitted function without facing non-stop repetirive invocation of the function?
Code
Class Displaying textfeild and and it's onFieldSubmitted function.
class SearchProductView extends StatefulWidget {
#override
_SearchProductViewState createState() => _SearchProductViewState();
}
class _SearchProductViewState extends State<SearchProductView> {
storeUserSearchedValue(String _userSearchedProduct,List<Product> _productsData) {
print("I N S I D E storeUserSearchedValue: " + _userSearchedProduct.toString());
int _noOfResultsOfUserProducts = 0;
for (int i = 0; i < _productsData.length; i++) {
if (_productsData[i].name.contains(_userSearchedProduct))
_noOfResultsOfUserProducts++;
}
if (_noOfResultsOfUserProducts == 0)
SearchviewModel().addCustomerSearchedProducts(_userSearchedProduct);
}
#override
Widget build(BuildContext context) {
final _allProductsData = Provider.of<List<Product>>(context);
return Scaffold(
............
TextFormField(
controller: _searchedProductText,
onFieldSubmitted:
storeUserSearchedValue(
_searchedProductText.text,
_allProductsData,
),
onChanged: searchOperation,
),
)
.........
}
Actual logic of storing data in the database
class SearchviewModel extends BaseViewModel {
// -------- Below Function are adding searched product in `customer` collection
addCustomerSearchedProducts(String searchedProductText) async {
String _currentUser = FirebaseAuth.instance.currentUser;
Map<String, dynamic> custdataMap = {
"custSearchedProducts": [searchedProductText, DateTime.now()]
};
await updateCustData(custdataMap, _currentUser.uid);
}
Future<bool> updateCustData(Map<String, dynamic> dataMap, String custID) async {
// - Dynamically adding data in the db
dataMap.forEach(
(key, value) async {
await FirebaseFirestore.instance.collection('customer').doc(custID).set(
{
key: value,
},SetOptions(merge: true),
);
},
);
return true;
}
}
Video representaion of problem

Simply adding this (_)=> (not this ()=>) with onFieldSubmitted property of textformfeild while calling storeUserSearchedValue function stop repetative invocations.
onFieldSubmitted: (_)=> storeUserSearchedValue(_searchedProductText.text,_allProductsData),
The above code snippet worked fine!
I'm not quite sure what is the reason behind it though

Related

Assigning a field value from firestore to variable in flutter

I am trying to get four text field values from a specific firestore document, assign them to four different variables and use them in the build context.
This is what I have been trying to do for just one field value:
class PackageScanPage extends StatefulWidget {
#override
State<PackageScanPage> createState() => _PackageScanPageState();
}
class _PackageScanPageState extends State<PackageScanPage> {
#override
Widget build(BuildContext context) {
final _firestore = FirebaseFirestore.instance;
final textFieldValue = ModalRoute.of(context)!.settings.arguments;
var _message;
_fetchData() async{
_firestore.collection("groceries").document('$textFieldValue').get().then((value) {
setState(() {
_message = value.data['name'];
});
});
}
return Container(
child: Text(_message.toString())
),
}
Thanks in advance.
What do you mean by 'get four text field values from a specific firestore document, assign them to four different variables'
if you are trying to pass the text field value from the app to firebase then I got you
TextField(
onSubmitted: (value) async {
FirebaseFirestore.instance.runTransaction(
(Transaction myTransaction) async {
FirebaseFirestore.instance
.collection('example')
.doc('example')
.update({'something': value});
}

Flutter GetX state management initial null value

This is what I'm trying to achieve using flutter GetX package but not working properly.
I have a Firestore document, if the document is changed I want to call an api and keep the data up to date as observable.
The code below seems to work but initial screen shows null error then it shows the data.
I don't know how I can make sure both fetchFirestoreUser() and fetchApiData() (async methods) returns data before I move to the home screen.
GetX StateMixin seems to help with async data load problem but then I don't know how I can refresh the api data when the firestore document is changed.
I'm not sure if any other state management would be best for my scenario but I find GetX easy compared to other state management package.
I would very much appreciate if someone would tell me how I can solve this problem, many thanks in advance.
Auth Controller.
class AuthController extends SuperController {
static AuthController instance = Get.find();
late Rx<User?> _user;
FirebaseAuth auth = FirebaseAuth.instance;
var _firestoreUser = FirestoreUser().obs;
var _apiData = ProfileUser().obs;
#override
void onReady() async {
super.onReady();
_user = Rx<User?>(auth.currentUser);
_user.bindStream(auth.userChanges());
//get firestore document
fetchFirestoreUser();
//fetch data from api
fetchApiData();
ever(_user, _initialScreen);
//Refresh api data if firestore document has changed.
_firestoreUser.listen((val) {
fetchApiData();
});
}
Rx<FirestoreUser?> get firestoreUser => _firestoreUser;
_initialScreen(User? user) {
if (user == null) {
Get.offAll(() => Login());
} else {
Get.offAll(() => Home());
}
}
ProfileUser get apiData => _apiData.value;
void fetchFirestoreUser() async {
Stream<FirestoreUser> firestoreUser =
FirestoreDB().getFirestoreUser(_user.value!.uid);
_firestoreUser.bindStream(firestoreUser);
}
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
#override
void onDetached() {}
#override
void onInactive() {}
#override
void onPaused() {}
#override
void onResumed() {
fetchApiData();
}
}
Home screen
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: Obx(() =>
Text("username: " + AuthController.instance.apiData.username!))),
),
);
}
}
To be honest, I never used GetX so I'm not too familiar with that syntax.
But I can see from your code that you're setting some mutable state when you call this method:
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
Instead, a more robust solution would be to make everything reactive and immutable. You could do this by combining providers if you use Riverpod:
final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
final authService = ref.watch(authRepositoryProvider);
return authService.authStateChanges();
});
final apiDataProvider = FutureProvider.autoDispose<APIData?>((ref) {
final userValue = ref.watch(authStateChangesProvider);
final user = userValue.value;
if (user != null) {
// note: this should also be turned into a provider, rather than using a static method
return RemoteService.getProfile(user.uid);
} else {
// decide if it makes sense to return null or throw and exception when the user is not signed in
return Future.value(null);
}
});
Then, you can just use a ConsumerWidget to watch the data:
#override
Widget build(BuildContext context, WidgetRef ref) {
// this will cause the widget to rebuild whenever the auth state changes
final apiData = ref.watch(apiDataProvider);
return apiData.when(
data: (data) => /* some widget */,
loading: () => /* some loading widget */,
error: (e, st) => /* some error widget */,
);
}
Note: Riverpod has a bit of a learning curve (worth it imho) so you'll have to learn it how to use it first, before you can understand how this code works.
Actually the reason behind this that you put your controller in the same page that you are calling so in the starting stage of your page Get.put() calls your controller and because you are fetching data from the API it takes a few seconds/milliseconds to get the data and for that time your Obx() renders the error. To prevent this you can apply some conditional logic to your code like below :
Obx(() => AuthController.instance.apiData != null ? Text("username: " + AuthController.instance.apiData.username!) : CircularProgressIndicator())) :

I want to use data from a Future inside a ChangeNotifier Provider and a ListView

I can't figure out how to get the data from the myProvider before I call the getWalletItems(). Should I do 2 seperate providers??
My goal here is just to get all these items from a Future<List<Wallet'>> and return them into a listview that is able to have each item be selectable with a checkbox which will then pass on all the selected items to a different page. They will not be rebuilt there so I don't think I need another model but if I do just let me know. Here is my code for the ChangeNotifier:
class WalletModel extends ChangeNotifier {
List<Wallet> _wallet = [];
List<Wallet> get wallet => _wallet;
set wallet(List<Wallet> newValue) {
_wallet = newValue;
notifyListeners();
}
myProvider() {
loadValue();
}
Future<void> loadValue() async {
wallet = await WalletApi.getWalletItems();
}
UnmodifiableListView<Wallet> get allWalletItems =>
UnmodifiableListView(_wallet);
UnmodifiableListView<Wallet> get incompleteTasks =>
UnmodifiableListView(_wallet.where((_wallet) => !_wallet.isSelected));
UnmodifiableListView<Wallet> get completedTasks =>
UnmodifiableListView(_wallet.where((_wallet) => _wallet.isSelected));
void toggleWallet(Wallet wallet) {
final walletIndex = _wallet.indexOf(wallet);
_wallet[walletIndex].toggleSelected();
notifyListeners();
}
}
Here is the checkbox to select
Checkbox(
value: wallet.isSelected,
onChanged: (bool? checked) {
Provider.of<WalletModel>(context, listen: false)
.toggleWallet(wallet);
},
),
Here is the listview and if I need to post anyother code just let me know because I'm quite lost on what to do.
class WalletList extends StatelessWidget {
final List<Wallet> wallets;
WalletList({required this.wallets});
#override
Widget build(BuildContext context) {
return ListView(
children: getWalletListItems(),
);
}
List<Widget> getWalletListItems() {
return wallets
.map((walletItem) => WalletListItem(wallet: walletItem))
.toList();
}
}
make myProvider() a future and then use below code for WalletList Widget
before build runs for WalletList we want to get the items from the provider so we have used didChangedDependencies() as it runs before build and can be converted to future.
when the list is got we use the list that was set by above the make the UI
Note : Consumer changes its state whenever notifyListener() is called in Provider.
import 'package:flutter/material.dart';
class WalletList extends StatefulWidget {
#override
_WalletListState createState() => _WalletListState();
}
class _WalletListState extends State<WalletList> {
bool _isInit = true;
#override
void didChangeDependencies() async {
//boolean used to run the set list fucntion only once
if (_isInit) {
//this will save the incoming data to list before build runs
await Provider.of<WalletModel>(context, listen: false).myProvider();
_isInit = false;
}
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
return Consumer<WalletModel>(builder: (context, providerInstance, _) {
return ListView(
children: providerInstance
.wallet
.map<Widget>((walletItem) => WalletListItem(wallet: walletItem))
.toList(),
);
});
}
// List<Widget> getWalletListItems() {
// return Provider.of<WalletModel>(context, listen: false)
// .wallet
// .map((walletItem) => WalletListItem(wallet: walletItem))
// .toList();
// }
}

Flutter - Providers and Future calls, how to share the same instance?

I'm learning Flutter and there is something I cannot grasp my head around.
I implemented a Infinite scroll pagination, with a package (infine_scroll_pagination),
it works fine, but the data this Package is getting, comes from a Future call, which takes data from the WEB, and parses it in my Provider Class.
My issue is, the data that is loaded by the Infinite Scroll widget, cannot be accessed, in its state, anywhere else.
Example:
Let's take a contact list, that loads 10 contacts at a time:
class ContactsBody extends StatefulWidget {
#override
_ContactsBodyState createState() => _ContactsBodyState();
}
class _ContactsBodyState extends State<ContactsBody> {
static const _pageSize = 10;
final PagingController<int, Contact> pagingController =
PagingController(firstPageKey: 0);
#override
void initState() {
super.initState();
pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
}
Future<void> _fetchPage(int pageKey) async {
try {
final newItems = await ContactsService().fetchContactsPaged(pageKey, _pageSize);
final isLastPage = newItems.length < _pageSize;
if (isLastPage) {
pagingController.appendLastPage(newItems.contacts);
} else {
final nextPageKey = pageKey + 1;
pagingController.appendPage(newItems.contacts, nextPageKey);
}
} catch (error) {
pagingController.error = error;
}
}
#override
Widget build(BuildContext context) {
return ContactsList(pagingController);
}
#override
void dispose() {
pagingController.dispose();
super.dispose();
}
So basically this Infinite Scroll package, will fetch my contacts, 10 at a time, and here my ContactsService call:
Future<Contacts> fetchContactsPaged(int pageKey, int pageSize) async {
final response = await http.get(.....);
if (response.statusCode == 200) {
return Contacts.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load contacts');
}
}
And finally, as you can see here above, it initializes my Provider class (Contacts), using its factory method, "fromJson()", and returns the parsed data.
Now my Provider class:
class Contacts extends ChangeNotifier {
List<Contact> _contacts = <Contact>[];
Contacts();
factory Contacts.fromJson(final Map<String, dynamic> json) {
final Contacts contacts = Contacts();
if (json['data'] != null) {
json['data'].forEach((contact) {
contacts.add(Contact.fromJson(contact));
});
}
return contacts;
}
void add(final Contact contact) {
this._contacts.add(contact);
this.notifyListeners();
}
The problem I'm having here is, when the Inifinite Scroll listView is loaded, and for example I change the state of a single contact (contacts can be set as favorite for example),
How can I access the SAME instance of the Contacts() class, that the FUTURE call initialized, so that I can access the current state of the data in that class?
Of course if I were to POST my changes onto the API, and refetch the new values where I need them, I would get the updated state of my data, but I want to understand how to access the same instance here and make the current data available inside the app everywhere
EDIT : I removed the original answer to give a better sample of what the OP wants to achieve.
I made a repo on GitHub to try to show you what you want to achieve: https://github.com/Kobatsu/stackoverflow_66578191
There are a few confusing things in your code :
When to create instances of your objects (ContactsService, Contacts)
Provider usage
(Accessing the list of the pagingController ?)
Parsing a JSON / using a factory method
The repository results in the following :
When you update the list (by scrolling down), the yellow container is updated with the number of contacts and the number of favorites.
If you click on a Contact, it becomes a favorite and the yellow container is also updated.
I commented the repository to explain you each part.
Note: the Contacts class in your code became ContactProvider in mine.
The ContactsService class to make the API call :
class ContactsService {
static Future<List<Contact>> fetchContactsPaged(
int pageKey, int pageSize) async {
// Here, you should get your data from your API
// final response = await http.get(.....);
// if (response.statusCode == 200) {
// return Contacts.fromJson(jsonDecode(response.body));
// } else {
// throw Exception('Failed to load contacts');
// }
// I didn't do the backend part, so here is an example
// with what I understand you get from your API:
var responseBody =
"{\"data\":[{\"name\":\"John\", \"isFavorite\":false},{\"name\":\"Rose\", \"isFavorite\":false}]}";
Map<String, dynamic> decoded = json.decode(responseBody);
List<dynamic> contactsDynamic = decoded["data"];
List<Contact> listOfContacts =
contactsDynamic.map((c) => Contact.fromJson(c)).toList();
// you can return listOfContacts, for this example, I will add
// more Contacts for the Pagination plugin since my json only has 2 contacts
for (int i = pageKey + listOfContacts.length; i < pageKey + pageSize; i++) {
listOfContacts.add(Contact(name: "Name $i"));
}
return listOfContacts;
}
}
Usage of Provider :
Consumer<ContactProvider>(
builder: (_, foo, __) => Container(
child: Text(
"${foo.contacts.length} contacts - ${foo.contacts.where((c) => c.isFavorite).length} favorites"),
padding: EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
color: Colors.amber,
)),
Expanded(child: ContactsBody())
]),
)
Fetch page method in the ContactsBody class, where we add the contact to our ContactProvider :
Future<void> _fetchPage(int pageKey) async {
try {
// Note : no need to make a ContactsService, this can be a static method if you only need what's done in the fetchContactsPaged method
final newItems =
await ContactsService.fetchContactsPaged(pageKey, _pageSize);
final isLastPage = newItems.length < _pageSize;
if (isLastPage) {
_pagingController.appendLastPage(newItems);
} else {
final nextPageKey = pageKey + newItems.length;
_pagingController.appendPage(newItems, nextPageKey);
}
// Important : we add the contacts to our provider so we can get
// them in other parts of our app
context.read<ContactProvider>().addContacts(newItems);
} catch (error) {
print(error);
_pagingController.error = error;
}
}
ContactItem widget, in which we update the favorite statuts and notify the listeners :
class ContactItem extends StatefulWidget {
final Contact contact;
ContactItem({this.contact});
#override
_ContactItemState createState() => _ContactItemState();
}
class _ContactItemState extends State<ContactItem> {
#override
Widget build(BuildContext context) {
return InkWell(
child: Padding(child: Row(children: [
Expanded(child: Text(widget.contact.name)),
if (widget.contact.isFavorite) Icon(Icons.favorite)
]), padding: EdgeInsets.symmetric(vertical: 8, horizontal: 10),),
onTap: () {
// the below code updates the item
// BUT others parts of our app won't get updated because
// we are not notifying the listeners of our ContactProvider !
setState(() {
widget.contact.isFavorite = !widget.contact.isFavorite;
});
// To update other parts, we need to use the provider
context.read<ContactProvider>().notifyContactUpdated(widget.contact);
});
}
}
And the ContactProvider :
class ContactProvider extends ChangeNotifier {
final List<Contact> _contacts = [];
List<Contact> get contacts => _contacts;
void addContacts(List<Contact> newContacts) {
_contacts.addAll(newContacts);
notifyListeners();
}
void notifyContactUpdated(Contact contact) {
// You might want to update the contact in your database,
// send it to your backend, etc...
// Here we don't have these so we just notify our listeners :
notifyListeners();
}
}

Flutter: Strange behavour of StreamProvider, widgets rebuilt with incomplete data

I am getting incomplete data from a StreamProvider.
The next minimum widget tree reproduces my problem: A SreamProvider over a Tabbed Screen.
My User object contains a map of several values (among other properties) and I use these values for one screen of the Tabbed View by calling final user = Provider.of<User>(context); inside the build() method in order to get these values in this screen whenever the app starts or is completely rebuilt (i.e. hot reload).
The problem: Whenever I switch tabs and go back to this tab, the build() method is called only once with final user = Provider.of<User>(context); returning an incomplete copy of User: some data is missing (some properties of a map inside the User object) and build() is never called again to complete the data (given that the StreamProvider should return the complete object at some point, possibly causing some rebuilds. The result: some data is missing and some widgets of the Screen cannot be built.
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final firebaseUser = Provider.of<FirebaseUser>(context);
// return either Home or Authenticate widget:
if (firebaseUser == null) {
return WelcomeScreen();
} else {
return StreamProvider<User>.value(
value: FirestoreService(uid: firebaseUser.uid).user,
child: TabsWrapper(),
);
}
}
}
class TabsWrapper extends StatefulWidget {
final int initialIndex;
TabsWrapper({this.initialIndex: 0});
#override
_TabsWrapperState createState() => _TabsWrapperState();
}
class _TabsWrapperState extends State<TabsWrapper> with TickerProviderStateMixin {
TabController tabController;
#override
void initState() {
super.initState();
tabController = TabController(vsync: this, length: choices.length);
}
#override
Widget build(BuildContext context) {
return DefaultTabController(
initialIndex: widget.initialIndex,
length: 3,
child: Scaffold(
backgroundColor: lightBlue,
bottomNavigationBar: TabBar(
controller: tabController,
labelStyle: navi.copyWith(color: darkBlue),
unselectedLabelColor: darkBlue,
labelColor: darkBlue,
indicatorColor: darkBlue,
tabs: choices.map((TabScreen choice) {
return Tab(
text: choice.title,
icon: Icon(choice.icon, color: darkBlue),
);
}).toList(),
),
body: TabBarView(
controller: tabController,
children: <Widget>[
FirstScreen(),
SecondScreen(),
ThirdScreen(),
],
),
),
);
}
}
The problematic Screen (FirstScreen):
class FirstScreen extends StatelessWidget {
final TabController tabController;
final User user;
FirstScreen ();
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
print('**************************************** ${user.stats}'); //Here I can see the problem
return SomeBigWidget(user: user);
}
}
The print(${user.stats}) shows the incomplete map (stats) and the build() is never called again (with the remaining data) so the User object remains incomplete. It is only called twice (and the data returned with the complete object) when reloading or launching the app.
Any solution will be welcome!
PD: I found a simpler way to reproduce the situation. No need to change tabs:
Inside FirstScreen() I have a column of StatelessWidgets. If I call Provider.of<User>(context) within one of them I get an incomplete version of the object. The user.stats map has half of the key-value pairs that it has when accesing it through Provider.of some widgets above.
This is how the stream is updated from Firebase. (the object is created every time):
Stream<User> get user {
Stream<User> userStream = usersCollection.document(uid).snapshots().map((s) => User.fromSnapshot(s));
return userStream;
}
I also have updateShouldNotify = (_,__) => true;
The User model:
class User {
String uid;
String name;
String country;
Map stats;
User.fromUID({#required this.uid});
User.fromSnapshot(DocumentSnapshot snapshot)
: uid = snapshot.documentID,
name = snapshot.data['name'] ?? '',
country = snapshot.data['country'] {
try {
stats = snapshot.data['stats'] ?? {};
} catch (e) {
print('[User.fromSnapshot] Exception building user from snapshot');
print(e);
}
}
}
This is the stats data Map in Firestore:
Although firebase does allow you to use a map as a type for you to use I don't think it is your best option in this case, as it seems to be confusing.
There are multiple solutions to your problem.
The best one I think is to make a collection side by side your users collection called "stats" and have the docid of each stat = the userid of the user with that stats, this makes querying easier for you in the future.
after this you can use the method you have already used to get the stats just like you get the users.
class Stat {
String statid;
int avg;
int avg_score;
List<int> last_day;
int n_samples;
int total_score;
int words;
Stat.fromSnapshot(DocumentSnapshot snapshot){
statid = snapshot.documentID,
avg = snapshot.data['average'] ?? '',
//rest of data
}
}
perhaps the simple solution that you need is to change your map in the User class to this.
Map<String, dynamic> stats = Map<String, dynamic>();
remove the try and catch block, then you should be able to access your data like this user.stats['average'];
hope this helps, I will update my answer when I can actually put this to the test in an emulator, but if you try it and it works let me know.