Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
DanMossa opened this issue May 18, 2024 · 1 comment
Closed

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

DanMossa opened this issue May 18, 2024 · 1 comment
Labels
question Further information is requested

Comments

@DanMossa
Copy link

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

Bloc.mp4

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

@DanMossa DanMossa added the bug Something isn't working label May 18, 2024
@DanMossa
Copy link
Author

DanMossa commented May 18, 2024

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?

@felangel felangel added question Further information is requested and removed bug Something isn't working labels May 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants