Passing data to another screen with Flutter Provider

Issue

I’m trying to pass the data to another screen using Provider, but it seems I’m always passing on the same data unless I sort the List and then pass the different data (meaning I’m probably switching the index by sorting the list so that is why it’s passing different data now). In short, I call the API, populate the list, setting up the provider too for the next page, and on click I list out the the information from the previous screen, but the problem is I display the same item always unless I sort the list. Here is the code:

Calling the API and displaying the list:

 var posts = <RideData>[];
  var streamController = StreamController<List<RideData>>();

  @override
  void initState() {
    _getRideStreamList();
    super.initState();
  }

  _getRideStreamList() async {
    await Future.delayed(Duration(seconds: 3));
    var _vehicleStreamData = await APICalls.instance.getRides();

    var provider = Provider.of<RideStore>(context, listen: false);

    posts = await _vehicleStreamData
        .map<RideData>((e) => RideData.fromJson(e))
        .toList();

    streamController.add(posts);
    provider.setRideList(posts, notify: false);
  }

  bool isSwitched = true;
  void toggleSwitch(bool value) {
    if (isSwitched == false) {
      posts.sort((k1, k2) => k1.rideId.compareTo(k2.rideId));
    } else {
      posts.sort((k1, k2) => k2.rideId.compareTo(k1.rideId));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: SingleChildScrollView(
          scrollDirection: Axis.vertical,
          child: Column(
            children: [
              TextButton(
                  child: Text('sort ascending'),
                  onPressed: () {
                    setState(() {
                      toggleSwitch(isSwitched = !isSwitched);
                    });
                  }),
              Container(
                height: 1000,
                child: StreamBuilder<List<RideData>>(
                    initialData: posts,
                    stream: streamController.stream,
                    builder: (context, snapshot) {
                      return ListView.builder(
                        shrinkWrap: true,
                        itemCount: snapshot.data.length,
                        itemBuilder: (context, index) {
                          return Column(
                            children: [
                              Row(
                                children: [
                                  Padding(
                                    padding: const EdgeInsets.only(left: 15.0),
                                    child: Text(
                                      'Ride #${snapshot.data[index].rideId}',
                                    ),
                                  ),
                                  FlatButton(
                                    textColor: Colors.blue[700],
                                    minWidth: 0,
                                    child: Text('View'),
                                    onPressed: () {
// here is where I pass the data to the RideInfo screen
                                      Navigator.push(
                                          context,
                                          MaterialPageRoute(
                                              builder: (context) => RideInfo(
                                                    rideId: snapshot
                                                        .data[index].rideId,
                                                  )));
                                    },
                                  ),
                                ],
                              ),
                              Column(
                                crossAxisAlignment: CrossAxisAlignment.stretch,
                                children: [
                                  Text(
                                    '${snapshot.data[index].pickupTime}',
                                  ),
                                  Text(
                                    '${snapshot.data[index].jobArrived}',
                                  ),
                                ],
                              ),
                            ],
                          );
                        },
                      );
                    }),
              ),
            ],
          ),
        ),
      ),
    );
  }

After pressing the View button and passing the data to another screen (RideInfo):

class RideInfo extends StatefulWidget {
  static const String id = 'ride_info_screen';
  String rideId;
  RideInfo({@required this.rideId});

  @override
  _RideInfoState createState() => _RideInfoState();
}

class _RideInfoState extends State<RideInfo> {
  String rideID = '';

  @override
  void initState() {
    super.initState();
    setState(() {
      rideID = widget.rideId;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.white,
        title: Text(
          'Ride #$rideID',
          ),
        ),
      body: SafeArea(
        child: SingleChildScrollView(
          child: Consumer<RideStore>(
            builder: (context, rideStore, child) {
              return Column(
                children: [
                  ListView.builder(
                      shrinkWrap: true,
                      itemCount: 1,
                      itemBuilder: (context, index) {
                        RideData rides = rideStore.getRideByIndex(index);
                        return Column(
                          children: [
                            Expanded(
                              flex: 2,
                              child: Column(
                                children: [
                                  Text(
                                    "PICK UP",
                                  ),
// here I display the pickUpTime but it is always the same and I wanted to display the time based on the ID
                                  Text(
                                    '${rides.pickupTime}AM',
                                  ),
                                ],
                              ),
                            ),
                          ],
                        );
                      }),
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}

The data (pickUpTime in this case) doesn’t change when I press to see the View of a single item, but like I said, when I change the order of the list with the sort method, then I get the different data.
Here is the Provider model:

class RideStore extends ChangeNotifier {
  List<RideData> _rideList = [];

  List<RideData> get rideList => _rideList;

  setRideList(List<RideData> list, {bool notify = true}) {
    _rideList = list;
    if (notify) notifyListeners();
  }

  RideData getRideByIndex(int index) => _rideList[index];

  int get rideListLength => _rideList.length;
}

How do I display the correct information based on the ID from the List that I pressed and passed in the Ride Info screen so it doesn’t give back always the same data? Thanks in advance for the help!

Solution

The offending code is in RideInfo:

  ListView.builder(
      shrinkWrap: true,
      itemCount: 1,
      itemBuilder: (context, index) {
        RideData rides = rideStore.getRideByIndex(index);

The index is always 1, so you are always showing the first RideData. There are various options to fix it, e.g. pass the index, or even pass the RideData, to the RideInfo constructor:

class RideInfo extends StatefulWidget {
  static const String id = 'ride_info_screen';
  String rideId;
  final int index;
  RideInfo({@required this.rideId, @required this.index, Key key})
      : super(key: key) {

and:

        RideData rides = rideStore.getRideByIndex(widget.index);

I have some additional comments on the code. Firstly, the ListView is serving no purpose in RideInfo, so remove it.

Secondly, there is no need to construct the streamController and to use StreamBuilder in the parent form. Your list is available in the RideStore. So your parent form could have:

  Widget build(BuildContext context) {
    var data = Provider.of<RideStore>(context).rideList;
    ...
          Container(
            height: 1000,
            child:
                // StreamBuilder<List<RideData>>(
                //     initialData: posts,
                //     stream: streamController.stream,
                //     builder: (context, snapshot) {
                //       return
                ListView.builder(
                  shrinkWrap: true,
                  itemCount: data.length,

I hope these comments help.

Edit:

It is simple to edit your code to use FutureBuilder. Firstly, make _getRideStreamList return the data it read:

  _getRideStreamList() async {
    ...
    return posts;
  }

Remove the call to _getRideStreamList in initState and wrap the ListView in the FutureBuilder that invokes _getRideStreamList:

          Container(
            height: 1000,
            child: FutureBuilder(
              future: _getRideStreamList(),
              builder: (ctx, snapshot) {
                if (!snapshot.hasData) {
                  return Center(child: CircularProgressIndicator());
                } else {
                  var data = snapshot.data;
                  return ListView.builder(
                    ...
                  );
                }
              },
            ),
          ),

This displays the CircularProgressIndicator while waiting for the data.

Note that this is a quick hack – you do not want to read the data everytime that the widget rebuilds. So _getRideStreamList could check if the data has already been read and just return it rather than rereading.

Answered By – Patrick O'Hara

Answer Checked By – David Marino (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published.