You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中使用Appwrite和Riverpod获取评论时遇Bad State: No Element错误

问题描述

Flutter项目结合Appwrite与Riverpod开发时,点击查看帖子评论频繁触发Bad State: No Element错误,热重载后可能暂时消失,但后续仍会复现。

相关代码片段:

ref.watch(getRepliesToPostProvider(widget.postModel!)).when(
                  data: (post) {
                    return ref.watch(getLatestPostProvider).when(
                        data: (data) {
                          if (data.events.contains(
                            'databases.*.collections.${Common.postCollection}.documents.*.create',
                          )) {
                            post.insert(0, PostModel.fromMap(data.payload));
                          } else if (data.events.contains(
                            'databases.*.collections.${Common.postCollection}.documents.*.update',
                          )) {
                            final startingIndex =
                                data.events[0].lastIndexOf('documents.');
                            final endIndex =
                                data.events[0].lastIndexOf('.update');

                            var postId = data.events[0]
                                .substring(startingIndex + 10, endIndex);

                            var newPost = post
                                .where((element) => element.id == postId)
                                .first;

                            final postIndex = post.indexOf(newPost);
                            post.removeWhere((element) => element.id == postId);

                            newPost = PostModel.fromMap(data.payload);
                            post.insert(postIndex, newPost);
                          }

                          return Expanded(
                            child: ListView.builder(
                              padding: const EdgeInsets.all(0),
                              shrinkWrap: true,
                              scrollDirection: Axis.vertical,
                              physics: const ClampingScrollPhysics(),
                              itemCount: post.length,
                              itemBuilder: (BuildContext context, int index) {
                                var disPost = post[index];

                                final latestPost =
                                    PostModel.fromMap(data.payload);
                                bool isPostAlreadyPresent = false;
                                for (final postLoop in post) {
                                  if (postLoop.id == latestPost.id) {
                                    isPostAlreadyPresent = true;
                                    break;
                                  }
                                }

                                return ref
                                    .watch(userDetailsProvider(
                                        widget.postModel!.uid))
                                    .when(
                                        data: (user) {
                                          return Padding(
                                            padding: const EdgeInsets.only(
                                              left: 8.0,
                                              top: 2,
                                              right: 8,
                                              bottom: 2,
                                            ),
                                            child: Row(
                                              children: [
                                                CircularProfileAvatar(
                                                  user.profile,
                                                  radius: 20,
                                                  backgroundColor:
                                                      Colors.transparent,
                                                  borderWidth: 2,
                                                  initialsText: Text(
                                                    "AD",
                                                    style: TextStyle(
                                                        color: colorWhite),
                                                  ),
                                                  borderColor: colorWhite,
                                                  elevation: 5.0,
                                                  foregroundColor: Colors.brown
                                                      .withOpacity(0.5),
                                                  cacheImage: true,
                                                  imageFit: BoxFit.cover,
                                                  onTap: () {
                                                    print('adil');
                                                  },
                                                  showInitialTextAbovePicture:
                                                      false,
                                                ),
                                                const SizedBox(
                                                  width: 15,
                                                ),
                                                Expanded(
                                                  child: Text(disPost.text),
                                                ),
                                              ],
                                            ),
                                          );
                                        },
                                        error: (error, stk) => Container(),
                                        loading: () => Container());
                              },
                            ),
                          );
                        },
                        error: (error, stk) => ErrorText(
                              error: error.toString(),
                            ),
                        loading: () {
                          return Expanded(
                            child: ListView.builder(
                                padding: const EdgeInsets.all(0),
                                shrinkWrap: true,
                                scrollDirection: Axis.vertical,
                                physics: const ClampingScrollPhysics(),
                                itemCount: post.length,
                                itemBuilder: (BuildContext context, int index) {
                                  var disPost = post[index];

                                  return ref
                                      .watch(userDetailsProvider(
                                          widget.postModel!.uid))
                                      .when(
                                          data: (user) {
                                            return Padding(
                                              padding: const EdgeInsets.only(
                                                left: 8.0,
                                                top: 2,
                                                right: 8,
                                                bottom: 2,
                                              ),
                                              child: Row(
                                                children: [
                                                  CircularProfileAvatar(
                                                    user.profile,
                                                    radius: 20,
                                                    backgroundColor:
                                                        Colors.transparent,
                                                    borderWidth: 2,
                                                    initialsText: Text(
                                                      "AD",
                                                      style: TextStyle(
                                                          color: colorWhite),
                                                    ),
                                                    borderColor: colorWhite,
                                                    elevation: 5.0,
                                                    foregroundColor: Colors
                                                        .brown
                                                        .withOpacity(0.5),
                                                    cacheImage: true,
                                                    imageFit: BoxFit.cover,
                                                    onTap: () {
                                                      print('adil');
                                                    },
                                                    showInitialTextAbovePicture:
                                                        false,
                                                  ),
                                                  const SizedBox(
                                                    width: 15,
                                                  ),
                                                  Expanded(
                                                    child: Text(disPost.text),
                                                  ),
                                                ],
                                              ),
                                            );
                                          },
                                          error: (error, stk) => Container(),
                                          loading: () => Container());
                                }),
                          );
                        });
                  },
                  error: (error, stk) => ErrorText(
                    error: error.toString(),
                  ),
                  loading: () => const Loader(),
                ),

