6

To preface, I am completely new to riverpod so my apologies if I get some terminology wrong.

I have an edit feature that I'm implementing with riverpod that I'm migrating over to from flutter_blocs. Basically, with the bloc implementation, I am fetching my data from my server and when the widget builds, the BlocConsumer will emit an event to set my data from the server into my bloc state so that I can display and edit it in a TextInput.

Bloc implementation:

BlocConsumer<JournalBloc, JournalState>(
  bloc: BlocProvider.of<JournalBloc>(context)
    ..add(
      SetModelState(
        title: journalEntry.title,
        rating: journalEntry.rating.toDouble(),
      ),
    ),
  builder: (context, state) {
    return Column(
      children: [
        TextInput(
          labelText: 'label',
          value: state.editEntryTitle.value,
          onChanged: (title) => context.read<JournalBloc>().add(EditJournalEntryTitleChanged(title: title))
        )
      ],
    );
  }

Now with Riverpod, where I'm stuck on is I don't know how to set my values from my server into my state when the widget renders. I have my controller, and I have my providers.

My controller:

class EditJournalEntryController extends StateNotifier<EditJournalEntryState> {
  final JournalService journalService;

  EditJournalEntryController({required this.journalService})
      : super(EditJournalEntryState());

  void setSelectedJournalType(JournalType journalType) {
    state = state.copyWith(selectedJournalType: journalType);
  }

  void setRating(int rating) {
    state = state.copyWith(rating: rating);
  }
}

final editJournalEntryController =
    StateNotifierProvider<EditJournalEntryController, EditJournalEntryState>((ref) {
  final journalService = ref.watch(journalServiceProvider);
  return EditJournalEntryController(journalService: journalService);
});

My state:

class EditJournalEntryState implements Equatable {
  final AsyncValue<void> value;
  final JournalType? selectedJournalType;
  final int? rating;

  bool get isLoading => value.isLoading;

  EditJournalEntryState({
    this.selectedJournalType,
    this.rating,
    this.value = const AsyncValue.data(null),
  });

  EditJournalEntryState copyWith({
    MealType? selectedJournalType,
    int? rating,
    AsyncValue? value,
  }) {
    return EditJournalEntryState(
      selectedJournalType: selectedJournalType ?? this.selectedJournalType,
      rating: rating ?? this.rating,
      value: value ?? this.value,
    );
  }

  @override
  List<Object?> get props => [selectedJournalType, rating, value];

  @override
  bool? get stringify => true;
}

What I have tried

In my UI, I am referencing my controller and then using my notifier to set my state:

class EditJournal extends StatelessWidget {
  const EditFoodJournal({
    Key? key,
    required this.journalEntry, // coming from server in a few parents above
  }) : super(key: key);

  final JournalEntry journalEntry;

  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: ((context, ref, child) {
        final state = ref.watch(editJournalEntryController);
        final notifier = ref.read(editJournalEntryController.notifier);
        
        notifier.setSelectedJournalType(journalEntry.type)
        notifier.setRating(journalEntry.rating)

But for obvious reasons I get this error:

At least listener of the StateNotifier Instance of 'EditFoodJournalEntryController' threw an exception
when the notifier tried to update its state.

The exceptions thrown are:

Tried to modify a provider while the widget tree was building.
If you are encountering this error, chances are you tried to modify a provider
in a widget life-cycle, such as but not limited to:
- build
- initState
- dispose
- didUpdateWidget
- didChangeDepedencies

Modifying a provider inside those life-cycles is not allowed, as it could
lead to an inconsistent UI state. For example, two widgets could listen to the
same provider, but incorrectly receive different states.


To fix this problem, you have one of two solutions:
- (preferred) Move the logic for modifying your provider outside of a widget
  life-cycle. For example, maybe you could update your provider inside a button's
  onPressed instead.

- Delay your modification, such as by encasuplating the modification
  in a `Future(() {...})`.
  This will perform your upddate after the widget tree is done building.

Ideally I want to render that value from state and not from the variable I have.

I feel like I've ran into a wall. Is there an ideal way of handling with this?

nyphur
  • 2,404
  • 2
  • 24
  • 48
  • 1
    That's the thing: You're not supposed to do that. The question in itself is "flawed" since the error is there purposefully to prevent you from doing this, as the error explains. You should find a different solution to your real problem which does not involve modifying providers inside widget life-cycles – Rémi Rousselet Dec 23 '22 at 17:10
  • 1
    I... know? I already said `But for obvious reasons I get this error`. I've tried and failed. I want to try a different solution, but being new to Riverpod, I don't know how to and I can't find an answer which is why I'm asking stackoverflow ... – nyphur Dec 23 '22 at 17:33
  • 1
    The problem is the way you formulated your question. There's no way to give you the correct answer, since you asked your question around "How to do what this error doesn't want me to do?". You should tell us what you're truly trying to do, instead of forcibly wanting to modify your provider inside `build`. Otherwise we can't really help you – Rémi Rousselet Dec 23 '22 at 23:23
  • 1
    The answer to this question was what I needed. I don't think there's anything wrong with my answer as there seems to be someone who read my question and didn't immediately dismiss it. Much respect to the library you've made but I expected better behavior from someone who presumably wants people to use their library. – nyphur Dec 27 '22 at 20:11
  • You might be misunderstanding my point. The question was already answered when I posted. So I simply explained how you fell for something known as the "x/y problem". Pointing that out could help you find the solution on your own next time. – Rémi Rousselet Dec 27 '22 at 21:39

1 Answers1

6

you are trying to modify a provider while the widget tree is building.

  final notifier = ref.read(editJournalEntryController.notifier);
    
    notifier.setSelectedJournalType(journalEntry.type)//here
    notifier.setRating(journalEntry.rating)//here

this will cause the issue, you need to initialize the editJournalEntryController with an initial state you can do it by .family provider modifier

Edit:

Something like this

final editJournalEntryController = StateNotifierProvider.family<
    EditJournalEntryController,
    EditJournalEntryState,
    JournalEntry>((ref, journalEntry) {
  final journalService = ref.watch(journalServiceProvider);
  return EditJournalEntryController(journalService: journalService,journalentry: journalEntry);
});

EditJournalEntryController will be

class EditJournalEntryController extends StateNotifier<EditJournalEntryState> {
 
  final JournalService journalService;
  final JournalEntry journalentry;
  EditJournalEntryController({required this.journalService,required this.journalentry})
      : super(EditJournalEntryState(selectedJournalType: journalentry.type,rating: journalentry.rating));

  void setSelectedJournalType(String journalType) {
    state = state.copyWith(selectedJournalType: journalType);
  }

  void setRating(int rating) {
    state = state.copyWith(rating: rating);
  }
}

will be called like

class EditFoodJournal extends StatelessWidget {
  const EditFoodJournal({
    Key? key,
    required this.journalEntry, // coming from server in a few parents above
  }) : super(key: key);

  final JournalEntry journalEntry;

  @override
  Widget build(BuildContext context) {
    return Consumer(builder: (context, ref, child) { 
      final state = ref.watch(editJournalEntryController(journalEntry));

      return Container();
    });
  }
}
Omar Mahmoud
  • 2,902
  • 1
  • 14
  • 22