72

My dashboard code looks like this, Here I am doing get req in getReport method, I have added the RefreshIndicator in the code which when pulled down inside container should do the refresh, there I am calling my getData(), But I am not getting the refreshed content, I am adding my code below, let me know if anywhere I made a mistake.

below my dashboard.dart

class Window extends StatefulWidget {
  @override
  _WindowState createState() => _WindowState();
}

class _WindowState extends State<Window> {
  Future reportList;    
  @override
  void initState() {
    super.initState();
    reportList = getReport();
  }    

  Future<void> getReport() async {
    http.Response response =
        await http.get(reportsListURL, headers: {"token": "$token"});
    switch (response.statusCode) {
      case 200: 
        String reportList = response.body;
        var collection = json.decode(reportList);
        return collection;

      case 403:
          break;

      case 401:
        return null;

      default:
        return 1;
    }
  }

  getRefreshScaffold() {
    return Center(
      child: RaisedButton(
        onPressed: () {
          setState(() {
            reportList = getReport();
          });
        },
        child: Text('Refresh, Network issues.'),
      ),
    );
  }

  getDashBody(var data) {
    double maxHeight = MediaQuery.of(context).size.height;
    return Column(
      children: <Widget>[
        Container(
          height: maxHeight - 800,
        ),
        Container(
          margin: new EdgeInsets.all(0.0),
          height: maxHeight - 188,
          child: new Center(
          child: new RefreshIndicator(          //here I am adding the RefreshIndicator
          onRefresh:getReport,                  //and calling the getReport() which hits the get api
          child: createList(context, data),
          ),),
        ),
      ],
    );
  }

  Widget createList(BuildContext context, var data) {
    Widget _listView = ListView.builder(
      itemCount: data.length,
      itemBuilder: (context, count) {
        return createData(context, count, data);
      },
    );
    return _listView;
  }

  createData(BuildContext context, int count, var data) {
    var metrics = data["statistic_cards"].map<Widget>((cardInfo) {
      var cardColor = getColorFromHexString(cardInfo["color"]);
      if (cardInfo["progress_bar"] != null && cardInfo["progress_bar"]) {
        return buildRadialProgressBar(
          context: context,
          progressPercent: cardInfo["percentage"],
          color: cardColor,
          count: cardInfo["value"],
          title: cardInfo["title"],
        );
      } else {
        return buildSubscriberTile(context, cardInfo, cardColor);
      }
    }).toList();

    var rowMetrics = new List<Widget>();
    for (int i = 0; i < metrics.length; i += 2) {
      if (i + 2 < metrics.length)
        rowMetrics.add(Row(children: metrics.sublist(i, i + 2)));
      else
        rowMetrics.add(Row(children: [metrics[metrics.length - 1], Spacer()]));
    }
    return SingleChildScrollView(
      child: LimitedBox(
        //  maxHeight: MediaQuery.of(context).size.height / 1.30,
        child: Column(
          mainAxisSize: MainAxisSize.max,
          mainAxisAlignment: MainAxisAlignment.center,
          children: rowMetrics,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: reportList,
      builder: (BuildContext context, AsyncSnapshot snapshot) {
        switch (snapshot.connectionState) {
          case ConnectionState.none:
          case ConnectionState.waiting:
          case ConnectionState.active:
            return Center(
              child: CircularProgressIndicator(),
            );
          case ConnectionState.done:
            var data = snapshot.data;
            if (snapshot.hasData && !snapshot.hasError) {
              return getDashBody(data);
            } else if (data == null) {
              return Center(
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: <Widget>[
                    Text("Timeout! Log back in to continue"),
                    Padding(
                      padding: EdgeInsets.all(25.0),
                    ),
                    RaisedButton(
                      onPressed: () {
                        setState(() {
                          token = null;
                        });
                        Navigator.of(context).pushReplacement(
                          CupertinoPageRoute(
                              builder: (BuildContext context) => LoginPage()),
                        );
                      },
                      child: Text('Login Again!'),
                    ),
                  ],
                ),
              );
            } else {
              getRefreshScaffold();
            }
        }
      },
    );
  }
}
Aman Gupta
  • 156
  • 2
  • 16
mark
  • 1,045
  • 2
  • 13
  • 23

6 Answers6

118

Basic Example

Below is a State class of a StatefulWidget, where:

  • a ListView is wrapped in a RefreshIndicator
  • numbersList state variable is its data source
  • onRefresh calls _pullRefresh function to update data & ListView
  • _pullRefresh is an async function, returning nothing (a Future<void>)
  • when _pullRefresh's long running data request completes, numbersList member/state variable is updated in a setState() call to rebuild ListView to display new data
import 'package:flutter/material.dart';
import 'dart:math';

class PullRefreshPage extends StatefulWidget {
  const PullRefreshPage();

  @override
  State<PullRefreshPage> createState() => _PullRefreshPageState();
}

class _PullRefreshPageState extends State<PullRefreshPage> {
  List<String> numbersList = NumberGenerator().numbers;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: RefreshIndicator(
        onRefresh: _pullRefresh,
        child: ListView.builder(
          itemCount: numbersList.length,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text(numbersList[index]),
            );
          },),
      ),
    );
  }

  Future<void> _pullRefresh() async {
    List<String> freshNumbers = await NumberGenerator().slowNumbers();
    setState(() {
      numbersList = freshNumbers;
    });
    // why use freshNumbers var? https://stackoverflow.com/a/52992836/2301224
  }
}