错误截图:
错误截图1
错误截图2
错误截图3

问题根源

错误直接出在更新事件处理逻辑中:

var newPost = post.where((element) => element.id == postId).first;

当Appwrite推送更新事件时,本地评论列表post中可能还不存在对应postId的元素,调用.first会直接抛出No Element异常。此外还有两处潜在问题:

  • 直接修改ref.watch返回的列表对象,违反Riverpod状态不可变的设计规范
  • UI层重复构建相同加载状态代码,且存在冗余的帖子存在判断逻辑
修复方案

1. 处理.first的空安全问题

将.first替换为带默认值的.firstWhere,避免找不到元素时抛出异常:

// 替换原有的newPost获取逻辑
final targetPost = post.firstWhere(
  (element) => element.id == postId,
  orElse: () => PostModel(id: "", text: "", uid: ""), // 定义空PostModel作为默认值
);

// 确认找到有效元素后再执行更新
if (targetPost.id.isNotEmpty) {
  final postIndex = post.indexOf(targetPost);
  post.removeWhere((element) => element.id == postId);
  final updatedPost = PostModel.fromMap(data.payload);
  post.insert(postIndex, updatedPost);
}

2. 移到Provider内部处理状态更新

Riverpod的状态更新应该在Provider内部完成,避免UI层直接修改状态:

// 示例StateNotifierProvider结构
final getRepliesToPostProvider = StateNotifierProvider.family<RepliesNotifier, List<PostModel>, PostModel>((ref, postModel) {
  return RepliesNotifier(ref, postModel);
});

class RepliesNotifier extends StateNotifier<List<PostModel>> {
  final Ref ref;
  final PostModel postModel;

  RepliesNotifier(this.ref, this.postModel) : super([]) {
    fetchReplies();
    listenToUpdates();
  }

  // 从Appwrite获取评论列表
  void fetchReplies() async {
    final replies = await // 你的Appwrite请求逻辑
    state = replies;
  }

  // 监听Appwrite实时事件
  void listenToUpdates() {
    ref.listen(getLatestPostProvider, (previous, next) {
      if (next.events.contains('databases.*.collections.${Common.postCollection}.documents.*.update')) {
        final startingIndex = next.events[0].lastIndexOf('documents.');
        final endIndex = next.events[0].lastIndexOf('.update');
        final postId = next.events[0].substring(startingIndex + 10, endIndex);
        
        // 通过生成新列表更新状态
        state = state.map((post) {
          return post.id == postId ? PostModel.fromMap(next.payload) : post;
        }).toList();
      } else if (next.events.contains('databases.*.collections.${Common.postCollection}.documents.*.create')) {
        state = [PostModel.fromMap(next.payload), ...state];
      }
    });
  }
}

3. 简化UI层逻辑

抽离评论项Widget,移除冗余代码:

// 抽离评论项组件
class CommentItem extends StatelessWidget {
  final PostModel post;
  final String uid;

  const CommentItem({super.key, required this.post, required this.uid});

  @override
  Widget build(BuildContext context) {
    return ref.watch(userDetailsProvider(uid)).when(
      data: (user) {
        return Padding(
          padding: const EdgeInsets.only(left: 8.0, top: 2, right: 8, bottom: 2),
          child: Row(
            children: [
              CircularProfileAvatar(
                user.profile,
                radius: 20,
                backgroundColor: Colors.transparent,
                borderWidth: 2,
                initialsText: const Text("AD", style: TextStyle(color: Colors.white)),
                borderColor: Colors.white,
                elevation: 5.0,
                foregroundColor: Colors.brown.withOpacity(0.5),
                cacheImage: true,
                imageFit: BoxFit.cover,
                onTap: () => print('adil'),
                showInitialTextAbovePicture: false,
              ),
              const SizedBox(width: 15),
              Expanded(child: Text(post.text)),
            ],
          ),
        );
      },
      error: (_, __) => const SizedBox.shrink(),
      loading: () => const SizedBox.shrink(),
    );
  }
}

// 主UI部分
ref.watch(getRepliesToPostProvider(widget.postModel!)).when(
  data: (posts) {
    return Expanded(
      child: ListView.builder(
        padding: EdgeInsets.zero,
        shrinkWrap: true,
        scrollDirection: Axis.vertical,
        physics: const ClampingScrollPhysics(),
        itemCount: posts.length,
        itemBuilder: (_, index) => CommentItem(
          post: posts[index],
          uid: widget.postModel!.uid,
        ),
      ),
    );
  },
  error: (error, stk) => ErrorText(error: error.toString()),
  loading: () => const Loader(),
);

内容的提问来源于stack exchange,提问作者Zionnite

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 09:44:52