firebase / flutterfire

πŸ”₯ A collection of Firebase plugins for Flutter apps.
https://firebase.google.com/docs/flutter/setup
BSD 3-Clause "New" or "Revised" License
8.68k stars 3.97k forks source link

πŸ› [firebase_database] get() return incorrect results #10145

Open bonnybun opened 1 year ago

bonnybun commented 1 year ago

Bug report

This report relate to issue #9067 If I subscribe to a database reference, call get() function on a child reference will return wrong value in iOS. On Android it works as expected. A workaround exist using once() but it hasn't exactly the same behavior as get().

Steps to reproduce

  1. Create new firebase project and put this in the root database

    {
    "organizations": [
    {
      "city": "Lyon",
      "country": "France",
      "data": [
        "a",
        "b",
        "c",
        "d",
        "e",
        "f",
        "g",
        "h"
      ],
      "foo": "bar",
      "id": 0,
      "name": "my organization"
    }
    ]
    }
  2. Create new Flutter project and configure it with your firebase project

  3. Paste this in your main.dart

import 'dart:async';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';
import 'package:test_firebase/firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Object? _subscriptionResponse;
  Object? _response;
  final DatabaseReference _organizationRef =
      FirebaseDatabase.instance.ref("organizations/0");
  final DatabaseReference _dataRef =
      FirebaseDatabase.instance.ref("organizations/0/data");

  StreamSubscription<DatabaseEvent>? _subscription;

  @override
  void dispose() {
    super.dispose();
    _subscription?.cancel();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              onPressed: _toggleSubscription,
              child: Text(
                  "${_subscription == null ? "Subscribe" : "Unsubscribe"}  '${_organizationRef.path}'"),
            ),
            Text(
              'Subscription Response : $_subscriptionResponse',
              maxLines: 30,
            ),
            ElevatedButton(
              onPressed: _getData,
              child: Text("Get   '${_dataRef.path}'"),
            ),
            ElevatedButton(
              onPressed: _onceData,
              child: Text("Once  '${_dataRef.path}'"),
            ),
            Text(
              '$_response',
              maxLines: 30,
            ),
          ],
        ),
      ),
    );
  }

  void _onOrganizationsUpdated(DatabaseEvent event) {
    setState(() {
      _subscriptionResponse = event.snapshot.value;
    });
  }

  void _toggleSubscription() {
    if (_subscription == null) {
      setState(() {
        _subscription =
            _organizationRef.onValue.listen(_onOrganizationsUpdated);
      });
    } else {
      _subscription?.cancel();
      setState(() {
        _response = null;
        _subscriptionResponse = null;
        _subscription = null;
      });
    }
  }

  void _getData() async {
    final value = (await _dataRef.get()).value;
    setState(() {
      _response = "get : $value";
    });
  }

  void _onceData() async {
    final value = (await _dataRef.once()).snapshot.value;
    setState(() {
      _response = "once : $value";
    });
  }
}
  1. Run on iOS device
  2. Press Get and Once button without subscribe
  3. Now Press Subscribe button, and press again Get and Once Button

Expected behavior

I want to get the value of the key 'data'. The expected value is ["a","b","c","d","e","f","g","h"] Calling get() or once() should return the same value.

But if a suscription is made on the parent, the get() function will return the suscription's value. And once() return the correct value

This problem doesn't exist when I run on an android device

Sample project

sample_project


Flutter doctor

Run flutter doctor and paste the output below:

Click To Expand ``` Doctor summary (to see all details, run flutter doctor -v): [βœ“] Flutter (Channel stable, 3.3.10, on macOS 13.1 22C65 darwin-arm, locale fr-FR) [βœ“] Android toolchain - develop for Android devices (Android SDK version 33.0.1) [βœ“] Xcode - develop for iOS and macOS (Xcode 14.2) [βœ“] Chrome - develop for the web [βœ“] Android Studio (version 2021.3) [βœ“] VS Code (version 1.74.1) [βœ“] Connected device (4 available) [βœ“] HTTP Host Availability ```

Flutter dependencies

Run flutter pub deps -- --style=compact and paste the output below:

