【问题标题】:Flutter StreamBuilder ListView not reloading when stream data changesFlutter StreamBuilder ListView在流数据更改时不会重新加载
【发布时间】:2020-03-26 10:49:28
【问题描述】:

我正在尝试构建一个应用程序,该应用程序可以从 ListView 中的博客加载无穷无尽的提要。在顶部,用户可以通过“类别”菜单选择根据某个类别过滤提要。当用户点击“类别”菜单时,会出现另一个 ListView,其中包含所有可用类别。当用户点击所需的类别时,应用程序应返回到提要 ListView 仅显示该类别下的帖子。

预期结果:

  • 应用调用 API 并检索 10 个最新帖子
  • 随着用户滚动,接下来的 10 个帖子将通过连续的 API 调用检索
  • 用户点击“类别”菜单并打开带有类别的 ListView。
  • 用户点击所需的类别,应用程序返回到提要列表视图,创建一个 API 调用以检索该类别的前 10 个帖子。
  • 当用户滚动时,该类别的下 10 个帖子将通过连续 API 检索 来电。

观察结果:

  • 应用调用 API 并检索 10 个最新帖子
  • 随着用户滚动,接下来的 10 个帖子将通过连续的 API 调用检索
  • 用户点击“类别”菜单并打开带有类别的 ListView。
  • 用户点击所需的类别,应用程序返回到提要列表视图,创建一个 API 调用以检索该类别的前 10 个帖子。
  • 所需类别的帖子被附加到 ListView 并且仅出现在帖子之后 之前已加载。

我的问题:

我必须如何修改我的状态或我的 Bloc,才能获得想要的结果?

相关截图

我的结构:

PostBloc - 我的 bloc 组件,其中包含 Articles 和 ArticleCategory StreamBuilders 的流定义。还包含对 API 进行调用的方法 获取文章和文章类别。

  class PostBloc extends Bloc<PostEvent, PostState> {
  final http.Client httpClient;
  int _currentPage = 1;
  int _limit = 10;
  int _totalResults = 0;
  int _numberOfPages = 0;

  int _categoryId;

  bool hasReachedMax = false;

  var cachedData = new Map<int, Article>();

  PostBloc({@required this.httpClient}) {

    //Listen to when user taps a category in the ArticleCategory ListView
    _articleCategoryController.stream.listen((articleCategory) {
      if (articleCategory.id != null) {
        _categoryId = articleCategory.id;

        _articlesSubject.add(UnmodifiableListView(null));

        _currentPage = 1;

        _fetchPosts(_currentPage, _limit, _categoryId)
            .then((articles) {
          _articlesSubject.add(UnmodifiableListView(articles));
        });
        _currentPage++;
        dispatch(Fetch());
      }
    });

    _currentPage++;
  }

  List<Article> _articles = <Article>[];

  // Category Sink for listening to the tapped category
  final _articleCategoryController = StreamController<ArticleCategory>();
  Sink<ArticleCategory> get getArticleCategory =>
      _articleCategoryController.sink;

  //Article subject for populating articles ListView
  Stream<UnmodifiableListView<Article>> get articles => _articlesSubject.stream;
  final _articlesSubject = BehaviorSubject<UnmodifiableListView<Article>>();

  //Categories subjet for the article categories
  Stream<UnmodifiableListView<ArticleCategory>> get categories => _categoriesSubject.stream;
  final _categoriesSubject = BehaviorSubject<UnmodifiableListView<ArticleCategory>>();


  void dispose() {
    _articleCategoryController.close();
  }

  @override
  Stream<PostState> transform(
    Stream<PostEvent> events,
    Stream<PostState> Function(PostEvent event) next,
  ) {
    return super.transform(
      (events as Observable<PostEvent>).debounceTime(
        Duration(milliseconds: 500),
      ),
      next,
    );
  }

  @override
  get initialState => PostUninitialized();

  @override
  Stream<PostState> mapEventToState(PostEvent event) async* {

    //This event is triggered when user taps on categories menu
    if (event is ShowCategory) {
      _currentPage = 1;
      await _fetchCategories(_currentPage, _limit).then((categories) {
        _categoriesSubject.add(UnmodifiableListView(categories));
      });
      yield PostCategories();
    }

    // This event is triggered when user taps on a category
    if(event is FilterCategory){
      yield PostLoaded(hasReachedMax: false);
    }

    // This event is triggered when app loads and when user scrolls to the bottom of articles
    if (event is Fetch && !_hasReachedMax(currentState)) {
      try {
        //First time the articles feed opens
        if (currentState is PostUninitialized) {
          _currentPage = 1;
          await _fetchPosts(_currentPage, _limit).then((articles) {
            _articlesSubject.add(UnmodifiableListView(articles)); //Send to stream
          });
          this.hasReachedMax = false;
          yield PostLoaded(hasReachedMax: false);
          _currentPage++;
          return;
        }

        //User scrolls to bottom of ListView
        if (currentState is PostLoaded) {
          await _fetchPosts(_currentPage, _limit, _categoryId)
              .then((articles) {
            _articlesSubject.add(UnmodifiableListView(articles));//Append to stream
          });
          _currentPage++;

          // Check if last page has been reached or not
          if(_currentPage > _numberOfPages){
            this.hasReachedMax = true;
          }
          else{
            this.hasReachedMax = false;
          }
          yield (_currentPage > _numberOfPages)
              ? (currentState as PostLoaded).copyWith(hasReachedMax: true)
              : PostLoaded(
                  hasReachedMax: false,
                );
        }
      } catch (e) {
        print(e.toString());
        yield PostError();
      }
    }
  }

  bool _hasReachedMax(PostState state) =>
      state is PostLoaded && this.hasReachedMax;

  Article _getArticle(int index) {
    if (cachedData.containsKey(index)) {
      Article data = cachedData[index];
      return data;
    }
    throw Exception("Article could not be fetched");
  }

  /**
   * Fetch all articles
   */
  Future<List<Article>> _fetchPosts(int startIndex, int limit,
      [int categoryId]) async {
    String query =
        'https://www.batatolandia.de/api/batatolandia/articles?page=$startIndex&limit=$limit';
    if (categoryId != null) {
      query += '&category_id=$categoryId';
    }

    final response = await httpClient.get(query);
    if (response.statusCode == 200) {
      final data = json.decode(response.body);

      ArticlePagination res = ArticlePagination.fromJson(data);

      _totalResults = res.totalResults;
      _numberOfPages = res.numberOfPages;

      for (int i = 0; i < res.data.length; i++) {
        _articles.add(res.data[i]);
      }

      return _articles;
    } else {
      throw Exception('error fetching posts');
    }
  }

/**
 * Fetch article categories
 */
  Future<List<ArticleCategory>> _fetchCategories(int startIndex, int limit,
      [int categoryId]) async {
    String query =
        'https://www.batatolandia.de/api/batatolandia/articles/categories?page=$startIndex&limit=$limit';

    final response = await httpClient.get(query);
    if (response.statusCode == 200) {
      final data = json.decode(response.body);

      ArticleCategoryPagination res = ArticleCategoryPagination.fromJson(data);

      _totalResults = res.totalResults;
      _numberOfPages = res.numberOfPages;

      List<ArticleCategory> categories = <ArticleCategory>[];
      categories.add(ArticleCategory(id: 0 , title: 'Todos', color: '#000000'));

      for (int i = 0; i < res.data.length; i++) {
        categories.add(res.data[i]);
      }

      return categories;
    } else {
      throw Exception('error fetching categories');
    }
  }
}

