felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.76k stars 3.39k forks source link

Emit that occurs during init doesn't cause a rebuild #4171

Closed DanMossa closed 4 months ago

DanMossa commented 4 months ago

Description I have a BlocBuilder that needs to handle the different statuses of my Bloc's state. The issue is that the loading status is never called.

Here is part of my Bloc

import 'package:app/features/likes/data/liked_users_repo.dart';
import 'package:app/models/tables/user_model.dart';
import 'package:app/shared/constants/type_aliases.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'liked_users_event.dart';
part 'liked_users_state.dart';
part 'liked_users_bloc.freezed.dart';

class LikedUsersBloc extends Bloc<LikedUsersEvent, LikedUsersState> {
  LikedUsersBloc({required LikedUsersRepo likedUsersRepo})
      : _likedUsersRepo = likedUsersRepo,
        super(const LikedUsersState(
          status: LikedUsersStatus.initial,
          likedUsers: [],
          errorMessage: null,
        )) {
    on<_StreamRequested>(_onStreamRequested);
    on<_UserLikeToggled>(_onUserLikeToggled);
  }

  final LikedUsersRepo _likedUsersRepo;

  Future<void> _onStreamRequested(_StreamRequested event, Emitter<LikedUsersState> emit) async {
    emit(
      state.copyWith(status: LikedUsersStatus.loading, errorMessage: null),
    );

    await emit.forEach<List<UserModel>>(
      _likedUsersRepo.getLikedUsers(),
      onData: (List<UserModel> likedUsers) {
        return state.copyWith(
          status: LikedUsersStatus.success,
          likedUsers: likedUsers,
          errorMessage: null,
        );
      },
      onError: (Object error, StackTrace stackTrace) {
        return state.copyWith(
          status: LikedUsersStatus.failure,
          errorMessage: error.toString(),
        );
      },
    );
  }

And this is part of my Repo

class LikedUsersRepo {
  LikedUsersRepo() {
    unawaited(_init());
  }

  final _databaseClient= Database.client;

  /// Used to prevent sending duplicate notifications per sessions
  final Set<UserId> _likedUsersMemoryCache = {};

  final _likedUsersStreamController = BehaviorSubject<List<UserModel>>.seeded(const []);

  Stream<List<UserModel>> getLikedUsers() => _likedUsersStreamController.asBroadcastStream();

  Future<void> _init() async {
    try {
      final List<dynamic> res = await _databaseClient.rpc('get_liked_users');

      final List<UserModel> likedUsersFromDatabase = UserModel.fromJsons(res);

      Logger.info("getLikedUsers: ${res.length}");

      _likedUsersStreamController.add(likedUsersFromDatabase);

      _likedUsersMemoryCache.addAll(likedUsersFromDatabase.map((e) => e.userId));

      return;
    } catch (e, s) {
      Logger.error(
        'Unable to call rpc',
        exception: e,
        stack: s,
        location: 'getLikedUsers',
        data: {},
      );

      _likedUsersStreamController.add([]);

      return;
    }
  }

Here is part of my main.dart

    return MultiRepositoryProvider(
      providers: [
        RepositoryProvider(
          create: (context) => LikedUsersRepo(),
        ),
        RepositoryProvider(
          create: (context) => ProfileVisitRepo(),
        ),
      ],
      child: MultiBlocProvider(
        providers: [
          BlocProvider(
            create: (BuildContext context) {
              return LikedUsersBloc(
                likedUsersRepo: context.read<LikedUsersRepo>(),
              )..add(const LikedUsersEvent.streamRequested());
            },
          ),
          BlocProvider(
            create: (context) {
              return ProfileVisitBloc(
                profileVisitRepo: context.read<ProfileVisitRepo>(),
              )..add(const ProfileVisitEvent.streamRequested());
            },
          ),
        ],

Here is part of my UI

            BlocBuilder<LikedUsersBloc, LikedUsersState>(
              builder: (BuildContext context, LikedUsersState state) {
                print(state.status);
                switch (state.status) {
                  case LikedUsersStatus.initial:
                  case LikedUsersStatus.loading:
                    return const _LoadingShimmer();

                  case LikedUsersStatus.success:
                  case LikedUsersStatus.failure:
                    if (accountStore.user.paused == true) {
                      return ListView(
                        children: [
                          AllDoneTextButtonWidget(
                            "Your account is currently paused.",
                            buttonText: "Tap here to unpause",
                            onPressed: () {
                              Navigator.of(context, rootNavigator: true).push(
                                MaterialPageRoute(
                                  builder: (context) => const SettingsAccountView(),
                                ),
                              );
                            },
                          ),
                        ],
                      );
                    }

                    if (state.likedUsers.isEmpty) {
                      return const _EmptyPlaceholder("Like some profiles to see them here!");
                    }

                    return ListView.builder(
                      padding: const EdgeInsets.only(bottom: 24, top: 12),
                      itemCount: state.likedUsers.length,
                      itemBuilder: (context, index) {
                        final UserModel user = state.likedUsers.elementAt(index);

Expected Behavior

  1. The BlocBuilder lazily calls LikedUsersBloc. And the Constructor of LikedUsersBloc shows that the status is initial
  2. The event const LikedUsersEvent.streamRequested() is added.
  3. A new status of loading is emitted. (This part never happens)
  4. The _likedUsersRepo.getLikedUsers() is called.
  5. The repo does _init() and a value is added to the BehaviorSubject.
  6. The bloc's ondata is called and the status turns to success.

The issue is that I want to show the Shimmer while the status is loading, but I am not able to do so, since it goes from Initial to Success before it's actually added.

Video

https://github.com/felangel/bloc/assets/10294777/b9a1eef9-0531-4d2f-be47-52468061d48a

Notes I'm following this pattern: https://bloclibrary.dev/tutorials/flutter-todos/#localstoragetodosapi

DanMossa commented 4 months ago

Looking more into it, it seems like the fact that I'm seeding a value causes it to immediatly emit which causes onData to set the status to success.

I also found that if I keep seeded, but await a Future.delayed between emiting loading and the await emit.forEach line, loading is actually emitted.

I could just remove the seeding the behaviorsubject, but then I'll occasionally get the exception

ValueStream has no value. You should check ValueStream.hasValue before accessing ValueStream.value, or use ValueStream.valueOrNull instead

When I do something like final List<UserModel> likedUsers = [..._likedUsersStreamController.value];

and I think it has to do with a race condition then?

I'm just following the tutorial posted on the Bloc website, but changing it for my use case.

What's the best way to do this?