ZP L
ZP L

Reputation: 31

how to build dynamic list from http server in flutter?

Flutter newbie, learned a lot from here.

In a common case, people will request resource from web servers and server return an array of objects:

[ { "key": "value"}, {"key": "value}...]

We can easily handle this use FutureBuilder.

But I have a server with massive data which I have to get resource in this way:

  1. Query records count with an api, say "/resources/counts"
  2. Query a few records with an api , say "/resource?offset=101&limit=20" to get 20 records.
  3. When user scroll down the menu will trigger "/resource?offset=121&limit=20" to get another 20 records.

So I have a list with fix count some kind, but the resource have to dynamic loaded from server. how to do that?

Some code.

 @override
  Widget build(BuildContext context) {
    // _max == -1, request in progress.
    return _max == -1
        ? new CircularProgressIndicator()
        : ListView.builder(
            padding: const EdgeInsets.all(16.0),
            itemBuilder: (context, i) {
              return new FutureBuilder(
                future: _getFollowingContracts(),
                builder: (context, snapshot) {
                  if (i.isOdd) return new Divider();
                  switch (snapshot.connectionState) {
                    case ConnectionState.none:
                    case ConnectionState.waiting:
                    case ConnectionState.active:
                      return new Text('loading...');
                    case ConnectionState.done:
                      if (i.isOdd)
                        return new Divider(
                          height: 2.0,
                        );
                      final int index = i ~/ 2;
                      // when user scroll down here we got exception
                      // because only 20 records is available. 
                      // how to get another 20 records?
                      return _buildRow(_contracts[index]);
                  }
                },
              );
            },
            itemCount: _max * 2,
          );
  }

Upvotes: 3

Views: 4825

Answers (2)

R. C. Howell
R. C. Howell

Reputation: 1081

What you're looking for is "infinite-scrolling". There are Flutter tutorials out there which explain this in detail, and here a common pattern that I've found works well.

import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';


class Feed extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => new _FeedState();
}

class _FeedState extends State<Feed> {
  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
  final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
      new GlobalKey<RefreshIndicatorState>();

  bool _initialized;
  bool _readyToFetchNextPage;
  ScrollController _scrollController;
  Repository _repository;

  int _page;
  Map<int, Widget> _cache;

  @override
  void initState() {
    _initialized = false;
    _repository = Repository();
    _readyToFetchNextPage = true;
    _initialize();
    super.initState();
  }


  Future<Null> _initialize() async {
    _initialized = false;
    _readyToFetchNextPage = false;
    final Completer<Null> completer = new Completer<Null>();
    // Reset cache
    _cache = new Map();
    // Reset page
    _page = 1;
    // Fetch initial data
    List results = repository.getPage(_page);
    // Add results to the cache
    _update(_cache.length, results);
    completer.complete();
    // Let the refresh indicator know the method has completed
    _readyToFetchNextPage = true;
    // SetState must be called to dismiss the CircularProgressIndicator
    setState(() { _initialized = true; });
    return completer.future;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      body: new RefreshIndicator(
        key: _refreshIndicatorKey,
        onRefresh: _initialize,
        child: CustomScrollView(
          slivers: <Widget>[
            AppBar(),
            (_initialized) ? new SliverList(
              delegate: new SliverChildBuilderDelegate(
                  (BuildContext context, int index) {
                return _getItem(index);
              }),
            ) : new SliverFillRemaining(
              child: new Center(
                child: new CircularProgressIndicator(),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _getItem(int index) {
    if (_cache.containsKey(index)) {
      return _cache[index];
    } else {
      if (_readyToFetchNextPage) {
        _readyToFetchNextPage = false;
        _page += 1;
        _repository.getPage(_page).then((results) {
          setState(() {
            _update(_cache.length, results);
          });
        });
      }
      return null;
    }
  }

  void _update(int offset, List results) {
    for (int i = 0; i < results.length; i += 1) {
      int index = i + offset;
      _cache.putIfAbsent(
          index,
          () => new ListTile(
                key: new Key('post-$index'),
                title: Text(results[i]),
              ));
    }
    _readyToFetchNextPage = true;
  }
}

This works as follows.

  1. You have a Repository which makes your API calls given a page number

    Ex: repository.getPage(1) => GET http://api.com?page=1 => List

  2. You collect the results and run them through _update
  3. _update adds all of these posts to a cache (a Map).
  4. The ScrollView takes a function called getItem(int index) which must return a Widget for the given index.
  5. My getItem function looks for the item in the cache, and returns it if it's found.
  6. If it's not found, I tell the Repository to get the next page and update the cache and so on...

You should certainly consider using this pattern as it will allow to you to fetch new pages on demand.

IMPORTANT This is not a direct implementation. The Repository that I'm "using" in this code sample is meant to show you where you would make your API calls. It is not an existing class, rather an abstract representation of a repository that you would implement. Furthermore, you will specify the Widget you would like to appear in the list when you call _cache.putIfAbsent(index, () => MyWidget())

Upvotes: 1

Romain Rastel
Romain Rastel

Reputation: 5632

To implement this, you can attach a ScrollController to your ListView and listen its changes. In this listener when you are in the end of the scroll you fetch more data and update your app state.

Example:

The following code is extracted from this well explained blog post: https://marcinszalek.pl/flutter/infinite-dynamic-listview/.

import 'dart:async';

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      theme: new ThemeData(primarySwatch: Colors.blue),
      home: new MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<int> items = List.generate(10, (i) => i);
  ScrollController _scrollController = new ScrollController();
  bool isPerformingRequest = false;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels ==
          _scrollController.position.maxScrollExtent) {
        _getMoreData();
      }
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  _getMoreData() async {
    if (!isPerformingRequest) {
      setState(() => isPerformingRequest = true);
      List<int> newEntries = await fakeRequest(
          items.length, items.length + 10); //returns empty list
      if (newEntries.isEmpty) {
        double edge = 50.0;
        double offsetFromBottom = _scrollController.position.maxScrollExtent -
            _scrollController.position.pixels;
        if (offsetFromBottom < edge) {
          _scrollController.animateTo(
              _scrollController.offset - (edge - offsetFromBottom),
              duration: new Duration(milliseconds: 500),
              curve: Curves.easeOut);
        }
      }
      setState(() {
        items.addAll(newEntries);
        isPerformingRequest = false;
      });
    }
  }

  Widget _buildProgressIndicator() {
    return new Padding(
      padding: const EdgeInsets.all(8.0),
      child: new Center(
        child: new Opacity(
          opacity: isPerformingRequest ? 1.0 : 0.0,
          child: new CircularProgressIndicator(),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: AppBar(
        title: Text("Infinite ListView"),
      ),
      body: ListView.builder(
        itemCount: items.length + 1,
        itemBuilder: (context, index) {
          if (index == items.length) {
            return _buildProgressIndicator();
          } else {
            return ListTile(title: new Text("Number $index"));
          }
        },
        controller: _scrollController,
      ),
    );
  }
}

/// from - inclusive, to - exclusive
Future<List<int>> fakeRequest(int from, int to) async {
  return Future.delayed(Duration(seconds: 2), () {
    return List.generate(to - from, (i) => i + from);
  });
}

You will have to adapt this code to your scenario (mainly by changing the fakeRequest method and how the tiles are rendered), but I think it will give you the main idea.

Upvotes: 3

Related Questions