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

Xamarin Forms中Firebase Realtime Database订阅无增改却触发

问题描述

在Xamarin Forms中使用Firebase Realtime Database时遇到订阅异常:数据提交功能正常,但订阅Insert/Update变更事件时,未对数据库执行插入或更新操作,订阅事件就会触发。以下是MVVM代码及Get方法实现,请问为何会出现这种情况?是否采用了错误的实现模式?

MVVM代码

public ICommand SaveCommand { get; private set; }
public ICommand LoadContentsCommand { get; private set; }
private readonly App CurrentApp;
private readonly IFirebaseDatabaseService firebaseDatabaseService;
public ICommand SaveCommand { get; private set; }
public ICommand LoadContentsCommand { get; private set; }
private readonly App CurrentApp;
private readonly IFirebaseDatabaseService firebaseDatabaseService;

public CommunityHomePageViewModel()
{
    firebaseDatabaseService = DependencyService.Get<IFirebaseDatabaseService>();
    Title = "Community";

    LoadContentsCommand = new Command(async () => await LoadContentsAsync());
    SaveCommand = new Command(async () => await SaveDataAsync());

    LoadContentsCommand.Execute(null);

    CurrentApp = Application.Current as App;

    SubscribeToChanges();
}

ObservableCollection<Post> _items;
public ObservableCollection<Post> Items
{
    get { return _items; }
    set { SetProperty(ref _items, value); }
}
async Task SaveDataAsync()
{
    try
    {
        await firebaseDatabaseService.PostAsync(new Post() { Comment = $"Random comment {new Random().Next(0, 100)}", UserKey = CurrentApp.CurrentUser.Id });
    }
    catch (Exception e)
    {
        Logger.LogException(e);
    }
}

async Task LoadContentsAsync()
{
    try
    {
        var items = await firebaseDatabaseService.GetItemsAsync(string.Empty);
        Items = new ObservableCollection<Post>(items);  
    }
    catch (Exception e)
    {
        Logger.LogException(e);
    }
}
// subscribe occurs just after the items are loaded without waiting for the insert/save to occur.
void SubscribeToChanges()
{
    firebaseDatabaseService.Client().Child(nameof(Post))
        .AsObservable<Post>()
        .Where(post => !Items.Contains(post.Object) && post.EventType == Firebase.Database.Streaming.FirebaseEventType.InsertOrUpdate)
        .Subscribe(obs =>
        {
            Console.WriteLine($"{obs.Key}");
            var post = obs.Object;
            switch (obs.EventType)
            {
                case Firebase.Database.Streaming.FirebaseEventType.InsertOrUpdate:
                    Items.Insert(0, post);
                    break;
            }
        });
} 

Get方法代码

async Task<IEnumerable<Post>> IFirebaseDatabaseService.GetItemsAsync(string id)
{
    if (!_firebaseAuth.IsSignedIn) return null;

    if (!string.IsNullOrEmpty(id))
    {
        return (await _firebaseClient
           .Child(typeof(Post).Name)
           .OrderByKey()
           .StartAt(id)
           .LimitToFirst(10)
           .OnceAsync<Post>())
           .Select(i => new Post
           {
                Comment = i.Object.Comment,
                UserKey = i.Object.UserKey
           });
    }

    return (await _firebaseClient
           .Child(typeof(Post).Name)
           .OrderByKey()
           .LimitToFirst(10)
           .OnceAsync<Post>())
           .Select(i => new Post
           {
               Comment = i.Object.Comment,
               UserKey = i.Object.UserKey
           });
}

问题原因

  1. Firebase订阅的初始快照机制:AsObservable<T>订阅时会立即返回当前节点的所有已有数据,触发InsertOrUpdate事件——这是Firebase实时流式订阅的默认行为,先同步现有数据,再监听后续变更。
  2. 对象引用比较失效:GetItemsAsync中通过Select创建了全新的Post实例,而订阅返回的post.Object是Firebase反序列化的新实例。由于Post未重写Equals和GetHashCode,Items.Contains用引用比较判断,导致已有数据被误判为“不存在”,触发插入逻辑。
  3. 异步加载与订阅时机冲突:构造函数中先执行异步的LoadContentsCommand,紧接着调用SubscribeToChanges。数据加载完成前订阅就已生效,此时Items未赋值,!Items.Contains(post.Object)始终为true,所有初始数据都会被重复插入。

修复方案

1. 重写Post类的Equals和GetHashCode

让Contains能通过内容(或唯一Key)判断对象相等:

public class Post
{
    public string Comment { get; set; }
    public string UserKey { get; set; }
    public string Key { get; set; } // 新增Firebase节点Key属性

    public override bool Equals(object obj)
    {
        if (obj is Post other)
        {
            return string.Equals(Key, other.Key); // 用节点Key作为唯一判断依据
        }
        return false;
    }

    public override int GetHashCode()
    {
        return Key?.GetHashCode() ?? base.GetHashCode();
    }
}

2. 调整订阅时机,等数据加载完成后再订阅

修改构造函数,确保数据加载完再启动订阅:

public CommunityHomePageViewModel()
{
    firebaseDatabaseService = DependencyService.Get<IFirebaseDatabaseService>();
    Title = "Community";

    LoadContentsCommand = new Command(async () => 
    {
        await LoadContentsAsync();
        // 数据加载完成后再订阅变更
        SubscribeToChanges();
    });
    SaveCommand = new Command(async () => await SaveDataAsync());

    CurrentApp = Application.Current as App;

    LoadContentsCommand.Execute(null);
}

3. 优化GetItemsAsync,保留Firebase节点Key

获取数据时把节点Key赋值给Post,方便后续判断:

async Task<IEnumerable<Post>> IFirebaseDatabaseService.GetItemsAsync(string id)
{
    if (!_firebaseAuth.IsSignedIn) return null;

    var query = _firebaseClient.Child(typeof(Post).Name).OrderByKey();

    if (!string.IsNullOrEmpty(id))
    {
        query = query.StartAt(id).LimitToFirst(10);
    }
    else
    {
        query = query.LimitToFirst(10);
    }

    return (await query.OnceAsync<Post>())
           .Select(i => new Post
           {
               Key = i.Key, // 保存Firebase节点Key
               Comment = i.Object.Comment,
               UserKey = i.Object.UserKey
           });
}

4. 优化订阅逻辑,过滤初始数据(可选)

如果只需要监听后续变更,可通过记录已加载Key过滤初始快照:

void SubscribeToChanges()
{
    var existingKeys = Items.Select(p => p.Key).ToHashSet();

    firebaseDatabaseService.Client().Child(nameof(Post))
        .AsObservable<Post>()
        .Where(obs => 
            obs.EventType == Firebase.Database.Streaming.FirebaseEventType.InsertOrUpdate
            && !existingKeys.Contains(obs.Key))
        .Subscribe(obs =>
        {
            Console.WriteLine($"{obs.Key}");
            var post = obs.Object;
            post.Key = obs.Key;
            Items.Insert(0, post);
            existingKeys.Add(obs.Key);
        });
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 19:10:51