Flutter: How to Access Data & NotifyListeners Outside a Stream - flutter

I have a list of items and a stream within a class. The stream triggers a future where then notifylisteners is called to update the list of items. It works, but it only shows updates within the stream. How do I notifylistners outside the stream as well?
Where, if I were to call Provider.of(context).items it won't return as empty.
Here is the following code structure.
class Mans with ChangeNotifier {
List<Man> _items = [];
List<Man> get items {
return [..._items];
}
Stream<List<Man>> stream;
bool hasMore;
bool _isLoading;
List<Man> _data;
StreamController<List<Man>> _controller;
Mans({page = 1}) {
_data = List<Man>();
_controller = StreamController<List<Man>>.broadcast();
_isLoading = false;
// Test if list prints #1
items.forEach((list) {
print("nono: ${list.id}");
});
stream = _controller.stream.map((List<Man> mansData) {
// Test if list prints #2
items.forEach((list) {
print("nono: ${list.id}");
});
return mansData;
});
// Test if list prints #3
items.forEach((list) {
print("nono2: ${list.id}");
});
hasMore = true;
refresh();
}
Future<void> refresh() {
return loadMore(
page: 1,
clearCachedData: true,
);
}
Future<void> loadMore(
{bool clearCachedData = false,
page = 1}) async {
if (clearCachedData) {
_data = List<Man>();
hasMore = true;
}
if (_isLoading || !hasMore) {
return Future.value();
}
_isLoading = true;
return await fetchAndSetMans(page)
.then((mansData) {
_isLoading = false;
_data.addAll(mansData);
hasMore = (mansData.isNotEmpty);
_controller.add(_data);
});
}
Future<List<Man>> fetchAndSetMans(page) async {
var cookie = '';
try {
print('Called_API_Mans');
var response = await SiteApi(serverConfig["url"]).getAsync("api_link?view_id=$page");
List<Man> list = [];
for (var item in response) {
//This just adds an instance of Man to the list from a Model not added to this Stack Question. It works.
list.add(Man.fromJson(item));
}
_items = list;
notifyListeners();
return _items;
} catch (error) {
return [];
}
}
As you can see, I placed three different instances where I can print the items after notifyListeners() is called in the Future 'fetchAndSetMans'.
Unfortunately, only in the one where the comment says "Test if list prints #2" does it show that the list has been updated. Basically, within the stream data.
#1 and #3 are empty.
So, anything outside of the stream, notifyListeners() doesn't update the items list.
I wish to know how I can update the value outside the stream when the future is called.
So, if I call a Provider.... like, Provider.of(context).items... I can actually get results.
Thanks, I'd appreciate any help.

Related

Flutter: _TypeError

I'm trying to get datas from api and add them a list. But at this moment, I see datas i got but I can't get it out of the function. What should i do?
function
List<dynamic> xxx = [];
#override
void initState() {
super.initState();
Future<List<dynamic>> fetchCompanies(List<dynamic> datas) async {
var response = await Dio().get(CompaniesPath().url);
if (response.statusCode == HttpStatus.ok) {
Map<String, dynamic> company = jsonDecode(response.data);
for (int i = 0; i < company['Data'].length; i++) {
datas.add(company['Data'][i]);
}
//print(datas); //=> I see datas here
} else {
throw Exception();
}
return datas;
}
print(fetchCompanies(xxx));
}
When I run print(fetchCompanies(xxx)); I got "Instance of 'Future<List<dynamic>>'". How can i get data inside fetchCompanies to my xxx list?
You're trying to print future instance of List that's why you got
Instance of Future<List>
You have to wait until function finish executing.
Catch here is you can't call wait in initState() so you have to use .then method
try this:
fetchCompanies(xxx)
.then((result) {
print("result: $result");
});
It should already work fine like it is. But you probably want to call a setState to refresh the page. Try this:
#override
void initState() {
super.initState();
Future<List<dynamic>> fetchCompanies(List<dynamic> datas) async {
var response = await Dio().get(CompaniesPath().url);
if (response.statusCode == HttpStatus.ok) {
Map<String, dynamic> company = jsonDecode(response.data);
for (int i = 0; i < company['Data'].length; i++) {
datas.add(company['Data'][i]);
}
//print(datas); //=> I see datas here
setState(() {}); // added this
} else {
throw Exception();
}
return datas;
}
print(fetchCompanies(xxx));
}

NotifyListner is not working for List<String> and working for other object

In SearchData class any change in result update the UI but any change in history, is not updating the UI, even tho the implemenation of both is same. I'm calling notify for both of em.
Below is the class code
class SearchData<T> {
List<R> copyList<R>(List<R>? list) => List.from(list ?? []);
SearchData(this.notify, this.historyKey) {
_loadHistory();
}
final void Function([VoidCallback? action]) notify;
final String historyKey;
int page = 1;
List<T>? _result;
List<T>? get result => _result;
// search history
List<String> _history = [];
List<String> get history => _history;
void _loadHistory() async {
final data = await DataBox(historyKey).readStringList();
if (kDebugMode) {
print('hello loaded history = $data');
}
if (data != null) notify(() => _history = copyList(data));
}
void updateSearchResult(String query, List<T>? newResult, bool isLoadMore) {
if (newResult == null) return;
notify(() {
if (isLoadMore) {
_result = copyList(result)..addAll(newResult);
} else {
_result = newResult;
}
});
// save local history of search
if (!_history.contains(query)) {
_history.add(query);
DataBox(historyKey).writeStringList(history);
}
}
}
I tried the workoaroud of creating new instance of list, so notify listner could update the UI.

When I am using the provider package in Flutter to load data from an API into a list it repeatedly calls the API, how do I fix it?

I am trying to lode data from an api call that retrieves a map, I am able to get the map from the api to display how I want it to, however it repeatedly calls the api meaning the list keeps on refreshing. Even though I have tried setting the listener to false, it works but I have to manually refresh the app for it to work?
Additional Info: Assigning and Retrieving Data
import 'package:http/http.dart' as http;
class Stores with ChangeNotifier {
var s_length;
Future<List<Store>> getStores(String storeCatName) async {
final queryParameters = {
"store_category_name": storeCatName,
};
try {
//TODO this is the issue - must fix.
final uri = Uri.http("url", 'url', queryParameters);
//print(uri);
final response = await http.get(uri);
//print(response.statusCode);
//print(response.body);
if (response.statusCode == 200) {
final List<Store> stores = storeFromJson(response.body);
_stores = stores;
//print(_stores);
print("lenght: ${_stores.length}");
Store store;
for(store in _stores) {
store.products = Products().products(store.storeId);
}
//check if this is correct
notifyListeners();
//return stores;
} else {
print("error1");
return List<Store>();
}
} catch (e) {
print(e.toString());
return List<Store>();
}
//notifyListeners();
print(_stores);
}
List<Store> get favoriteItems {
//return _stores.where((storeItem) => storeItem.isFavorite).toList();
}
bool isNotFull(){
if (_stores.isEmpty){
return true;
} else {
return false;
}
}
int get numberOfStores{
return s_length;
}
List<Store> _stores = [];
List<Store> stores (String storeCatName){
getStores(storeCatName);
//print("cpp; + $s_length");
//notifyListeners();
return _stores;
}
}
final storesProvider = Provider.of<Stores>(
context, listen: false
);
storesProvider.getStores(categoryName);
final providerStoreList = storesProvider.stores(category.storeCategoryName);
Additional Info: Builder for List:
child: ListView.builder(
itemCount: providerStoreList.length,
itemBuilder: (context, index) => ChangeNotifierProvider.value(
value: providerStoreList[index],
child: StoreItem(),
)));
If any additional information is required just let me know. Any help would be greatly appreciated.
Thanks
Use
listen: false;
var ourClient = Provider.of<CartBlock>(context, listen: false);
Setting the listener to false means that your widget won't build again when notifyListeners() is called.
So, that might not be the issue.
The only reason I can think of is calling the API again from the build method,
which might happen if you are using a ListView builder.
So, every time you might be scrolling the ListView your API would call again.

