StreamBuilder ListView 在首次加载时从 Firestore 返回空列表

Posted

技术标签:

【中文标题】StreamBuilder ListView 在首次加载时从 Firestore 返回空列表【英文标题】:StreamBuilder ListView returns empty list from Firestore on first load 【发布时间】:2020-06-10 10:52:14 【问题描述】:

在我作为注册过程的一部分构建的应用程序中,为每个“用户”文档创建了一个子集合,其中包含多达 100 个文档。

我正在尝试在StreamBuilder 中显示这些子集合文档。

我有一个无法解决的奇怪错误。 StreamBuilder 在用户第一次查看时不显示数据。相反,它返回一个空列表。

我可以看到文档已在子集合中正确生成。数据正在使用 StreamBuilder 的页面之前的页面上设置。即使有延迟,我也会认为新文档会刚刚开始出现在 StreamBuilder 中。 Firebase console view

StreamBuilder确实在应用重新启动或用户注销并再次登录时按预期显示数据。

下面是我正在使用的代码:

Stream<QuerySnapshot> provideActivityStream() 
    return Firestore.instance
        .collection("users")
        .document(widget.userId)
        .collection('activities')
        .orderBy('startDate', descending: true)     
        .snapshots();
  
...
Widget activityStream() 
  return Container(
      padding: const EdgeInsets.all(20.0),
      child: StreamBuilder<QuerySnapshot>(
      stream: provideActivityStream(),
      builder: (BuildContext context,
          AsyncSnapshot<QuerySnapshot> snapshot) 
        if (snapshot.hasError)
          return new Text('Error: $snapshot.error');
        if(snapshot.data == null) 
          return CircularProgressIndicator();
        
        if(snapshot.data.documents.length < 1) 
          return new Text(
            snapshot.data.documents.toString()
            );
        
        if (snapshot != null) 
          print('$currentUser.userId');
        
        if (
          snapshot.hasData && snapshot.data.documents.length > 0
          ) 
          print("I have documents");
          return new ListView(
              children: snapshot.data.documents.map((
                  DocumentSnapshot document) 
                  return new PointCard(
                    title: document['title'],
                    type: document['type'],
                  );
                ).toList(),
            );
        
       
    )
  );

编辑:根据评论请求添加主构建

  @override
  Widget build(BuildContext context) 
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          title: Text("Home"),
          actions: <Widget>[
          ],
          bottom: TabBar(
            tabs: [
              Text("Account"),
              Text("Activity"),
              Text("Links"),
            ],
          ),
        ),
        body: TabBarView(
          children: [
            accountStream(),
            activityStream(),
            linksStream()
            ]
          )
        ),
      );
    
  

我已经尝试解决

我一开始以为是连接错误,所以根据switch (snapshot.connectionState)创建了一系列案例。我可以看到 ConnectionState.active = true 所以认为在 Firestore 中添加新文档可能会产生效果,但什么也没做。

我尝试了以下方法来使初始流构造函数异步。它无法加载任何数据。

Stream<QuerySnapshot> provideActivityStream() async* 
    await Firestore.instance
        .collection("users")
        .document(widget.userId)
        .collection('activities')
        .orderBy('startDate', descending: true)     
        .snapshots();
  

我尝试删除 tabcontroller 元素 - 例如只有一个页面 - 但这也无济于事。

我尝试使用DocumentSnapshotQuerySnapshot 访问数据。我两个都有问题。

我确信这很简单,但坚持下去。非常感谢任何帮助。谢谢!

【问题讨论】:

尝试传递 provideStravaActivityStream 作为对流的引用。 您介意发布整个小部件树的代码吗? @Neeraj 我已经编辑了这个问题。希望能给你更多的背景。它在 tabcontroller 中,但我不认为这是错误所在(因为我尝试将它作为独立页面加载并遇到同样的问题)。 @AbdelbakiBoukerche 感谢您发现错字。我已编辑问题以显示名称始终为provideActivityStream。如果我引用了错误的流,我将永远不会获得数据,但我会在重新加载事件中获得数据。 @heymonkeyriot 我的意思是使用 stream: provideActivityStream 而不是 stream: provideActivityStream() 【参考方案1】:

不是通过使用任何一个查询快照和文档快照来获取的

您应该首先使用 Querysnapshot 进行查询,然后将信息检索到 Documentsnapshot 是的,加载文档可能需要几秒钟,解决方案是正确的,您应该使用 async 和 await 函数

建议你使用 Direct snapshot 而不是 streamBuilder

我们可以在 statefullWidget 的 initstate 中加载文档快照 当您的类是 statefullWidget 并且问题也与状态有关时有效

...

 bool isLoading;
 List<DocumentSnapshot> activity =[];
 QuerySnapshot user;
 @override
 void initState() 
  print("in init state");
  super.initState();
  getDocument();
 ``     
  getDocument() async

 setState(() 
   isLoading = true;
 );
 user= await Firestore.instance
    .collection("users")
    .document(widget.userId)
    .collection('activities')
    .orderBy('startDate', descending: true)     
    .getDocuments();

  activity.isEmpty ? activity.addAll(user.documents) : null;

    setState(() 
  isLoading = false;
       );

     

