Flutter FutureBuilder将项目追加到列表中

时间:2019-01-13 16:47:16

标签: flutter

当我向下滚动时到达屏幕末端附近时,我尝试更新Thread对象的列表(在我不断向下滚动时显示无限的项目列表)

我当前的设置如下:

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart';

import 'dart:async';
import 'dart:convert';

import 'forums.dart';

// Retrieve JSON response forum thread list
Future<List<Thread>> fetchForumThreadList(String url, int page) async {
    final response =
        await http.get('http://10.0.2.2:8080/frest$url/page/$page');
    if (response == null) {
        throw new Exception("No site");
    }
    if (response.statusCode == 200) {
        return compute(parseForumThreadList, response.body);
    } else {
        List<Thread> e = [];
        return e;
    }
}

List<Thread> parseForumThreadList(String responseBody) {
    Map decoded = json.decode(responseBody);
    List<Thread> threads = [];
    Map threadList = decoded["list"];
    for (var thread in threadList["List"]) {
        threads.add(Thread(
            thread["ID"],
            thread["Staff"],    
            thread["Support"],
            thread["Sticky"],
            thread["Locked"],
            thread["Title"],
            thread["Replies"],
            thread["Views"],
            thread["Author"],
            thread["CreatedAt"],
        ));
    }
    return threads;
}

// Generate a card list from a List of forum threads
Widget generateForumThreadList(BuildContext context, int index, List<Thread> data) {
    // Use custom icon for staff posts
    IconData authorIcon = Icons.account_circle;
    Color authorIconColor = Color(0xAAFFFFFF);
    if (data[index].staff) {
        authorIcon = Icons.verified_user;
        authorIconColor = Color(0xAAFFBA08);
    }
    return Column(
        children: <Widget>[
            InkWell(
                onTap: () { 

                },
                child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: <Widget>[
                        // We need to wrap columns under Flexible
                        // To make text wrap if larger than screen width
                        Flexible(
                            child: Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: <Widget>[
                                    Padding(
                                        padding: const EdgeInsets.fromLTRB(12.0, 12.0, 12.0, 6.0),
                                        child: Text(
                                            data[index].title,
                                            style: TextStyle(
                                                fontSize: 22.0,
                                                fontWeight: FontWeight.bold,
                                            ),
                                        ),
                                    ),
                                    Padding(
                                        padding: const EdgeInsets.fromLTRB(12.0, 1.0, 12.0, 2.0),
                                        // Add thread author and created date
                                        child: Row(
                                            children: <Widget>[
                                                Icon(
                                                    authorIcon,
                                                    size: 15.0,
                                                    color: authorIconColor,
                                                ),
                                                Padding(
                                                    padding: const EdgeInsets.fromLTRB(5.0, 12.0, 12.0, 12.0),
                                                    child: Text(
                                                        "Author: " + data[index].author,
                                                    ),
                                                ),
                                            ],
                                        ),
                                    ),
                                    Padding(
                                        padding: const EdgeInsets.fromLTRB(12.0, 1.0, 12.0, 12.0),
                                        // Add threads and posts information
                                        child: Row(
                                            children: <Widget>[
                                                Icon(
                                                    Icons.chat_bubble,
                                                    size: 15.0,
                                                    color: Color(0xAAFFFFFF),
                                                ),
                                                Padding(
                                                    padding: const EdgeInsets.fromLTRB(5.0, 12.0, 12.0, 12.0),
                                                    child: Text(
                                                        data[index].replies.toString() + " Replies",
                                                    ),
                                                ),
                                                Icon(
                                                    Icons.pageview,
                                                    size: 15.0,
                                                    color: Color(0xAAFFFFFF),
                                                ),
                                                Padding(
                                                    padding: const EdgeInsets.fromLTRB(5.0, 12.0, 12.0, 12.0),
                                                    child: Text(
                                                        data[index].views.toString() + " Views",
                                                    ),
                                                ),
                                            ],  
                                        ),
                                    ),
                                ],
                            ),
                        ),
                    ],
                ),
            ),
            // Add a divider for each forum item
            Divider(
                height: 4.0,
            ),
        ],
    );  
}

