Displaying CircularProgressIndicator() will not stop after API call is completed - flutter

I'm attempting to have a CircularProgressIndicator display while the API call is made. When navigating to the OrdersScreen the CircularProgressIndicator displays and does not stop.
When clicking on the error it is directing me to my catch in my try{} catch{} block in my API call.
Here is the error I'm encountering:
I/flutter (22500): Invalid argument(s) (input): Must not be null
E/flutter (22500): [ERROR:flutter/lib/ui/ui_dart_state.cc(186)] Unhandled Exception: Invalid argument(s) (input): Must not be null
[38;5;248mE/flutter (22500): #0 Orders.getOrders[39;49m
E/flutter (22500): <asynchronous suspension>
[38;5;248mE/flutter (22500): #1 _OrdersScreenState.initState.<anonymous closure> (package:shop_app/screens/order_screen.dart)[39;49m
E/flutter (22500): <asynchronous suspension>
E/flutter (22500):
Here is my API call:
class Orders with ChangeNotifier {
List<OrderItem> _orders = [];
List<OrderItem> get orders {
return [..._orders];
}
//make a copy of private class _orders
//establishing so that we cannot modify the private class
//READ API call
Future<void> getOrders() async {
final url = Uri.https(
'shop-app-flutter-49ad1-default-rtdb.firebaseio.com', '/products.json');
//note that for the post URL when using this https package we had to remove the special characters (https://) in order to properly post via the API
//establish the URL where the API call will be made
try {
final response = await http.get(url);
// print(json.decode(response.body));
final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
//retrieve the json response data stored in firebase, translate to a Map, and store that map in the jsonResponse variable
if (jsonResponse == null) {
return;
}
//if there is no data returned in the jsonResponse (the db is empty) then we do nothing, avoiding an app crash on an empty API call
final List<OrderItem> orderProducts = [];
//establish an empty list in preparation to store the new Order values retrieved from the API call
jsonResponse.forEach((orderID, orderData) {
//forEach will exectue a function on every value that is housed within that Map
orderProducts.insert(
0, //insert at index 0 inserts the newest added product at the beginning of the list
OrderItem(
id: orderID,
amount: orderData['amount'],
dateTime: DateTime.parse(orderData['dateTime']),
products: (orderData['products'] as List<dynamic>)
.map(
(item) => CartItem(
id: item['id'],
title: item['title'],
quantity: item['quantity'],
price: item['price'],
),
)
.toList(),
//since products is stored on the db as a map, we have to retrieve those values and define how the properties of the items stored in the db should be mapped --> recreating our CartItem as it's stored in the db
));
//retrieve the values for each of the given properties and Map them according to the values stored on the server
});
_orders = orderProducts;
notifyListeners();
//set the value of the _items list - that is the primary data of the ProductsProvider to tell the different areas of the app the data to show - equal to the values retrieved from the API call
} catch (error) {
print(error);
throw (error);
}
}
}
Code with CircularProgressIndicator:
class OrdersScreen extends StatefulWidget {
static const routeName = '/orders';
#override
_OrdersScreenState createState() => _OrdersScreenState();
}
class _OrdersScreenState extends State<OrdersScreen> {
bool _isLoading = false;
#override
void initState() {
setState(() {
_isLoading = true;
});
// when the state of the screen is initialized set the value of _isLoading to true
// by setting _isLoading to true we are establishing another state while the API call is being made
Provider.of<Orders>(context, listen: false).getOrders().then((_) {
setState(() {
_isLoading = false;
});
});
// we are making the API call and then setting the state of _isLoading back to false indicating the change of the _isLoading variable means a completed API call
// --> by changing the value of _isLoading prior to and after the API call it allows us to put additional functionality while the API call is made --> we established a CircularProgressIndicator which may be found in the body
super.initState();
}
#override
Widget build(BuildContext context) {
final orderData = Provider.of<Orders>(context);
return Scaffold(
appBar: AppBar(
title: Text('Your Order'),
),
body: _isLoading == true
? Center(
child: CircularProgressIndicator(
backgroundColor: Theme.of(context).primaryColor),
)
: ListView.builder(
itemCount: orderData.orders.length,
itemBuilder: (ctx, index) => OrderCard(
order: orderData.orders[index],
),
//populate the order card UI element with data provided by the orders method within orders.dart
//this data is retrieved by calling the provider of type orders
),
drawer: SideDrawer(),
);
}
}
For reference:
OrderItem:
class OrderItem {
OrderItem({
#required this.id,
#required this.amount,
#required this.products,
#required this.dateTime,
});
final String id;
final double amount;
final List<CartItem> products; //CartItem from cart.dart
final DateTime dateTime;
}
CartItem:
class CartItem {
CartItem({
#required this.id,
#required this.title,
#required this.quantity,
#required this.price,
});
final String id;
final String title;
final int quantity;
final double price;
}

To fully take advantage of the Provider you already have setup, you should make the body of your scaffold a Consumer<Orders> widget. Keep the same logic inside, but it would need to be based on a bool (initialized to true) that lives within the Orders class.
Consumer<Orders>(builder: (context, orderData, child) {
return orderData.isLoading == true
? Center(
child: CircularProgressIndicator(
backgroundColor: Theme.of(context).primaryColor),
)
: ListView.builder(
itemCount: orderData.orders.length,
itemBuilder: (ctx, index) => OrderCard(
order: orderData.orders[index],
),
//populate the order card UI element with data provided by the orders method within orders.dart
//this data is retrieved by calling the provider of type orders
);
});
Handle the value of isLoading in your getOrders() function and that will notify the Consumer<Orders> widget to either return a CircularProgressIndicator or the ListView.builder once isLoading is updated to false.
You still call that function in initState but the local bool in that class would go away.

Related

Firebase: If I query a Firestore collection for a record and pass one column of data into a model, would my app do a second query for the next model?

I have a function called getNotifications that queries a collection in Firestore. I am running it on my Notifications screen.
On this screen, I want to optimize the number of Firestore querying to only query once. When the user gets to this screen, the app should query the data once, determine the notifID for the current index, then pass the initial data into the appropriate model. If the notifID == '1', then the initial data should be transformed via the GroupModel. If the notifID == '2', then transform via the FriendRequestModel. In doing all this, am I correct in assuming that Firestore will only query once, i.e. it will not re-query when passing the data through either the GroupModel or the FriendRequestModel? I'm worried because CommonModel only needs to read the notifID. I'm not even defining any other data fields in it, so I worry that this might signal to the Flutter framework that it needs to re-query.
notifications.dart
class ScreenNotifications extends StatefulWidget {
const ScreenNotifications({Key? key}) : super(key: key);
#override
State<ScreenNotifications> createState() => _ScreenNotificationsState();
}
class _ScreenNotificationsState extends State<ScreenNotifications> {
void initialize() async {
tempNotifsList = await database.getNotifications();
setState(() {
notifsList = tempNotifsList;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Notifications'),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: notifsList?.length ?? 0,
itemBuilder: (context, index) {
final notif = CommonModel.fromJson(data);
final notifID = notif.notifID;
if (notifID == '1') {
final group = GroupModel.fromJson(data);
}
if (notifID == '2') {
final friendRequest = FriendRequestModel.fromJson(data);
}
}
...//rest of code//
database.dart
Future<List> getNotifications() async {
final uid = getUID();
List notifsList = [];
FirebaseFirestore firestore = FirebaseFirestore.instance;
CollectionReference notifCollection = firestore.collection('notifications_' + uid);
final docsRef = await notifCollection.get();
docsRef.docs.forEach((element) {
Map<dynamic, dynamic> docMap = {'docID': element.id, 'data': element.data()};
notifsList.add(docMap);
});
return notifsList;
}
the best way to go about this is to the defined a notification type as part of fields while storing your notification,
"nofiType":....//here will be group of friends
so in your ListView.builder then you check if the notif.notiType is equl to the value show the widget

How to re-render a Widget based on another widget using riverpod in flutter?

I want to know how can I refresh a table data (which is fetched from an API using a future provider) and re-render the table widget based on dropdown value change.
Following is the Repo file with providers:
import 'package:ct_analyst_app/src/features/dashboard/domain/dashboard_data.dart';
import 'package:dio/dio.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../authentication/application/auth_local_service.dart';
abstract class IDashboardRepository {
Future<void> fetchDashboard(String name);
Future<void> fetchNames();
}
final clientProvider = Provider.family((ref, token) => Dio(BaseOptions(
baseUrl: "http://${dotenv.env['IP']}/excel/",
headers: {"authorization": token})));
class DashboardRepository implements IDashboardRepository {
DashboardRepository(this.read);
final Reader read;
DashboardData? _data;
DashboardData? get dashboardData => _data;
List<dynamic>? _names;
List<dynamic>? get names => _names;
#override
Future<DashboardData?> fetchDashboard(String name) async {
final token = await read(authServiceProvider).getToken();
final response = await read(clientProvider(token))
.get('/getData', queryParameters: {"name": name});
_data = DashboardData.fromJson(response.data);
print(name);
return _data;
}
#override
Future<void> fetchNames() async {
final token = await read(authServiceProvider).getToken();
final response = await read(clientProvider(token)).get('/analystNames');
_names = response.data["names"];
}
}
final dashboardRepositoryProvider =
Provider((ref) => DashboardRepository(ref.read));
final fetchDashboardData = FutureProvider.family<void, String>((ref, name) {
final repoProvider = ref.watch(dashboardRepositoryProvider);
return repoProvider.fetchDashboard(name);
});
final fetchAnalystNames = FutureProvider((ref) {
final repoProvider = ref.watch(dashboardRepositoryProvider);
return repoProvider.fetchNames();
});
I have tried to refresh the future provider in the dropdown onChange and it does fetch the new table data from the API. However, the widget which renders the data in the table is not getting re-rendered when the refresh is called.
Done as following:
onChanged: (String? newValue) {
ref.read(dropItemProvider.notifier).state = newValue as String;
ref.refresh(fetchDashboardData(newValue));
setState(() {
widget.value = newValue;
});
},
I am using ref.watch on the data, still it does not re-render the widget if the data is changed.
class TableGenerator extends ConsumerWidget {
const TableGenerator({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final data = ref.watch(dashboardRepositoryProvider);
return data.dashboardData != null
? SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Row(
children: [
const FixedColumnWidget(data: [
"one",
"two",
"three",
"four",
"fifth",
]),
ScrollableColumnWidget(
data: data.dashboardData as DashboardData)
],
))
: const CircularProgressIndicator();
}
}
Am I missing something or how should I approach this problem? like different providers or something else?
Thanks!
Your Widget is watching dashboardRepositoryProvider, which doesn't update after the ref.refresh call.
There's two things to consider:
dashboardRepository should just expose your repo / services, and instead it is used to observe actual data. This is not affecting your app directly, but it is part of the problem imho. I'd expect your Widget to observe a FutureProvider that exposes (and caches, etc.) the data by calling the methods inside your repository;
Then, let's analyze why your Widget isn't updating: dashboardRepository isn't depending, i.e. performing a watch, on the Provider you're refreshing, which is fetchDashboardData, nor it is depending on dropItemProvider (I am specifying this since your onChanged callback updates / refreshes two different Providers).
I think your should refactor your code so that it will expose actual data from a FutureProvider which exploits your repositories and can be simply refreshed similarly as what you already are doing.
Quick FutureProvider example:
// WARNING: PSEUDOCODE
final myDataProvider = FutureProvider<MyClass>((ref) {
final repo = ref.watch(myRepo);
final response = repo.getSomeData(...);
// TODO: add error handling, debouncing, cancel tokens, etc.
return MyClass.fromJson(response.data); // e.g.
});
Quick usage:
// WARNING: PSEUDOCODE
#override
Widget build(BuildContext context, WidgetRef ref) {
final myData = ref.watch(myDataProvider);
return ElevatedButton(
onTap: () {
ref.refresh(myDataProvider);
},
child: Text("Click me to refresh me (data: $myData)"),
);
}

User state managment with firebase and provider

My goal is to provide all the data of the logged in user throughout the app using provider and use it to customize the UI based on the user.
The problem is that the data takes time to arrive from Firestore and at the start of the application the provider is not able to provide the widgets of the home screen, so I get the next error:
Exception has occurred.
_CastError (Null check operator used on a null value)
When I click "continue" in debug options then the interface get the data from the user correctly and all works fine, so I understand that I need a way to wait the data to be available.
I know that the returns of type Future have methods to deal with the asynchronous response from Firestore, but in order to use provider I have "synchronized" that data using try{}catch(e){} as shown in the code.
class CurrentUserProvider extends ChangeNotifier {
UserModel _currentUser = UserModel();
UserModel get getCurrentUser => _currentUser;
void updateStateFromFirebase(String uid) async {
try {
_currentUser = await OurDatabase().getUserInfo(uid);
notifyListeners();
} catch (e) {
print(e);
}
}
}
getUserInfo(uid){} is the async function that download the current user data from firestore using the uid provided by Authentification Firebase after the user logged in.
To my knowledge this implies that _currentUser is not going to be async anymore, so I cannot create an alternative to represent something on the screen while the data is arriving.
This is the code where I receive the data from the provider and try to render _currentUser.uid as text on the screen.
import 'package:beeteam/providers/currentUserProvider.dart';
import 'package:flutter/material.dart';
import 'package:beeteam/models/user_model.dart';
import 'package:provider/provider.dart';
class MyTeams extends StatefulWidget {
#override
State<MyTeams> createState() => _MyTeamsState();
}
class _MyTeamsState extends State<MyTeams> {
#override
Widget build(BuildContext context) {
UserModel? _currentUser =
Provider.of<CurrentUserProvider>(context, listen: true).getCurrentUser;
return Scaffold(
body: Container(
margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 20),
child: Text(_currentUser.uid!),
//Text(_currentUser.uid!),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
backgroundColor: Color.fromARGB(255, 190, 158, 62),
onPressed: () {
Navigator.of(context).pushNamed('/join_team_screen');
}));
}
I got an error if I dont code ! at the end of _currentUser.uid.
This is the UserModel code.
class UserModel {
String? uid;
String? email;
String? firstName;
String? secondName;
String? userName;
String? teamSelected;
List<String>? teamsMemberUid;
List<String>? notifications;
UserModel(
{this.uid,
this.email,
this.firstName,
this.secondName,
this.userName,
this.teamSelected,
this.teamsMemberUid,
this.notifications});
Do you have any idea how to solve this problem?
Make currentUser nullable allowing null value before loading is finished.
class CurrentUserProvider extends ChangeNotifier {
UserModel? _currentUser;
UserModel? get currentUser => _currentUser;
void updateStateFromFirebase(String uid) async {
try {
_currentUser = await OurDatabase().getUserInfo(uid);
notifyListeners();
} catch (e) {
print(e);
}
}
}
Show loading state while currentUser is null.
class MyTeams extends StatelessWidget {
#override
Widget build(BuildContext context) {
UserModel? currentUser =
Provider.of<CurrentUserProvider>(context, listen: true).currentUser;
return Scaffold(
body: currentUser == null
? const Center(child: CircularProgressIndicator())
: Container(
margin:
const EdgeInsets.symmetric(horizontal: 10.0, vertical: 20),
child: Text(currentUser.uid!),
),
...
}
}

How to link up web api call to the list view

So i have my dart call to my api get method. Btw the way am just learning flutter and dart and trying out basic crud operations I would use to be doing in .net and c#
import 'dart:convert';
import 'package:theapp/models/Players.dart';
import 'package:http/http.dart';
class ApiService {
final String apiUrl = "https://apiurlhidden.com/api";
final String getAllPlayersEndPoint = "/GetAllPlayers/";
Future<List<Player>> getAllPlayers() async {
final getallPlayersUrl = Uri.parse(apiUrl + getAllPlayersEndPoint);
Response res = await get(getallPlayersUrl);
if (res.statusCode == 200) {
List<dynamic> body = jsonDecode(res.body);
List<Player> players =
body.map((dynamic item) => Player.fromJson(item)).toList();
return players;
} else {
throw "Failed to load cases list";
}
}
}
And I have my listview here but it complaining saying key and players do not exist
import 'package:flutter/material.dart';
import 'package:theapp/models/Players.dart';
class PlayerList extends StatelessWidget {
List<Player> players = [];
PlayerList({Key key, this.players}) : super(key: key);
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: players == null ? 0 : players.length,
itemBuilder: (BuildContext context, int index) {
return Card(
child: InkWell(
onTap: () {},
child: ListTile(
leading: Icon(Icons.person),
title: Text(players[index].firstName),
subtitle: Text(players[index].surname.toString()),
),
));
});
}
}
My Model
class Player {
final int id;
final int type;
final String playerLevel;
final String firstName;
final String surname;
Player(this.id, this.type, this.playerLevel, this.firstName, this.surname);
factory Player.fromJson(Map<String, dynamic> json) {
return Player(
json['id'],
json['type'],
json['playerlevel'],
json['firstname'],
json['surname'],
);
}
#override
String toString() =>
'Players{id: $id, firstName: $firstName, lastName: $surname}';
}
Is there any reason why it should not recognize players and key in my list view page also how do I get the items to appear in the listview.
Picture only added to show the context in the items I mentioned above. Also coming from a .net background I would normally use an observable collection so it gets any changes in data in real-time am I using the correct approach for that.
Use required keyword to make parameters mandatory.
PlayerList({required Key key, required this.players}) : super(key: key);
Named parameters are optional unless they’re explicitly marked as required.
See Parameters for details.

Provider in `didChangeDependencies` does not update data

I'm initializing the data in my provider in didChangeDependencies in the parent widget.
#override
void didChangeDependencies() {
super.didChangeDependencies();
final provider = Provider.of<NewArrivalsProvider>(context);
FirebaseFirestore.instance.collection(CurrentUser.getCurrentUser().uid).doc('newArrivals').get().then(
(snapshot) {
Map<String, dynamic> data = snapshot.data();
provider.init(data);
},
);
}
Then updating the data in the child widget.
The change in the data is does not persist however.
Switch(
value: item.value,
onChanged: (state) => provider.update(key: item.key, state: state),
)
Only one switch changes value at a time.
class NewArrivalsProvider extends ChangeNotifier {
Map<String, dynamic> _items = {};
Map<String, dynamic> get items => _items;
int get length => _items.length;
void init(Map<String, dynamic> data) {
_items = data['mapUrls'];
}
void update({#required String key, #required bool state}) {
_items.update(key, (value) => value = state);
notifyListeners();
}
}
Since this is the first I used Provider in the didChangeDependencies method, I'm suspecting that's where the error is. Am I correct?
I solved it.
I changed the provider declaration in the didChangeDependencies to
final provider = Provider.of<NewArrivalsProvider>(context, listen: false);
When listen: true, this line listens to the changes I make to the data.
Then it downloads the data again from Firestore (which is set to false initially)
The change to listen: false makes line run only when the user navigates to the page and does not update every time I change the data