Flutter:使用动态 ListView 显示分页 API 中的内容 [英] Flutter: Display content from paginated API with dynamic ListView

查看:41
本文介绍了Flutter:使用动态 ListView 显示分页 API 中的内容的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我是 Flutter 的新手,所以请多多包涵.我有一个分页 API,这意味着调用 example.com?loaditems.php?page=0 将加载前 10 个项目(播客列表)和 example.com?loaditems.php?page=1 将加载从 10 到 20 的项目,依此类推.我希望 StreamBuilder 首先获取页面 0,然后当列表到达底部时它应该加载页面 1 并显示它.要检查是否已到达 listview 中的最后一项,我正在使用 ListView 的 ScrollController.

I am new to Flutter so please bear with me. I have a paginated API that means on calling example.com?loaditems.php?page=0 will load first 10 items(podcasts list) and example.com?loaditems.php?page=1 will load items from 10 to 20 and so on. I want StreamBuilder to fetch page 0 first then when the list reaches the bottom it should load page 1 and show it. To check if I have reached the last item in listview I am using ScrollController of ListView.

现在我以块模式使用 StreamBuilder、ListView、InheritedWidget.我不确定我是否正确实现了它,所以我要粘贴整个代码.我的问题是,这是正确的 BLOC 模式方法吗?如果不是那么它是什么?我还看到了这篇文章:https://crossingthestreams.io/loading-paginated-data-with-list-views-in-flutter/最后它说更新:"但我不太明白.

Now I am using StreamBuilder, ListView, InheritedWidget in bloc pattern. I am not sure if I have implemented it correctly so I am gonna paste the entire code. My question is, is this is the correct BLOC pattern way to do it? If not then what is it? I also came across with this article: https://crossingthestreams.io/loading-paginated-data-with-list-views-in-flutter/ In the end it says "Update:" but I could not understand it much.

这里是应用的入口点:

void main() => runApp(new MaterialApp(
title: "XYZ",
theme: ThemeData(fontFamily: 'Lato'),
home: PodcastsProvider( //This is InheritedWidget
  child: RecentPodcasts(), //This is the child of InheritedWidget
 ),
));

这是 InheritedWidget PodcastsProvider:

class PodcastsProvider extends InheritedWidget{

    final PodcastsBloc bloc;  //THIS IS THE BLOC

    PodcastsProvider({Key key, Widget child})
    :   bloc = PodcastsBloc(),
    super(key: key, child: child);

    @override
    bool updateShouldNotify(InheritedWidget oldWidget) {
      return true;
    }

    static PodcastsBloc of(BuildContext context){
      return (context.inheritFromWidgetOfExactType(PodcastsProvider) as 
PodcastsProvider).bloc;
    }
}

这是集团

class PodcastsBloc{

    var _podcasts = PublishSubject<List<Podcast>>();

    Observable<List<Podcast>> get podcasts =>_podcasts.stream;

    getPodcasts(pageCount) async{
      NetworkProvider provider = NetworkProvider();
      var podcasts = await provider.getRecentPodcasts(pageCount);
     _podcasts.sink.add(podcasts);
    }

    despose(){
      _podcasts.close();
    }
}

这是视图部分(InheritedWidget 的子级)

class RecentPodcasts extends StatefulWidget {
   @override
  _RecentPodcastsState createState() => _RecentPodcastsState();
}

class _RecentPodcastsState extends State<RecentPodcasts> {
   ScrollController controller = ScrollController();
   PodcastsBloc podcastsBloc;
   bool isLoading = false;
   List<Podcast> podcasts;

   @override
   void didChangeDependencies() {
     super.didChangeDependencies();
     podcastsBloc = PodcastsProvider.of(context);
     podcastsBloc.getPodcasts(null);
     controller.addListener((){
     if(controller.position.pixels == controller.position.maxScrollExtent && !isLoading){
       setState(() {
         isLoading = true;
         podcastsBloc.getPodcasts(podcasts[podcasts.length-1].id);
       });
      }
     }); //listerner ends
   }

最后,_RecentPodcastsState的构建方法调用了这个:

Finally, build method of _RecentPodcastsState calls this:

Widget getRecentPodcastsList(PodcastsBloc podcastsBloc) {

return StreamBuilder(
  stream: podcastsBloc.podcasts,
  builder: (context, snapshot) {
    //isLoading = false;
    if (snapshot.hasData) {

      podcasts.addAll(snapshot.data); //THIS IS A PROBLEM, THIS GIVES ME AN ERROR: flutter: Tried calling: addAll(Instance(length:20) of '_GrowableList')

      return ListView.builder(
          scrollDirection: Axis.vertical,
          padding: EdgeInsets.zero,
          controller: controller,
          itemCount: podcasts.length,
          itemBuilder: (context, index) {
            return RecentPodcastListItem(podcasts[index]);
          });
    } else if (snapshot.hasError) {
      //SHOW ERROR TEXT
      return Text("Error!");
    } else {
      //LOADER GOES HERE
      return Text(
        "Loading...",
        style: TextStyle(color: Colors.white),
      );
    }
  },
);
}}

推荐答案

我是 Flutter 的新手

I am new to Flutter

欢迎!

首先,我想表达我对分页 API 的担忧,因为当用户滚动列表时可以添加播客,导致播客丢失或显示两次.