// Class used for a forum thread
class Thread {
    final int id;
    final bool staff;
    final bool support;
    final bool sticky;
    final bool locked;
    final String title;
    final int replies;
    final int views;
    final String author;
    final String createdAt; 

    Thread(
        this.id,
        this.staff,
        this.support,
        this.sticky,
        this.locked,
        this.title,
        this.replies,
        this.views,
        this.author,
        this.createdAt,
    );
}

class ForumThreadList extends StatefulWidget {
    final Forum forum;
    ForumThreadList(this.forum);

    @override
    _ForumThreadListState createState() => _ForumThreadListState(forum);
}

class _ForumThreadListState extends State<ForumThreadList> {
    final Forum forum;
    int page = 1;
    ScrollController controller;    

    _ForumThreadListState(this.forum);

    @override
    void initState() {
        controller = new ScrollController();
        controller.addListener(_scrollListener);
        super.initState();
    }

    @override
    void dispose() {
        controller.removeListener(_scrollListener);
        super.dispose();
    }

    VoidCallback _scrollListener() {
        // If we are near the end of the list
        if (controller.position.extentAfter < 300) {
            page++;
            print(page);
        }
    }

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: Text(forum.name),
            ),
            body: Scrollbar(
                child: Center(
                    child: FutureBuilder<List<Thread>>(
                        future: fetchForumThreadList(forum.url, page),
                        builder: (context, snapshot) {
                            if (snapshot.hasData) {
                                return new ListView.builder(
                                    controller: controller,
                                    itemCount: snapshot.data.length,
                                    itemBuilder: (BuildContext ctx, int index) {
                                        return generateForumThreadList(ctx, index, snapshot.data);
                                    },
                                );
                            } else if (snapshot.hasError) {
                                return Text(snapshot.error);
                            }   
                            return CircularProgressIndicator();
                        },
                    ),
                ),
            ),
            // Add floating button to reload forums
            floatingActionButton: new FloatingActionButton(
                elevation: 0.0,
                child: Icon(
                    Icons.sync,
                    size: 32.0,
                ),
                // When pressing the button reload the forum list
                onPressed: () {  },
            ),
        );
    }
}

现在一切正常,当我加载应用程序时,获取了我的API的第一页并填充了列表,但是当我接近屏幕末尾时,我无法确定如何追加元素的下一页。