Articles - 包含一个 BlocProvider 来读取 PostBloc 中设置的当前状态并显示 对应的视图。

class Articles extends StatelessWidget{

  PostBloc _postBloc;

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
        builder: (context) =>
        PostBloc(httpClient: http.Client())..dispatch(Fetch()),
        child:  BlocBuilder<PostBloc, PostState>(
            builder: (context, state){

              _postBloc = BlocProvider.of<PostBloc>(context);

              // Displays circular progress indicator while posts are being retrieved
              if (state is PostUninitialized) {
                return Center(
                  child: CircularProgressIndicator(),
                );
              }
              // Shows the feed Listview when API responds with the posts data
              if (state is PostLoaded) {
                return ArticlesList(postBloc:_postBloc );
              }
              // Shows the Article categories Listview when user clicks on menu
              if(state is PostCategories){
                return ArticlesCategoriesList(postBloc: _postBloc);
              }
              //Shows error if there are any problems while fetching posts
              if (state is PostError) {
                return Center(
                  child: Text('Failed to fetch posts'),
                );
              }
              return null;
            }
        )
    );
  }
}

ArticlesList - 包含一个 StreamBuilder,它从 PostBloc 读取文章数据并加载到提要 ListView 中。

class ArticlesList extends StatelessWidget {

  ScrollController _scrollController = new ScrollController();

  int currentPage = 1;
  int _limit = 10;
  int totalResults = 0;
  int numberOfPages = 0;

  final _scrollThreshold = 200.0;

  Completer<void> _refreshCompleter;

  PostBloc postBloc;
  ArticlesList({Key key, this.postBloc}) : super(key: key);

