Search code examples
flutterlistviewstoragepull-to-refresh

Pull-to-refresh in Flutter don't work when no internet connection


I have a class that displays in a list view some JSON data (events) that I get with an API request and save them to the storage of the device so to not be downloaded every time, UNLESS the user makes a Pull-to-refresh operation so to download news events. In case during the operation of download there is no internet connection the app display "Impossible to download the events list: check your internet connection!". So I aspect that if it is the first time the user opens the app, it should download the events or show in case of internet connection missing the message mentioned above (or that there are no events in case the length of the events array downloaded == 0). If it is not the first time show the list of the events previously downloaded and saved.

My problem is that if, for example, I have internet turned off and after I turned on, the pull to refresh doesn't work, instead when I have the list downloaded I can make a pull to refresh operation.

This is my code:

class EventDetails {
  String data;
  int id;
  String name;
  String description;
  String applicationStatus;
  String applicationStarts;
  String applicationEnd;
  String starts;
  String ends;
  int fee;

  EventDetails({
    this.data,
    this.id,
    this.name,
    this.description,
    this.applicationStatus,
    this.applicationStarts,
    this.applicationEnd,
    this.starts,
    this.ends,
    this.fee,
  });

  EventDetails.fromJson(Map<String, dynamic> json) {
    data = json['data'];
    id = json['id'];
    name = json['name'];
    description = json['description'];
    applicationStatus = json['application_status'];
    applicationStarts = json['application_starts'];
    applicationEnd = json['application_ends'];
    starts = json['starts'];
    ends = json['ends'];
    fee = json['fee'];
  }
}

class EventsListView extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _EventListState();
  }
}


class _EventListState extends State<EventsListView> {

  List<EventDetails> list;
  Storage storage = Storage();

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: _getData,
      child: FutureBuilder<List<EventDetails>>(
        future: loadEvents(),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            List<EventDetails> data = snapshot.data;
            if (data.length == 0) {
              return Text("No events found",
                  style: TextStyle(
                    fontWeight: FontWeight.w500,
                    fontSize: 20,
                  ));
            } else {
              return _eventsListView(data);
            }
          } else if (snapshot.hasError) {
            if (snapshot.error.runtimeType.toString() == "SocketException") {
              return Text(
                  "Impossible to download the events list: check your internet connection!",
                  style: TextStyle(
                    fontWeight: FontWeight.w500,
                    fontSize: 20,
                  ));
            } else {
              return Text("${snapshot.error}");
            }
          }
          return CircularProgressIndicator();
        },
      ),
    );
  }

  Future<List<EventDetails>> loadEvents() async {

    String content = await storage.readList();

     if (content != 'no file available') {
      list = getListFromData(content, list);
    }

    if ((list != null) && (list.length != 0)) {
      print('not empty');
      return list;
    } else {
    return await downloadEvents(list, storage);
    }
  }

  Future<List<EventDetails>> downloadEvents(
      List<EventDetails> list, Storage storage) async {
 
    String url = "https://myurl";
    final response = await http.get(url);
    if (response.statusCode == 200) {
      String responseResult = response.body;
      list = getListFromData(responseResult, list);
      storage.writeList(response.body);
      return list;
    } else {
      throw Exception('Failed to load events from API');
    }
  }

  List<EventDetails> getListFromData(String response, List<EventDetails> list) {
    Map<String, dynamic> map = json.decode(response);
    List<dynamic> jsonResponse = map["data"];
    list = jsonResponse.map((job) => new EventDetails.fromJson(job)).toList();
    return list;
  }

  ListView _eventsListView(data) {
    return ListView.separated(
        itemCount: data.length,
        separatorBuilder: (context, index) => Divider(
          color: const Color(0xFFCCCCCC),
        ),
        itemBuilder: (BuildContext context, int index) {
          return GestureDetector(
              child: _tile(data[index].name),
              onTap: () {
                Navigator.pushNamed(
                  context,
                  SingleEvent.routeName,
                  arguments: ScreenArguments(
                    data[index].name,
                    data[index].description,
                    data[index].starts,
                    data[index].ends,
                  ),
                );
              });
        });
  }

  Future<void> _getData() async {
    setState(() {
      downloadEvents(list,storage);
    });
  }

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

  ListTile _tile(String title) => ListTile(
    title: Text(title,
        style: TextStyle(
          fontWeight: FontWeight.w500,
          fontSize: 20,
        )),
  );
}

I am really new in Flutter, what I am doing wrong?


Solution

  • FutureBuilder will not refresh once the future is evaluated. If you want the pull to refresh to work, you could just store the list data as a state of the widget and render different UI based on the state.

    In addition to that, RefreshIndicator will not work if the child is not scrollable. Instead returning plain Text widget when there is no data, return SingleChildScrollView with a text inside so that you have a scrollable inside your RefreshIndicator.

    Here is an example:

    class EventsListView extends StatefulWidget {
      @override
      _EventsListViewState createState() => _EventsListViewState();
    }
    
    class _EventsListViewState extends State<EventsListView> {
      List list;
      Storage storage = Storage();
      String errorMessage;
    
      @override
      Widget build(BuildContext context) {
        return RefreshIndicator(
          onRefresh: downloadEvents,
          child: listWidget(),
        );
      }
    
      Widget listWidget() {
        if (list != null) {
          return ListView(); // here you would return the list view with contents in it
        } else {
          return SingleChildScrollView(child: Text('noData'));  // You need to return a scrollable widget for the refresh to work. 
        }
      }
    
      Future<void> loadEvents() async {
        String content = await storage.readList();
    
        if (content != 'no file available') {
          list = getListFromData(content, list);
        }
    
        if ((list != null) && (list.length != 0)) {
          print('not empty');
          errorMessage = null;
          setState(() {});
        } else {
          await downloadEvents();
        }
      }
    
      Future<void> downloadEvents() async {
        String url = "https://myurl";
        final response = await http.get(url);
        if (response.statusCode == 200) {
          String responseResult = response.body;
          list = getListFromData(responseResult, list);
          storage.writeList(response.body);
          errorMessage = null;
          setState(() {});
        } else {
          setState(() {
            errorMessage =
                'Error occured'; // here, you would actually add more if, else statements to show better error message
          });
          throw Exception('Failed to load events from API');
        }
      }
    
      List<EventDetails> getListFromData(String response, List<EventDetails> list) {
        Map<String, dynamic> map = json.decode(response);
        List<dynamic> jsonResponse = map["data"];
        list = jsonResponse.map((job) => new EventDetails.fromJson(job)).toList();
        return list;
      }
    
      @override
      void initState() {
        super.initState();
        loadEvents();
      }
    }