//inside  Widget build(BuildContext context)  return  Scaffold( in your body 
//section of scaffold in the cointainer
Container(
padding: const EdgeInsets.all(20.0),
child: isLoading ?
         CircularProgressIndicator(),
        :ListView.builder(

                          itemCount: global.category.length,
                          itemBuilder: (context, index) 
                            return  PointCard(
                                    title: activity[index].data['title'],
                                      type: activity[index].data['type'],
                                    );//pointcard
                             
                            ),//builder
                     ),//container

【讨论】:

非常感谢分享这个。我会试试这个。也许我对 StreamBuilder 缺乏了解,但我认为使用 Streams 的目的是让我们不必担心 initState 和 setState。还是我误解了什么? 你的概念是绝对正确的,但在我们的例子中,我们应该使用 async 和 await 因为必须从 firestore 查询数据,这些是强制性的【参考方案2】:

我们也可以试试下面的

 QuerySnapshot qs;
 Stream<QuerySnapshot> provideActivityStream() async
    qs= await Firestore.instance
             .collection("users")
             .document(widget.userId)
             .collection('activities')
            .orderBy('startDate', descending: true)     
            .snapshots();

      return qs;
  //this should work

但如果上述部分不起作用,则根据 streambuilder 的基础知识 然后还有一个

     QuerySnapshot qs;
 Stream<QuerySnapshot> provideActivityStream() async* 
    qs= await Firestore.instance
             .collection("users")
             .document(widget.userId)
             .collection('activities')
            .orderBy('startDate', descending: true)     
            .snapshots();

      yield qs;
  //give this a try

【讨论】:

【参考方案3】:

tl;博士

需要使用 setState 才能让 Firebase currentUser uid 可用于小部件 需要使用AutomaticKeepAliveClientMixin 才能正确使用TabBar 我认为使用 Provider 包可能是保持用户状态的更好方法,但不能解决此问题

说明

我的代码通过 Future 获取 currentUser uid。根据the SO answer here,这是一个问题,因为在FirebaseAuth 可以返回uid 之前,所有小部件都将被构建。我最初尝试使用initState 来获取uid,但这有完全相同的同步问题。从函数调用setState 以调用FirebaseAuth.instance 允许更新小部件树。

我将此小部件放置在 TabBar 小部件中。我的理解是,每次从视图中删除选项卡时,它都会在返回时重新构建。这导致了进一步的状态问题。 AutomaticKeepAlive mixin 的 API 文档是 here

解决方案代码

添加了cmets,希望对其他人的理解有所帮助(或者有人可以纠正我的误解)

activitylist.dart

class ActivityList extends StatefulWidget 
// Need a stateful widget to use initState and setState later
  @override
  _ActivityListState createState() => _ActivityListState();


class _ActivityListState extends State<ActivityList> 
  with AutomaticKeepAliveClientMixin<ActivityList>
  // `with AutomaticKeepAliveClientMixin` added for TabBar state issues

  @override
  bool get wantKeepAlive => true;
  // above override required for mixin

  final databaseReference = Firestore.instance;

  @override
    initState() 
      this.getCurrentUser(); // call the void getCurrentUser function
      super.initState();
  

  FirebaseUser currentUser;

  void getCurrentUser() async 
    currentUser = await FirebaseAuth.instance.currentUser();
    setState(() 
      currentUser.uid;
    );
    // calling setState allows widgets to access uid and access stream
  

  Stream<QuerySnapshot> provideActivityStream() async* 
    yield* Firestore.instance
        .collection("users")
        .document(currentUser.uid)
        .collection('activities')
        .orderBy('startDate', descending: true)     
        .snapshots();
  

  @override
  Widget build(BuildContext context) 
    super.build(context);
    return Container(
                  padding: const EdgeInsets.all(20.0),
                  child: StreamBuilder<QuerySnapshot>(
                  stream: provideActivityStream(),
                  builder: (BuildContext context,
                  AsyncSnapshot<QuerySnapshot> snapshot) 
                    if(snapshot.hasError) return CircularProgressIndicator();
                    if(snapshot.data == null) return CircularProgressIndicator();
                    else if(snapshot.data !=null) 
                      return new ListView(
                          children: snapshot.data.documents.map((
                              DocumentSnapshot document) 
                            return new ActivityCard(
                              title: document['title'],
                              type: document['type'],
                              startDateLocal: document['startDateLocal'],
                            );
                          ).toList(),
                        );
                    
                  ,
                )
            );
  

home.dart

...
@override
  Widget build(BuildContext context) 
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          title: Text("Home"),
          actions: <Widget>[
          ],
          bottom: TabBar(
            tabs: [
              Text("Account"),
              Text("Activity"),
              Text("Links"),
            ],
          ),
        ),
        body: TabBarView(
          children: [
            accountStream(),
            ActivityList(), // now calling a stateful widget in an external file
            linksStream()
            ]
          )
        ),
      );
    
  

【讨论】:

以上是关于StreamBuilder ListView 在首次加载时从 Firestore 返回空列表的主要内容,如果未能解决你的问题,请参考以下文章

在 StreamBuilder 中渲染 ListView.ListTile

Flutter Refreshindicator 在使用 StreamBuilder 添加新项目后重建 Listview.Builder

防止 Flutter StreamBuilder 在 ListView 中复制数据

如何在streambuilder中查询firestore文档并更新listview

Flutter:如何将 StreamBuilder 与 ListView.separated 一起使用

Flutter StreamBuilder ListView在流数据更改时不会重新加载