  @override
  Widget build(BuildContext context) {

    _scrollController.addListener(_onScroll);
    _refreshCompleter = Completer<void>();

    return Scaffold(
      appBar: AppBar(
        title: Text("Posts"),
      ),
      body:  StreamBuilder<UnmodifiableListView<Article>>(
    stream: postBloc.articles,
        initialData: UnmodifiableListView<Article>([]),
        builder: (context, snapshot) {
          if(snapshot.hasData && snapshot != null) {
            if(snapshot.data.length > 0){
              return Column(
                mainAxisSize: MainAxisSize.max,
                children: <Widget>[
                  ArticlesFilterBar(),
                  Expanded(
                    child: RefreshIndicator(
                      child: ListView.builder(
                        itemBuilder: (BuildContext context,
                            int index) {
                          return index >= snapshot.data.length
                              ? BottomLoader()
                              : ArticlesListItem(
                              article: snapshot.data.elementAt(
                                  index));
                        },
                        itemCount: postBloc.hasReachedMax
                            ? snapshot.data.length
                            : snapshot.data.length + 1,
                        controller: _scrollController,
                      ),
                      onRefresh: _refreshList,
                    ),
                  )
                ],
              );
            }
            else if (snapshot.data.length==0){
              return Center(
                child: CircularProgressIndicator(),
              );
            }

          }
          else{
            Text("Error!");
          }
          return CircularProgressIndicator();
        }
        )
    );
  }

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

  void _onScroll() {
    final maxScroll = _scrollController.position.maxScrollExtent;
    final currentScroll = _scrollController.position.pixels;
    if (maxScroll - currentScroll <= _scrollThreshold) {
      postBloc.dispatch(Fetch());
    }
  }

  Future<void> _refreshList() async {
    postBloc.dispatch(Fetch());
    return null;
  }
}

ArticlesCategoriesList - 一个 StreamBuilder,它从 PostBloc 读取类别并加载到 ListView。

class ArticlesCategoriesList extends StatelessWidget {

  PostBloc postBloc;
  ArticlesCategoriesList({Key key, this.postBloc}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Categorias"),
        ),
        body:
        SafeArea(
            child: StreamBuilder<UnmodifiableListView<ArticleCategory>>(
          stream: postBloc.categories,
          initialData: UnmodifiableListView<ArticleCategory>([]),
      builder: (context, snapshot) {
        return ListView.separated(
            itemBuilder: (BuildContext context, int index) {
              return new Container(
                  decoration: new BoxDecoration(
                    color: Colors.white,
                  ),
                  child: ListTile(
                    dense: true,
                    leading: Icon(Icons.fiber_manual_record,color: HexColor(snapshot.data[index].color)),
                    trailing: Icon(Icons.keyboard_arrow_right),
                    title: Text(snapshot.data[index].title),
                    onTap: () {
                      postBloc.getArticleCategory.add(snapshot.data[index]);
                    },
                  ));
            },
            separatorBuilder: (context, index) => Divider(
                  color: Color(0xff666666),
                  height: 1,
                ),
            itemCount: snapshot.data.length);
      },
    )));
  }
}

【问题讨论】:

  • 所有这些代码?伙计,你需要转行。

标签: flutter dart bloc


【解决方案1】:

我在这里回答我自己的问题... 最后,每当检测到类别点击事件时,我通过清除 _articles 列表让一切顺利运行。

所以这里是新的 PostBloc

class PostBloc extends Bloc<PostEvent, PostState> {
  final http.Client httpClient;
  int _currentPage = 1;
  int _limit = 10;
  int _totalResults = 0;
  int _numberOfPages = 0;

  int _categoryId;

  bool hasReachedMax = false;

  var cachedData = new Map<int, Article>();

  List<Article> _articles = <Article>[];


  PostBloc({@required this.httpClient}) {

    //Listen to when user taps a category in the ArticleCategory ListView
    _articleCategoryController.stream.listen((articleCategory) {
      if (articleCategory.id != null) {
        _categoryId = articleCategory.id;

        _currentPage = 1;
        _articles.clear();

        _fetchPosts(_currentPage, _limit, _categoryId)
            .then((articles) {
          _articlesSubject.add(UnmodifiableListView(articles));
        });
        _currentPage++;
        dispatch(FilterCategory());
      }
    });

  }



  // Category Sink for listening to the tapped category
  final _articleCategoryController = StreamController<ArticleCategory>();
  Sink<ArticleCategory> get getArticleCategory =>
      _articleCategoryController.sink;

  //Article subject for populating articles ListView
  Stream<UnmodifiableListView<Article>> get articles => _articlesSubject.stream;
  final _articlesSubject = BehaviorSubject<UnmodifiableListView<Article>>();

