Flutter Bloc State not updating when using copywith
Asked Answered
N

3

6

Outline:

  1. Navigate to profile page
  2. Pass in user id to fetch doc from firestore
  3. Pass the retrieved data to state.copyWith(data:data)
  4. yield a successful status and present ui

My problem is that when i am using state.copyWith(data:data) the state is not being updated even though the data 100% exists as I can print it in console.

Code:

UI: 
class ProfileView extends StatelessWidget {
  final String uid;

  final UserRepository userRepository = UserRepository();

  ProfileView({required this.uid});

  @override
  Widget build(BuildContext context) {
    // Init repository
    return BlocProvider<ProfileBloc>(
        create: (context) => ProfileBloc(
            // Should be able to pull user repo from context
            userRepository: UserRepository(),
            isCurrentUser: UserRepository().isCurrentUser(uid))
          ..add(InitializeProfile(uid: uid)),
        // Get User Doc
        child: _profileView(context));
  }

  Widget _profileView(context) {
    return BlocListener<ProfileBloc, ProfileState>(
      listener: (context, state) {
        if (state.imageSourceActionSheetIsVisible) {
          _showImageSourceActionSheet(context);
        }
        final loadingStatus = state.loadingStatus;
        if (loadingStatus is LoadingFailed) {
          showSnackBar(context, state.loadingStatus.exception.toString());
        }
        if (loadingStatus is LoadingInProgress) {
          LoadingView();
        }
        if (loadingStatus is LoadingFailed) {
          LoadingFailedView(exception: loadingStatus.exception.toString());
        }
      },
      child: Scaffold(
        appBar: _appBar(),
        body: _profilePage(),
        bottomNavigationBar: bottomNavbar(context),
      ),
    );
  }



BLoC:
class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
  final bool isCurrentUser;
  final UserRepository userRepository;
  final _imagePicker = ImagePicker();

  ProfileBloc({
    required this.isCurrentUser,
    required this.userRepository,
  }) : super(ProfileState(
            isCurrentUser: isCurrentUser, loadingStatus: LoadingInProgress()));

  @override
  Stream<ProfileState> mapEventToState(
    ProfileEvent event,
  ) async* {
    if (event is InitializeProfile) {
      yield* _initProfile(uid: event.uid);
    }
  }

  Stream<ProfileState> _initProfile({required String uid}) async* {
    // Loading View
    yield state.copyWith(loadingStatus: LoadingInProgress());
    // Fetch profile data
    try {
      final snapshot = await userRepository.getUserDoc(uid);
      if (snapshot.exists) {
        final data = snapshot.data();
        print(data['fullName']);

        yield state.copyWith(
          fullName: data["fullName"].toString(),

        );
        print(state.fullName);
        yield state.copyWith(loadingStatus: LoadingSuccess());
      }
 
      else {
        yield state.copyWith(
            loadingStatus: LoadingFailed("Profile Data Not present :("));
      }
    } catch (e) {
      print(e);
    }
  }


State:
part of 'profile_bloc.dart';

class ProfileState {
  final bool isCurrentUser;
  final String? fullName;
  final LoadingStatus loadingStatus;
  bool imageSourceActionSheetIsVisible;

  ProfileState({
    required bool isCurrentUser,
    this.fullName,
    this.loadingStatus = const InitialLoadingStatus(),
    imageSourceActionSheetIsVisible = false,
  })  : this.isCurrentUser = isCurrentUser,
        this.imageSourceActionSheetIsVisible = imageSourceActionSheetIsVisible;

  ProfileState copyWith({
    bool? isCurrentUser,
    String? fullName,
    LoadingStatus? loadingStatus,
    bool? imageSourceActionSheetIsVisible,

  }) {
    print('State $fullName');
    print('State ${this.fullName}');
    return ProfileState(
      isCurrentUser: this.isCurrentUser,
      fullName: fullName ?? this.fullName,
      loadingStatus: loadingStatus ?? this.loadingStatus,
      imageSourceActionSheetIsVisible: imageSourceActionSheetIsVisible ??
          this.imageSourceActionSheetIsVisible,
    );
  }
}


Newly updated Code implementing Bloc builder as opposed to listener as suggested

