Flutter 颤振:使用动态ListView显示分页API中的内容

Flutter 颤振:使用动态ListView显示分页API中的内容,flutter,Flutter,我是新手,所以请容忍我。我有一个分页API,这意味着调用example.com?loaditems.php?page=0将加载前10个项目(播客列表),而example.com?loaditems.php?page=1将加载10到20个项目,依此类推。 我希望StreamBuilder首先获取第0页,然后当列表到达底部时,它应该加载第1页并显示它。要检查是否已到达listview中的最后一项,我正在使用listview的ScrollController 现在,我在bloc模式中使用StreamB

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

现在,我在bloc模式中使用StreamBuilder、ListView和InheritedWidget。我不确定我是否正确实现了它,所以我将粘贴整个代码。 我的问题是,这是正确的集团模式吗?如果不是,那是什么? 我还看到了这篇文章: 最后它写着“更新:”但我不太明白

下面是应用程序的入口点:

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播客提供者:

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();
    }
}
我是个新手

欢迎光临

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

在此,我想指出,您的问题措辞相当宽泛,因此我将描述我自己的、自以为是的方法,说明我将如何在这个特定用例中进行状态管理。 很抱歉没有提供源代码,但颤振和BLoC模式是两个相对较新的东西,分页加载等应用程序仍然需要探索

我喜欢你选择的集团模式,尽管我不确定每次加载新播客时是否需要重新生成整个列表

此外,完全使用
Sink
s和
Stream
s的迂腐的整体式做事方式有时过于复杂。 特别是如果没有连续的“数据流”,而只是一个数据传输点,
Future
s会很好地完成这项工作。 这就是为什么我会在集群中生成一个方法,每次需要显示播客时都会调用该方法。它根据页面上播客的数量或加载的概念进行抽象,每次只返回一个
Future

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

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];
  }
}
很简单,对吧

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

  • 如果用户快速滚动,他们不会失去滚动速度,滚动视图不会“阻塞”
  • 如果用户滚动速度快或网络延迟高,则可能同时加载多个页面
  • 播客的寿命与小部件的寿命无关。如果您再次向下和向上滚动,播客不会被重新加载,尽管小部件会被重新加载。因为网络流量通常是一个瓶颈,所以这通常是一个值得做的权衡。请注意,这也可能是一个缺点,因为如果有成千上万的播客,您需要担心缓存失效
TL;DR:我喜欢该解决方案的一点是,它具有固有的灵活性和模块化,因为小部件本身非常“愚蠢”——缓存、加载等。所有这些都发生在后台。 利用这种灵活性,只需稍加努力,您就可以轻松实现以下功能:

  • 您可以跳转到任意id,从而只下载必要的小部件
  • 如果您想使用拉式重新加载功能,只需扔掉所有缓存(
    \u cache.clear()
    )即可自动重新获取播客

这是非常广泛的。你想完成什么?非常感谢你抽出时间详细回答。我会尝试你提供的解决方案,并会回来。考虑使用SpReTreMap代替地图。看见
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),
      );
    }
  },
);
}}
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];
  }
}
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);
        }
      );
    }
  );
}