  //Categories subjet for the article categories
  Stream<UnmodifiableListView<ArticleCategory>> get categories => _categoriesSubject.stream;
  final _categoriesSubject = BehaviorSubject<UnmodifiableListView<ArticleCategory>>();


  void dispose() {
    _articleCategoryController.close();
  }

  @override
  Stream<PostState> transform(
    Stream<PostEvent> events,
    Stream<PostState> Function(PostEvent event) next,
  ) {
    return super.transform(
      (events as Observable<PostEvent>).debounceTime(
        Duration(milliseconds: 500),
      ),
      next,
    );
  }

  @override
  get initialState => PostUninitialized();

  @override
  Stream<PostState> mapEventToState(PostEvent event) async* {

    //This event is triggered when user taps on categories menu
    if (event is ShowCategory) {
      _currentPage = 1;
      await _fetchCategories(_currentPage, _limit).then((categories) {
        _categoriesSubject.add(UnmodifiableListView(categories));
      });
      yield PostCategories();
    }

    // This event is triggered when user taps on a category
    if(event is FilterCategory){
      yield PostLoaded(hasReachedMax: false);
    }

    // This event is triggered when app loads and when user scrolls to the bottom of articles
    if (event is Fetch && !_hasReachedMax(currentState)) {
      try {
        //First time the articles feed opens
        if (currentState is PostUninitialized) {
          _currentPage = 1;
          await _fetchPosts(_currentPage, _limit).then((articles) {
            _articlesSubject.add(UnmodifiableListView(articles)); //Send to stream
          });
          this.hasReachedMax = false;
          yield PostLoaded(hasReachedMax: false);
          _currentPage++;
          return;
        }

        //User scrolls to bottom of ListView
        if (currentState is PostLoaded) {
          await _fetchPosts(_currentPage, _limit, _categoryId)
              .then((articles) {
            _articlesSubject.add(UnmodifiableListView(_articles));//Append to stream
          });
          _currentPage++;

          // Check if last page has been reached or not
          if(_currentPage > _numberOfPages){
            this.hasReachedMax = true;
          }
          else{
            this.hasReachedMax = false;
          }
          yield (_currentPage > _numberOfPages)
              ? (currentState as PostLoaded).copyWith(hasReachedMax: true)
              : PostLoaded(
                  hasReachedMax: false,
                );
        }
      } catch (e) {
        print(e.toString());
        yield PostError();
      }
    }
  }

  bool _hasReachedMax(PostState state) =>
      state is PostLoaded && this.hasReachedMax;

  Article _getArticle(int index) {
    if (cachedData.containsKey(index)) {
      Article data = cachedData[index];
      return data;
    }
    throw Exception("Article could not be fetched");
  }

  /**
   * Fetch all articles
   */
  Future<List<Article>> _fetchPosts(int startIndex, int limit,
      [int categoryId]) async {
    String query =
        'https://www.batatolandia.de/api/batatolandia/articles?page=$startIndex&limit=$limit';
    if (categoryId != null) {
      query += '&category_id=$categoryId';
    }

    final response = await httpClient.get(query);
    if (response.statusCode == 200) {
      final data = json.decode(response.body);

      ArticlePagination res = ArticlePagination.fromJson(data);

      _totalResults = res.totalResults;
      _numberOfPages = res.numberOfPages;

      List<Article> posts = <Article>[];

      for (int i = 0; i < res.data.length; i++) {
        _articles.add(res.data[i]);
        posts.add(res.data[i]);
      }

      return posts;
    } else {
      throw Exception('error fetching posts');
    }
  }

/**
 * Fetch article categories
 */
  Future<List<ArticleCategory>> _fetchCategories(int startIndex, int limit,
      [int categoryId]) async {
    String query =
        'https://www.batatolandia.de/api/batatolandia/articles/categories?page=$startIndex&limit=$limit';

    final response = await httpClient.get(query);
    if (response.statusCode == 200) {
      final data = json.decode(response.body);

      ArticleCategoryPagination res = ArticleCategoryPagination.fromJson(data);

      _totalResults = res.totalResults;
      _numberOfPages = res.numberOfPages;

      List<ArticleCategory> categories = <ArticleCategory>[];
      categories.add(ArticleCategory(id: 0 , title: 'Todos', color: '#000000'));

      for (int i = 0; i < res.data.length; i++) {
        categories.add(res.data[i]);
      }

      return categories;
    } else {
      throw Exception('error fetching categories');
    }
  }
}

【讨论】:

    猜你喜欢
    • 2019-08-19
    • 1970-01-01
    • 2020-07-02
    • 1970-01-01
    • 2019-05-09
    • 2020-11-10
    • 2021-08-04
    • 2021-10-19
    • 2021-12-14
    相关资源
    最近更新 更多