class NumberGenerator {
  Future<List<String>> slowNumbers() async {
    return Future.delayed(const Duration(milliseconds: 1000), () => numbers,);
  }

  List<String> get numbers => List.generate(5, (index) => number);


  String get number => Random().nextInt(99999).toString();
}

Notes

  • If your async onRefresh function completes very quickly, you may want to add an await Future.delayed(Duration(seconds: 2)); after it, just so the UX is more pleasant.
  • This gives time for the user to complete a swipe / pull down gesture & for the refresh indicator to render / animate / spin indicating data has been fetched.

FutureBuilder Example

Here's another version of the above State<PullRefreshPage> class using a FutureBuilder, which is common when fetching data from a Database or HTTP source:

class _PullRefreshPageState extends State<PullRefreshPage> {
  late Future<List<String>> futureNumbersList;

  @override
  void initState() {
    super.initState();
    futureNumbersList = NumberGenerator().slowNumbers();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FutureBuilder<List<String>>(
        future: futureNumbersList,
        builder: (context, snapshot) {
          return RefreshIndicator(
            child: _listView(snapshot),
            onRefresh: _pullRefresh,
          );
        },
      ),
    );
  }

  Widget _listView(AsyncSnapshot snapshot) {
    if (snapshot.hasData) {
      return ListView.builder(
        itemCount: snapshot.data.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(snapshot.data[index]),
          );
        },);
    }
    else {
      return Center(
        child: Text('Loading data...'),
      );
    }
  }

  Future<void> _pullRefresh() async {
    List<String> freshNumbers = await NumberGenerator().slowNumbers();
    setState(() {
      futureNumbersList = Future.value(freshNumbers);
    });
  }
}

Notes

  • slowNumbers() function is the same as in the Basic Example above, but the data is wrapped in a Future.value() since FutureBuilder expects a Future, but setState() should not await async data
  • according to Rémi, Collin & other Dart/Flutter demigods it's good practice to update Stateful Widget member variables inside setState() (futureNumbersList in FutureBuilder example & numbersList in Basic example), after its long running async data fetch functions have completed.
  • if you try to make setState async, you'll get an exception
  • updating member variables outside of setState and having an empty setState closure, may result in hand-slapping / code analysis warnings in the future
Baker
  • 24,730
  • 11
  • 100
  • 106
15

Not sure about futures, but for refresh indicator you must return a void so Use something like

RefreshIndicator(
                onRefresh: () async  {
                  await getData().then((lA) {
                    if (lA is Future) {
                      setState(() {
                        reportList = lA;
                      });
                      return;
                    } else {
                      setState(() {
                       //error
                      });
                      return;
                    }
                  });

                  return;
                },

Try this and let me know!

EDIT:

Well, then just try this inside you refresh method

          setState(() {
            reportList = getReport();  
          });
          return reportList;
Cristian Bregant
  • 1,797
  • 1
  • 16
  • 22
  • if you need the future, maybe just remove the await async params I wrote. – Cristian Bregant Sep 17 '19 at 11:06
  • onRefresh: () async { await getReport().then((lA) { if (lA is Future) { setState(() { reportList = lA; }); return; } else { setState(() { //error }); return; } }); return; }, child: createDashList(context, data), ),), ) , – mark Sep 17 '19 at 11:12
  • I have written this inside the getDashBody but it didnt worked for me – mark Sep 17 '19 at 11:13
4

Easy method: you can just use Pull Down to Refresh Package - https://pub.dev/packages/pull_to_refresh

Mano Haran
  • 634
  • 1
  • 6
  • 19
3

Try this:

onRefresh: () {
  setState(() {});
}}

instead of onRefresh:getReport

reportList field is Future which returns its value once. So, when you call getReport again it changes nothing. Actually, more correctly it'll be with Stream and StreamBuilder instead of Future and FutureBuilder. But for this code it can be shortest solution

Andrii Turkovskyi
  • 27,554
  • 16
  • 95
  • 105
  • when changed I am getting this "Function expressions can't be named. Try removing the name, or moving the function expression to a function declaration statement." – mark Sep 17 '19 at 10:58
  • initially it was just getReport() , when I added the RefreshIndicator I get error saying that method should be future thing, so I renamed gerReport as => Future getReport – mark Sep 17 '19 at 11:01
  • Sorry. Edited my answer – Andrii Turkovskyi Sep 17 '19 at 11:09
2

In Non-scrollable list view, RefreshIndicator does not work, so you have to wrap your widget with Stack for implementing pull down to refresh.

RefreshIndicator(
  onRefresh: () {
  // Refresh Functionality 
  },
  child: Stack(
            children: [
              ListView(
                padding: EdgeInsets.zero,
                shrinkWrap: true,
                children: [
                  SizedBox(
                    height: MediaQuery.of(context).size.height,
                  )
                ],
              ),
              // Your Widget 
            ],
          );
        ),
Tarun Sharma
  • 914
  • 9
  • 15
0

I am working on a huge project which contains CustomScrollView, NestedScrollView, ListView, etc I tried every answer above and all of the answers use RefreshIndicator from flutter SDK. It doesn't work entirely with my app because I also have horizontal scroll views. So in order to implement it I had to use NestedScrollView on almost every screen. Then I came to know about liquid_pull_to_refresh, applied it to the top widget, and WOLAAH! If you need a separate logic for each screen then use it at the top of each screen but in my case, I'm refreshing the whole project's data.

Faizan Ahmad
  • 274
  • 3
  • 7