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;
}


Solution 1: Loren.A

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.