Click To Expand ``` Dart SDK 2.18.6 Flutter SDK 3.3.10 test_firebase 1.0.0+1 dependencies: - cupertino_icons 1.0.5 - firebase_core 2.4.0 [firebase_core_platform_interface firebase_core_web flutter meta] - firebase_database 10.0.7 [firebase_core firebase_core_platform_interface firebase_database_platform_interface firebase_database_web flutter] - flutter 0.0.0 [characters collection material_color_utilities meta vector_math sky_engine] dev dependencies: - flutter_lints 2.0.1 [lints] - flutter_test 0.0.0 [flutter test_api path fake_async clock stack_trace vector_math async boolean_selector characters collection matcher material_color_utilities meta source_span stream_channel string_scanner term_glyph] transitive dependencies: - _flutterfire_internals 1.0.10 [cloud_firestore_platform_interface cloud_firestore_web collection firebase_core firebase_core_platform_interface flutter meta] - async 2.9.0 [collection meta] - boolean_selector 2.1.0 [source_span string_scanner] - characters 1.2.1 - clock 1.1.1 - cloud_firestore_platform_interface 5.9.2 [_flutterfire_internals collection firebase_core flutter meta plugin_platform_interface] - cloud_firestore_web 3.1.1 [_flutterfire_internals cloud_firestore_platform_interface collection firebase_core firebase_core_web flutter flutter_web_plugins js] - collection 1.16.0 - fake_async 1.3.1 [clock collection] - firebase_core_platform_interface 4.5.2 [collection flutter flutter_test meta plugin_platform_interface] - firebase_core_web 2.0.2 [firebase_core_platform_interface flutter flutter_web_plugins js meta] - firebase_database_platform_interface 0.2.2+15 [_flutterfire_internals collection firebase_core flutter meta plugin_platform_interface] - firebase_database_web 0.2.1+17 [firebase_core firebase_core_web firebase_database_platform_interface flutter flutter_web_plugins js] - flutter_web_plugins 0.0.0 [flutter js characters collection material_color_utilities meta vector_math] - js 0.6.4 - lints 2.0.1 - matcher 0.12.12 [stack_trace] - material_color_utilities 0.1.5 - meta 1.8.0 - path 1.8.2 - plugin_platform_interface 2.1.3 [meta] - sky_engine 0.0.99 - source_span 1.9.0 [collection path term_glyph] - stack_trace 1.10.0 [path] - stream_channel 2.1.0 [async] - string_scanner 1.1.1 [source_span] - term_glyph 1.2.1 - test_api 0.4.12 [async boolean_selector collection meta source_span stack_trace stream_channel string_scanner term_glyph matcher] - vector_math 2.1.2 ```

darshankawar commented 1 year ago

Thanks for the detailed report @bonnybun I created the sample db and ran the code sample, but have been getting null upon following steps to replicate. Did I miss something to properly replicate ?

Screenshot 2022-12-20 at 6 19 51 PM
bonnybun commented 1 year ago

Did you set the rules in your db @darshankawar ?

{
  "rules": {
    ".read": "true",
    ".write": "true"
  }
}
darshankawar commented 1 year ago

Yes, I have the rules properly set. I figured that the database instance reference isn't correct as mentioned in your code sample. Instead of organizations/0 and organizations/0/data, it should be organizations/data and organizations/data/0. After fixing this, I was able to run and see the behavior as below:

https://user-images.githubusercontent.com/67046386/208903212-51dd1c24-2fac-4804-a2bf-4854224eb42b.mov

According to my findings, tapping get gives ["a","b","c","d","e","f","g","h"], but with once, it gives only a. Is this correct interpretation of your issue ? Because in your expected results, you mentioned that The expected value is ["a","b","c","d","e","f","g","h"]

bonnybun commented 1 year ago

Your problem is that "organizations" should be an array, not an object. It's like a list of companies. Instead, you should to have something like this :

Capture d’écran 2022-12-21 aΜ€ 14 38 40

You can also import this json in your root database example_root_firebase.json.zip

Get() and Once() must always return the same value : ["a","b","c","d","e","f","g","h"]

but after a subscription on a parent, the result of Get() becomes : {"city":"Lyon","country":"France","data":["a","b","c","d","e","f","g","h"],"foo":"bar","id":0,"name":"my organization"}

darshankawar commented 1 year ago

I see, thanks. I updated the db to have the root node to be an array and then ran the code sample again which gives me same results as you mentioned above:

{"city":"Lyon","country":"France","data":["a","b","c","d","e","f","g","h"],"foo":"bar","id":0,"name":"my organization"}

JoolsMcFly commented 1 year ago

Hi @darshankawar I've added a comment on @Lyokone 's PR : https://github.com/firebase/flutterfire/pull/10182#issuecomment-1403825724

Is there anything else we can do on our end regarding this issue? Thanks.

darshankawar commented 1 year ago

Is there anything else we can do on our end regarding this issue?

I think Guillaume should be aware of your comment in that PR and since you have tagged him in it, he'll probably let you know if there's anything else required from your end.

JoolsMcFly commented 1 year ago

Sounds good, we'll wait :)

mrares commented 10 months ago

Hello, I don't know why OP hasn't back but I am having the same exact issue.

In my case I'm attaching a listener using

ref.child("a/path/to/data").onValue.listen((event) {
    /// Then here I execute a callback function that redirects the user to another page
 }

then a subsequent read from a child path such as: ref.child("a/path/to/data/child/node").get()

Will return the wrong data, at the parent path "a/path/to/data"

If I run the following:

StreamSubscription<DatabaseEvent>? sub;
sub = ref.child("a/path/to/data").onValue.listen((event) {
    sub?.cancel();
    /// Then here I execute a callback function that redirects the user to another page
 }

then a subsequent read from a child path such as: ref.child("a/path/to/data/child/node").get()

will return the correct data at the parent path "a/path/to/data/child/node"

This is the behaviour running on an iPhone 15 Pro simulator with:

environment:
  sdk: ">=3.1.1 <4.0.0"

dependencies:
  firebase_database: ^10.2.7
russellwheatley commented 9 months ago

Thank you for the report, I have opened an issue on the Firebase iOS SDK: https://github.com/firebase/firebase-ios-sdk/issues/12225

Paitomax commented 8 months ago

The same problem happens to me when using _organizationRef.keepSynced(true) In fact, the SET method also writes to the wrong path.

Removing keepSynced the problem does not occur.