Flutter StreamBuilder ListView在流数据更改时不会重新加载
Posted
技术标签:
【中文标题】Flutter StreamBuilder ListView在流数据更改时不会重新加载【英文标题】:Flutter StreamBuilder ListView not reloading when stream data changes 【发布时间】: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);
,
)));
【问题讨论】:
所有这些代码?伙计,你需要转行。 【参考方案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');
【讨论】:
以上是关于Flutter StreamBuilder ListView在流数据更改时不会重新加载的主要内容,如果未能解决你的问题,请参考以下文章
无法从 StreamBuilder (Flutter) 查询子集合