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

基于Riverpod(Future+Stream)的Appwrite实时数据查询优化问题

问题:优化Riverpod结合Appwrite的实时数据加载逻辑

背景

我在学习一份YouTube教程时,卡在了数据加载环节,觉得教程里的实现方案有优化空间,想找更简洁的写法。

目前从Appwrite获取数据的逻辑是教程作者推荐的:先通过FutureProvider(getTweetsProvider)拉取初始数据,再通过StreamProvider(getLatestTweetProvider)监听数据的实时变化。原代码如下:

Widget build(BuildContext context, WidgetRef ref) {
  return ref.watch(getTweetsProvider).when(
        data: (tweets) {
          return ref.watch(getLatestTweetProvider).when(
                data: (tweet) {
                  if (tweet.events.contains(
                      'databases.*.collections.${AppwriteConstants.tweetsCollection}.documents.*.create')) {
                    tweets.insert(0, TweetModel.fromMap(tweet.payload));
                  }
                  return ListView.builder(
                    itemCount: tweets.length,
                    itemBuilder: (context, index) {
                      final tweet = tweets.elementAt(index);
                      return TweetCard(tweet: tweet);
                    },
                  );
                },
                error: (error, stackTrace) =>
                    ErrorText(errorTxt: error.toString()),
                // 我不太喜欢这种写法...
                loading: () => ListView.builder(
                  itemCount: tweets.length,
                  itemBuilder: (context, index) {
                    final tweet = tweets.elementAt(index);
                    return TweetCard(tweet: tweet);
                  },
                ),
              );
        },
        error: (error, stackTrace) => ErrorText(errorTxt: error.toString()),
        loading: () => const Loader(),
      );
}

不满的点

这段代码里,必须在StreamProvider的loading分支里重复写一遍FutureProvider数据的渲染逻辑,非常冗余。

尝试过的思路

我想把ConsumerWidget改成ConsumerStatefulWidget,在initState里调用FutureProvider把初始数据存到本地变量(List tweets),只在build方法里监听StreamProvider的变化。但多次尝试都没成功。

之前在没有使用Provider的小型应用里,我实现过类似逻辑,代码如下:

List<Map<String, dynamic>> items = [];
RealtimeSubscription? subscription;
late final Client client;
final databaseID = '...';
final itemsCollection = '...';
late final Databases database;

void loadItems() async {
  try {
    final res = await database.listDocuments(
        collectionId: itemsCollection, databaseId: databaseID);
    setState(() {
      items = List<Document>.from(res.documents).map((e) => e.data).toList();
    });
  } on AppwriteException catch (e) {
    debugPrint(e.message);
  }
}

void subscribe() {
  final realtime = Realtime(client);

  subscription = realtime.subscribe(
      ['databases.$databaseID.collections.$itemsCollection.documents']);

  subscription!.stream.listen((data) {
    if (data.payload.isNotEmpty) {
      if (data.events
          .contains('databases.*.collections.*.documents.*.create')) {
        var item = data.payload;
        items.add(item);
        setState(() {});
      } else if (data.events
          .contains('databases.*.collections.*.documents.*.delete')) {
        var item = data.payload;
        items.removeWhere((it) => it['\$id'] == item['\$id']);
        setState(() {});
      }
    }
  });
}

@override
void initState() {
  client = Client();

  client
      .setEndpoint('...')
      .setProject('...')
      .setSelfSigned(status: true);
  database = Databases(client);
  loadItems();
  subscribe();
  super.initState();
}

@override
void dispose() {
  subscription?.close();
  super.dispose();
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('Appwrite Realtime Demo'),
    ),
    body: ListView(children: [
      ...items.map((item) => ListTile(
            title: Text(item['name']),
            trailing: IconButton(
              icon: const Icon(Icons.delete),
              onPressed: () => _removeItem(item['\$id']),
            ),
          )),
    ]),
    floatingActionButton: FloatingActionButton(
      child: const Icon(Icons.add),
      onPressed: () {
        // dialog to add new item
        showDialog(
          context: context,
          builder: (context) => AlertDialog(
            title: const Text('Add new item'),
            content: TextField(
              controller: _nameController,
            ),
            actions: [
              TextButton(
                child: const Text('Cancel'),
                onPressed: () => Navigator.of(context).pop(),
              ),
              TextButton(
                child: const Text('Add'),
                onPressed: () {
                  // add new item
                  final name = _nameController.text;
                  if (name.isNotEmpty) {
                    _nameController.clear();
                    _addItem(name);
                  }
                  Navigator.of(context).pop();
                },
              ),
            ],
          ),
        );
      },
    ),
  );
}

最终解决方案

后来我找到了更简洁的写法:用whenData替代第二个when,这样就不用重复处理StreamProvider的loading分支,代码更连贯清晰:

Widget build(BuildContext context, WidgetRef ref) {
  return ref.watch(getTweetsProvider).when(
        data: (tweets) {
          ref.watch(getLatestTweetProvider).whenData((tweet) {
            if (tweet.events.contains(
                'databases.*.collections.${AppwriteConstants.tweetsCollection}.documents.*.create')) {
              tweets.insert(0, TweetModel.fromMap(tweet.payload));
            }
          });
          return ListView.builder(
            itemCount: tweets.length,
            itemBuilder: (context, index) {
              final tweet = tweets.elementAt(index);
              return TweetCard(tweet: tweet);
            },
          );
        },
        error: (error, stackTrace) => ErrorText(errorTxt: error.toString()),
        loading: () => const Loader(),
      );
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 14:24:54