First of all, I want to express my concern against paginated APIs, since podcasts can be added while the user scrolls the list, resulting in podcasts missing or being displayed twice.

顺便说一下,我想指出您的问题措辞相当宽泛,所以我将描述我自己的,关于如何在这个特定用例中进行状态管理的自以为是的方法.抱歉没有提供源码,但 Flutter 和 BLoC 模式是两个比较新的东西,像分页加载这样的应用还有待探索.

Having that out of the way, I'd like to point out that your question is quite broadly phrased, so I'll describe my own, opinionated approach on how I would do state management in this particular use case. Sorry for not providing sources, but Flutter and BLoC pattern are two relatively new things, and applications like paginated loading still need to be explored.

我喜欢您选择的 BLoC 模式,尽管我不确定每次加载一些新播客时都需要重建整个列表.

I like your choice of BLoC pattern, although I'm not sure the entire list needs to rebuild every time some new podcasts loaded.

此外,完全使用 Sinks 和 Streams 的迂腐 BLoC-y 方式有时过于复杂.特别是如果没有连续的数据流"而只有一个数据传输点,Future 就可以很好地完成这项工作.这就是为什么我会在 BLoC 中生成一个方法,每次需要显示播客时都会调用该方法.它从页面上的播客数量或加载概念中抽象出来 - 它每次都简单地返回一个Future.

Also, the pedantically BLoC-y way of doing things entirely with Sinks and Streams is sometimes overly complex. Especially if there is no continual "stream of data" but rather just a single point of data transimission, Futures do the job quite well. That's why I'd generate a method in the BLoC that gets called every time a podcast needs to be displayed. It abstracts from the number of podcasts on a page or the concept of loading - it simply returns a Future<Podcast> every time.

例如,考虑一个提供这种方法的 BLoC:

For example, consider a BLoC providing this method:

final _cache = Map<int, Podcast>();
final _downloaders = Map<int, Future<List<Podcast>>>();

/// Downloads the podcast, if necessary.
Future<Podcast> getPodcast(int index) async {
  if (!_cache.containsKey(index)) {
    final page = index / 10;
    await _downloadPodcastsToCache(page);
  }
  if (!_cache.containsKey(index)) {
    // TODO: The download failed, so you should probably provide a more
    // meaningful error here.
    throw Error();
  }
  return _cache[index];
}

/// Downloads a page of podcasts to the cache or just waits if the page is
/// already being downloaded.
Future<void> _downloadPodcastsToCache(int page) async {
  if (!_downloaders.containsKey(page)) {
    _downloaders[page] = NetworkProvider().getRecentPodcasts(page);
    _downloaders[page].then((_) => _downloaders.remove(page));
  }
  final podcasts = await _downloaders[page];
  for (int i = 0; i < podcasts.length; i++) {
    _cache[10 * page + i] = podcasts[i];
  }
}

此方法为您的小部件层提供了一个非常简单的 API.那么现在,让我们看看从小部件层的角度来看它的外观.假设您有一个 PodcastView 小部件,它显示 Podcast 或如果 podcastnull 的占位符.然后你可以很容易地写:

This method provides a very simple API to your widget layer. So now, let's see how it look from the widget layer point of view. Assume, you have a PodcastView widget that displays a Podcast or a placeholder if podcast is null. Then you could easily write:

Widget build(BuildContext context) {
  return ListView.builder(
    itemBuilder: (ctx, index) {
      return FutureBuilder(
        future: PodcastsProvider.of(ctx).getPodcast(index),
        builder: (BuildContext ctx, AsyncSnapshot<Podcast> snapshot) {
          if (snapshot.hasError) {
            return Text('An error occurred while downloading this podcast.');
          }
          return PodcastView(podcast: snapshot.data);
        }
      );
    }
  );
}

很简单吧?

与您链接中的解决方案相比,此解决方案的优势:

Benefits of this solution compared to the one from your link:

  • 用户在快速滚动时不会失去滚动速度,滚动视图永远不会阻塞".
  • 如果用户滚动速度快或网络延迟高,可能会同时加载多个页面.
  • 播客寿命与小部件寿命无关.如果您再次向下和向上滚动,则不会重新加载播客,尽管小部件会重新加载.因为网络流量通常是一个瓶颈,所以这通常是值得做的权衡.请注意,这也可能是一个缺点,因为如果您有数万个播客,您需要担心缓存失效.

TL;DR:我喜欢这个解决方案的地方在于它本质上是灵活和模块化的,因为小部件本身非常愚蠢"——缓存、加载等都发生在后台.利用这种灵活性,只需稍加努力,您就可以轻松实现这些功能:

TL;DR: What I like about that solution is that it's inherently flexible and modular because the widgets themselves are quite "dumb" - caching, loading etc. all happens in the background. Making use of that flexibility, with a little bit of work, you could easily achieve these features:

  • 您可以跳转到任意 ID,从而仅下载必要的小部件.
  • 如果您想实现拉取重新加载功能,只需丢弃所有缓存(_cache.clear()),播客将自动重新获取.
  • You could jump to an arbitrary id, causing only the necessary widgets to be downloaded.
  • If you want to make a pull-to-reload functionality, just throw out all the cache (_cache.clear()) and the podcasts will be re-fetched automatically.

这篇关于Flutter:使用动态 ListView 显示分页 API 中的内容的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