我在接近尾声时尝试更新page变量,以为这将使FutureBuilder更新,但这似乎并不正确,而且我也认为这不会给我我想要的结果(使列表扩大,而不是用新批次替换项目。

1 个答案:

答案 0 :(得分:2)

您正在寻找的似乎是列表分页。检查您的重现,当滚动位于列表末尾时无法触发回调。您可以为 _scrollListener() 添加此侦听器,而不是 ScrollController

_scrollController.addListener(() {
  if (_scrollController.position.atEdge) {
    if (_scrollController.position.pixels == 0)
      debugPrint('List scroll at top');
    else {
      debugPrint('List scroll at bottom');
      // Scroll is at the end of the page, load next page
      loadMoreImages(true);
    }
  }
});

点击页面底部后,调用应该在 ListView 上添加更多项目的方法。

// Succeeding pages will display 3 more items from the List
loadMoreImages(bool increment) {
  setState(() {
  if (!increment)
    // if increment is set to false
    // List will only show first page
    _listCursorEnd = 3;
  else
    // else, add items to load next page
    _listCursorEnd += 3;
  });
}

关于使用 StreamBuilder,利用此功能的一种方法是在 StreamController 中设置列​​表项。

var _streamController = StreamController<List<Album>>();

要在 StreamController 上添加内容,请使用 StreamController.add()

fetchAlbum().then((response) => _streamController.add(response));

然后在 Widget build() 上添加 StreamBuilder,并使用来自 StreamBuilder 的快照数据来填充 ListView。

StreamBuilder(
  stream: _streamController.stream,
  builder: (BuildContext context, AsyncSnapshot<List<Album>> snapshot) {
    if (snapshot.hasData) {
      // This ensures that the cursor won't exceed List<Album> length
      if (_listCursorEnd > snapshot.data.length)
        _listCursorEnd = snapshot.data.length;
    }
    return Widget(); // Populate ListView widget using snapshot data
  },
);
   

这是完整的代码。该示例还演示了作为奖励的“拉动刷新”功能。

import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  var _streamController = StreamController<List<Album>>();
  var _scrollController = ScrollController();

  // Succeeding pages should display 3 more items from the List
  loadMoreImages(bool increment) {
    setState(() {
      if (!increment)
        _listCursorEnd = 3;
      else
        _listCursorEnd += 3;
    });
  }

  // Call to fetch images
  loadImages(bool refresh) {
    fetchAlbum().then((response) => _streamController.add(response));
    if (refresh) loadMoreImages(!refresh); // refresh whole List
  }

  @override
  void initState() {
    super.initState();
    loadImages(false);
    _scrollController.addListener(() {
      if (_scrollController.position.atEdge) {
        if (_scrollController.position.pixels == 0)
          print('List scroll at top');
        else {
          print('List scroll at bottom');
          loadMoreImages(true);
        }
      }
    });
  }

  @override
  void dispose() {
    super.dispose();
    _streamController.close();
  }

  var _listCursorEnd = 21;

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: _streamController.stream,
      builder: (BuildContext context, AsyncSnapshot<List<Album>> snapshot) {
        if (snapshot.hasData) {
          // This ensures that the cursor won't exceed List<Album> length
          if (_listCursorEnd > snapshot.data.length)
            _listCursorEnd = snapshot.data.length;
          debugPrint('Stream snapshot contains ${snapshot.data.length} item/s');
        }
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: Center(
            child: RefreshIndicator(
              // onRefresh is a RefreshCallback
              // RefreshCallback is a Future Function().
              onRefresh: () async => loadImages(true),
              child: snapshot.hasData
                  ? ListView.builder(
                      controller: _scrollController,
                      primary: false,
                      padding: const EdgeInsets.all(20),
                      itemBuilder: (context, index) {
                        if (index < _listCursorEnd) {
                          return Container(
                            padding: const EdgeInsets.all(8),
                            child: Image.network(
                                snapshot.data[index].albumThumbUrl,
                                fit: BoxFit.cover),
                            // child: Thumbnail(image: imagePath, size: Size(100, 100)),
                          );
                        } else
                          return null;
                      },
                    )
                  : Text('Waiting...'),
            ),
          ),
        );
      },
    );
  }

  Future<List<Album>> fetchAlbum() async {
    final response =
        await http.get('https://jsonplaceholder.typicode.com/photos');

    if (response.statusCode == 200) {
      // If the server did return a 200 OK response,
      // then parse the JSON.
      Iterable iterableAlbum = json.decode(response.body);
      var albumList = List<Album>();
      List<Map<String, dynamic>>.from(iterableAlbum).map((Map model) {
        // Add Album mapped from json to List<Album>
        albumList.add(Album.fromJson(model));
      }).toList();
      return albumList;
    } else {
      // If the server did not return a 200 OK response,
      // then throw an exception.
      throw Exception('Failed to load album');
    }
  }

  getListImg(List<Album> listAlbum) {
    var listImages = List<Widget>();
    for (var album in listAlbum) {
      listImages.add(
        Container(
          padding: const EdgeInsets.all(8),
          child: Image.network(album.albumThumbUrl, fit: BoxFit.cover),
          // child: Thumbnail(image: imagePath, size: Size(100, 100)),
        ),
      );
    }
    return listImages;
  }
}

class Album {
  final int albumId;
  final int id;
  final String title;
  final String albumImageUrl;
  final String albumThumbUrl;

  Album(
      {this.albumId,
      this.id,
      this.title,
      this.albumImageUrl,
      this.albumThumbUrl});

  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      albumId: json['albumId'],
      id: json['id'],
      title: json['title'],
      albumImageUrl: json['url'],
      albumThumbUrl: json['thumbnailUrl'],
    );
  }
}

demo