Search code examples
flutterdartflutter-providerriverpod

Why is RiverPod StreamProvider stuck in loading even though Stream.empty() should be returning when database is null


I'm using this great flutter/riverpod/firebase starter architecture. On my initial flutter screen, which imports top_level_providers.dart the loading parameter is all that is ever called.

In my accountStreamProvider when the databaseProvider returns null Stream.empty() is emitted, which according to the docs "This is a stream which does nothing except sending a done event when it's listened to.".

Why isn't the data parameter being called in the account.when section?

root_screen.dart

class AppStartupScreenRouter extends ConsumerWidget {
  const AppStartupScreenRouter({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final didCompleteOnboarding = watch(onboardingViewModelProvider.state);
    AsyncValue<Account> account = watch(accountStreamProvider);

    return account.when(
      data: (data) {
        if (data != null) {
          // evaluate the account info data and
          // return a screen widget
        } else if (didCompleteOnboarding) {
          // return a screen widget
        }

        return IntroScreen();
      },
      loading: () => LoadingScreen(),
      error: (err, stack) => EmptyContent(),
    );
  }
}

top_level_providers.dart

final firebaseAuthProvider =
    Provider<FirebaseAuth>((ref) => FirebaseAuth.instance);

final authStateChangesProvider = StreamProvider<User>(
    (ref) => ref.watch(firebaseAuthProvider).authStateChanges());

final databaseProvider = Provider<FirestoreDatabase>((ref) {
  final auth = ref.watch(authStateChangesProvider);

  if (auth.data?.value?.uid != null) {
    return FirestoreDatabase(uid: auth.data?.value?.uid);
  }

  return null;
});

final accountStreamProvider = StreamProvider<Account>((ref) {
  final db = ref.watch(databaseProvider);
  return db != null ? db.accountStream() : Stream.empty();
});

firestore_database.dart

class FirestoreDatabase {
  FirestoreDatabase({@required this.uid})
      : assert(
            uid != null, 'Cannot create FirestoreDatabase entry with null uid');
  final String uid;

  final _service = FirestoreService.instance;

  Future<void> setAccount(Account account) => _service.setData(
        path: FirestorePath.account(uid),
        data: account.toMap(),
      );

  Stream<Account> accountStream() => _service.documentStream(
        path: FirestorePath.account(uid),
        builder: (data, documentId) => Account.fromMap(data, uid),
      );

  Future<void> deleteAccount(Account account) =>
      _service.deleteData(path: FirestorePath.account(uid));
}

account.dart

@immutable
class Account extends Equatable {
  const Account({
    @required this.id,
    @required this.firstName,
    @required this.lastName,
  });

  final String id;
  final String firstName;
  final String lastName;

  @override
  List<Object> get props => [
        id,
        firstName,
        lastName,
      ];

  @override
  bool get stringify => true;

  factory Account.fromMap(Map<String, dynamic> data, String documentId) {
    if (data == null) {
      return null;
    }

    final firstName = data['firstName'] as String;
    final lastName = data['lastName'] as String;

    if (firstName == null || lastName == null) {
      return null;
    }

    return Account(
      id: documentId,
      firstName: firstName,
      lastName: lastName,
    );
  }

  Map<String, dynamic> toMap() {
    return {
      'firstName': firstName,
      'lastName': lastName,
    };
  }
}

Solution

  • Stream.empty() never contains any data. If this is what your StreamProvider returns, the data callback will not be called. Most likely you'll get loading only.

    In order to get a data callback, your stream should return at least one element (whether it's null or not).