Pagination with streams in flutter

I have implemented a pagination scheme in my flutter app but im not sure if its performance friendly and if it may result to trouble on production in future so i would like to get advice on it.
here is my implementation
First, i get data using a stream provider in my parent widget.
class BuyerSellerPostsPage extends StatefulWidget {
#override
_BuyerSellerPostsPageState createState() => _BuyerSellerPostsPageState();
}
class _BuyerSellerPostsPageState extends State<BuyerSellerPostsPage> {
...some code here...
bool isAtBottom = false;
int postToDisplay=10;
#override
void initState() {
super.initState();
// Setup the listener.
_scrollController.addListener(() {
if (_scrollController.position.atEdge) {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
setState(() {
isAtBottom=true;
postToDisplay+=10;
print('now true');
});
}else{
setState(() {
print('now false');
isAtBottom=false;
});
}
}
});
}
#override
void dispose(){
_scrollController.dispose();
super.dispose();
}
...some code here..
#override
Widget build(BuildContext context) {
Position coordinates=Provider.of<Position>(context);
...some code here...
body: SingleChildScrollView(
controller: _scrollController,
child: StreamProvider<List<SellerPost>>.value(
value: SellerDatabaseService(
currentLocation: new GeoPoint(coordinates.latitude, coordinates.longitude,)
,filters: _setFilters,
selectedCategory: _selectedCategory,
selectedTag: _selectedTag,
postsToDisplay: postToDisplay
).inRangeSellerPosts ,
...some code here...
);
}else{
return Container(
);
}
},
),
),
//post list
BSellerPostList(),
...some code here...
}
}
The initial posts to display are 10.
In my initstate i have used a listener to my scroll controller so that when the user scrolls to the bottom more items(+10) are loaded on screen.
In my stream provider i pass the postsToDisplay int to my stream in the backend below
Stream <List<SellerPost>> get inRangeSellerPosts {
try {
return sellerPostCollection
.where("expireTime" , isGreaterThan: DateTime.now())
.orderBy('expireTime',descending: true)
.snapshots()
.map(yieldSellerPosts);
} catch (e) {
print(e.toString());
return null;
}
}
List<SellerPost> yieldSellerPosts(QuerySnapshot snapshot) {
List<String> l = [];
print(snapshot.documents.length);
try {
return snapshot.documents.map((doc) {
return SellerPost(
...some code here...
);
}).take(postsToDisplay)
.toList();
} catch (e) {
print(e.toString());
return null;
}
}
I now get the snapshots and use the take method on the list to get only the required number(postsToDisplay).
This method works fine in my debug mode. Im not sure how it will behave in production or with large data sets. Could someone scrutinise it, i would appreciate alot.
I personally used a modified version of this answer posted in a previous similar question.
Both my implementation and that other guys implementation have the pros and cons.
His method leads to listening to document changes for documents you may never need to use considering you may need only 10 paginated items. My method on the other hand does not work along with the stream. It uses a future to query for document snapshots to update the list as you proceed.
Here is the sample code
bool _isRequesting = false;
bool _isFinish = false;
final _scrollController = ScrollController();
List<DocumentSnapshot> _posts = [];
#override
void initState() {
_scrollController.addListener(() {
if (_scrollController.position.atEdge) {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
setState(() {
requestNextPage();
});
}
}
});
requestNextPage();
super.initState();
}
void requestNextPage() async {
try{
if (!_isRequesting && !_isFinish) {
QuerySnapshot querySnapshot;
_isRequesting = true;
if (_posts.isEmpty) {
//check if _posts list is empty so that we may render the first list of 10 items.
querySnapshot = await Firestore.instance
.collection('sellerPost')
.limit(10)
.getDocuments();
} else {
//if _posts list is not empty it means we have already rendered the first 10 items so we start querying from where we left off to avoid repetition.
querySnapshot = await Firestore.instance
.collection('sellerPost')
.startAfterDocument(_posts[_posts.length - 1])
.limit(10)
.getDocuments();
}
if (querySnapshot != null) {
int oldSize = _posts.length;
_posts.addAll(querySnapshot.documents);
int newSize = _posts.length;
if (oldSize == newSize) {
_isFinish = true;
}
_isRequesting = false;
}else{
_isFinish = false;
_isRequesting = false;
}
}catch(e){
print(e.toString());
}
}
So in this above code i used the scroll controller to detect when the user scrolls to the bottom of the page with the paginated items e.g 10 posts. This event triggers my function requestNextPage(); Note that on inititState we also call the requestNextPage(); to render the initial 10 posts.
So now, the each time a scroll to bottom is detected, 10 extra posts are added to
_posts

Flutter flutter_in_app_purchases subscription FlutterInAppPurchses.instance.getSubscriptions() is not retrieving any items for IAPItem

I'm trying to implement a renewable subscription in flutter using the flutter_in_app_purchases plugin. When I click on the screen that this is declared in, it goes through the initState() function and then gets to the initPlatformState() and goes through that successfully, but when it gets to the getProducts() function, it's returning an empty item list for the List items = FlutterInappPurchase.instance.getSubscriptions([productID]); call. I've added the monthly subscription in both the App Store Connect and Google Play Store and completed the tax forms. Any help would be appreciated.
List<IAPItem> _items = [];
static const String productID = 'monthly_subscription';
#override
void initState() {
super.initState();
print("IN INIT STATE");
initPlatformState();
}
Future<void> initPlatformState() async {
print("In init platform state");
// prepare
final bool available = await InAppPurchaseConnection.instance.isAvailable();
print(available);
var close = await FlutterInappPurchase.instance.endConnection;
var result = await FlutterInappPurchase.instance.initConnection;
print('result: $result');
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) {
print('In not mounded');
return;
}
// refresh items for android
/*try {
String msg = await FlutterInappPurchase.instance.consumeAllItems;
print('consumeAllItems: $msg');
} catch(e){
print(e.toString());
}*/
await _getProduct();
}
Future<Null> _getProduct() async {
print("In get products");
try {
List<IAPItem> items = await FlutterInappPurchase.instance.getSubscriptions([productID]);
print("Items is: $items");
for (var item in items) {
print('${item.toString()}');
this._items.add(item);
}
setState(() {
this._items = items;
});
} catch(e) {
print(e.toString());
}
}
Here you have a working example from app in production. Disclaimer: I'm not using it anymore but the last time I did it worked fine:
class _InAppState extends State<InApp> {
StreamSubscription _purchaseUpdatedSubscription;
StreamSubscription _purchaseErrorSubscription;
StreamSubscription _conectionSubscription;
final List<String> _productLists = Platform.isAndroid
? [
'subs_premium', 'subs_user'
]
: ['subs_premium', 'subs_boss', 'subscripcion_user'];
String _platformVersion = 'Unknown';
List<IAPItem> _items = [];
List<IAPItem> _subscripions = [];
List<PurchasedItem> _purchases = [];
#override
void initState() {
super.initState();
initPlatformState();
}
#override
void dispose() {
super.dispose();
if (_conectionSubscription != null) {
_conectionSubscription.cancel();
_conectionSubscription = null;
}
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
String platformVersion;
// Platform messages may fail, so we use a try/catch PlatformException.
try {
platformVersion = await FlutterInappPurchase.instance.platformVersion;
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
// prepare
var result = await FlutterInappPurchase.instance.initConnection;
print('result: $result');
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
// refresh items for android
try {
String msg = await FlutterInappPurchase.instance.consumeAllItems;
print('consumeAllItems: $msg');
} catch (err) {
print('consumeAllItems error: $err');
}
_conectionSubscription = FlutterInappPurchase.connectionUpdated.listen((connected) {
print('connected: $connected');
});
_purchaseUpdatedSubscription = FlutterInappPurchase.purchaseUpdated.listen((productItem) {
print('purchase-updated: $productItem');
});
_purchaseErrorSubscription = FlutterInappPurchase.purchaseError.listen((purchaseError) {
print('purchase-error: $purchaseError');
});
final List<String> _SKUS = widget.premium ? ['subs_boss']
: ['subs_user'] ;
_getSubscriptions(_SKUS);
}
void _requestPurchase(IAPItem item) {
FlutterInappPurchase.instance.requestPurchase(item.productId);
}
Future _getProduct() async {
print('TEST 1 HERE ${_productLists.length}, ${_productLists.first.toString()}');
List<IAPItem> items = await FlutterInappPurchase.instance.getProducts(_productLists);
print('TEST 2 HERE ${items.length}');
for (var item in items) {
print('${item.toString()}');
this._items.add(item);
}
setState(() {
this._items = items;
this._purchases = [];
});
}
Future _getPurchases() async {
List<PurchasedItem> items =
await FlutterInappPurchase.instance.getAvailablePurchases();
for (var item in items) {
print('${item.toString()}');
this._purchases.add(item);
}
setState(() {
this._items = [];
this._purchases = items;
});
}
Future _getSubscriptions(_SKUS) async {
List<IAPItem> items =
await FlutterInappPurchase.instance.getSubscriptions(_SKUS);
for (var item in items) {
print('${item.toString()}');
this._subscripions.add(item);
}
setState(() {
this._items = [];
this._subscripions = items;
});
}
Future _getPurchaseHistory() async {
List<PurchasedItem> items = await FlutterInappPurchase.instance.getPurchaseHistory();
for (var item in items) {
print('${item.toString()}');
this._purchases.add(item);
}
setState(() {
this._items = [];
this._purchases = items;
});
}