  Widget _profileView(context) {
    return BlocBuilder<ProfileBloc, ProfileState>(builder: (context, state) {
      final loadingStatus = state.loadingStatus;
      if (loadingStatus is LoadingInProgress) {
        return LoadingView();
      }
      if (loadingStatus is LoadingFailed) {
        return LoadingFailedView(exception: loadingStatus.exception.toString());
      }
      if (loadingStatus is LoadingSuccess) {
        return Scaffold(
          appBar: _appBar(),
          body: _profilePage(),
          bottomNavigationBar: bottomNavbar(context),
        );
      }
      return LoadingView();
    });
  }

This code remains stuck on loading screen, when i print out the various states it is registered as an event however the state prints out null after using copy with

Thanks for the help

Natatory answered 11/5, 2021 at 16:13 Comment(0)
G
7

It's because you need to add an operator to compare your ProfileState objects. As it is, your event mapper yields ProfileState objects, but all those are considered as being equals so the UI doesn't update. A good pratice is to make your state implement Equatable, so you just have to define the props that will be used to compare the objects.

Gagman answered 11/5, 2021 at 20:24 Comment(4)
Thank you for your response! However when I print out what the state is it returns null even after directly using copy with, this suggests that the state is not being updated no? Once again many thanks for your response. I have edited my state and event class to extend equatable however it has no effect and the state still return null even after just calling copywithNatatory
I think it would in fact be simpler and cleaner to have at least 2 states, ProfileStateInitial and ProfileStateSuccess (and maybe ProfileStateFailure), rather than in fact using a property of the state to check... the state.Gagman
This worked for me thank you. It appears this approach is better. For future future people divide each possible state into its own separate class that extends the base class rather than modifying the base class and checking the modifications. Thank youNatatory
sometimes it also helps checking whether one has changed the state parameters and forgot to change this in get props, tooQuimby
N
5

From the bloc documentation, I think this was where my error was.

When we yield a state in the private mapEventToState handlers, we are always yielding a new state instead of mutating the state. This is because every time we yield, bloc will compare the state to the nextState and will only trigger a state change (transition) if the two states are not equal. If we just mutate and yield the same instance of state, then state == nextState would evaluate to true and no state change would occur.

Natatory answered 14/5, 2021 at 14:47 Comment(0)
S
3

it looks like you have messed up concepts in this code. Your Bloc logic is fine, the problem sits in the UI layer.

My observations:

  1. BlocListener does not update your UI (it is not designed for it, rather for single-shot operations, like navigation/dialog displaying). Consider using BlocBuilder instead.
  2. If you would really need BlocListener to trigger your UI state change, please consider converting your Widget to StatefulWidget and work your way up with setState like this:

class MyWidget extends StatefulWidget {
  ... 
}

class MyWidgetState extends State<MyWidget> {

  Widget _currentWidget; // use it on your view hierarchy, remember to initialize with default Widget!

  ...


  @override
  Widget build(BuildContext context) {
    // Init repository
    return BlocProvider<ProfileBloc>(
        create: (context) => ProfileBloc(
            // Should be able to pull user repo from context
            userRepository: UserRepository(),
            isCurrentUser: UserRepository().isCurrentUser(uid))
          ..add(InitializeProfile(uid: uid)),
        // Get User Doc
        child: _profileView(context));
  }

  Widget _profileView(context) {
    return BlocListener<ProfileBloc, ProfileState>(
      listener: (context, state) {
        if (state.imageSourceActionSheetIsVisible) {
          _showImageSourceActionSheet(context);
        }
        final loadingStatus = state.loadingStatus;
        if (loadingStatus is LoadingFailed) {
          showSnackBar(context, state.loadingStatus.exception.toString());
        }
        if (loadingStatus is LoadingInProgress) {
            /// HERE'S THE CHANGE:
            setState(() {
                _currentWidget = LoadingWidget();
            });
        }
        if (loadingStatus is LoadingFailed) {
            /// HERE'S THE CHANGE:
            setState(() {
                _currentWidget = LoadingFailedView(exception: loadingStatus.exception.toString());
            });
        }
      },
      child: Scaffold(
        appBar: _appBar(),
        body: _profilePage(),
        bottomNavigationBar: bottomNavbar(context),
      ),
    );
  }

Stipitate answered 11/5, 2021 at 16:45 Comment(2)
I suggest to use BlocBuilder as he says.Yawl
Thanks for the response, sadly simply changing to a bloc builder did not solve the issue I took the recommendation of the post below which fixed the problem, thank you kindly for your inputNatatory

© 2022 - 2024 — McMap. All rights reserved.