4

The UI of my app is not updating when I know for a fact the state is changing. I am using the watch method from Riverpod to handle this, but the changes don't take effect unless I do a hot reload.

I have a class HabitListStateNotifier with methods to add/remove habits from the list:

class HabitListStateNotifier extends StateNotifier<List<Habit>> {
  HabitListStateNotifier(state) : super(state ?? []);

  void startAddNewHabit(BuildContext context) {
    showModalBottomSheet(
        context: context,
        builder: (_) {
          return NewHabit();
        });
  }

  void addNewHabit(String title) {
    final newHabit = Habit(title: title);
    state.add(newHabit);
  }

  void deleteHabit(String id) {
    state.removeWhere((habit) => habit.id == id);
  }
}

And here is the provider for this:

final habitsProvider = StateNotifierProvider(
  (ref) => HabitListStateNotifier(
    [
      Habit(title: 'Example Habit'),
    ],
  ),
);

Here is how the HabitList (the part of the UI not updating) is implemented:

class HabitList extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final habitList = watch(habitsProvider.state);

    /////////////not updating/////////////
    return ListView.builder(
      shrinkWrap: true,
      scrollDirection: Axis.vertical,
      itemBuilder: (context, index) {
        return HabitCard(
          habit: habitList[index],
        );
      },
      itemCount: habitList.length,
    );
    /////////////not updating/////////////
  }
}

And finally, the HabitCard (what the HabitList is comprised of):

class HabitCard extends StatelessWidget {
  final Habit habit;

  HabitCard({@required this.habit});

  @override
  Widget build(BuildContext context) {

    /////////////function in question/////////////
    void deleteHabit() {
      context.read(habitsProvider).deleteHabit(habit.id);
    }
    /////////////function in question/////////////

    return Card(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(devHeight * 0.03),
      ),
      color: Colors.grey[350],
      elevation: 3,
      child: Column(
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              HabitTitle(
                title: habit.title,
              ),
              Consumer(
                builder: (context, watch, child) => IconButton(
                  padding: EdgeInsets.all(8),
                  icon: Icon(Icons.delete),

                  /////////////function in question/////////////
                  onPressed: deleteHabit,
                  /////////////function in question/////////////
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

When I press the delete icon in a HabitCard, I know the Habit is being removed from the list, but the change is not reflecting in the UI. However, when I do a hot reload, it disappears as expected. What am I doing wrong here?

wolfeweeks
  • 355
  • 4
  • 12
  • 3
    just like you already figured it out in your comment, you need to update the state to force a refresh, in your case because you're just adding/removing a new object to the list it won't refresh, unless you do something like this state = state.add(newHabit); this also happens with ValueNotifier because its the same object being modified, not a new one – EdwynZN Dec 29 '20 at 00:25

3 Answers3

10

Since StateNotifierProvider state is immutable, you need to replace the data on CRUD operations by using state = - which is what will trigger UI updates as per docs.

Then assign new data using state = <newData>

Add and Delete Rewrite

You need to write your add & delete like this:

void addNewHabit(String title) {
    state = [ ...state, Habit(title: title)];
}

void deleteHabit(String id) {
  state = state.where((Habit habit) => habit.id != id).toList();
}

You need to exchange your old list with a new one for Riverpod to fire up.

Update code (for others)

Whilst your query does not need to update data, here is an example of how an update could be done to retain the original sort order of the list;

void updateHabit(Habit newHabit) {
    List<Habit> newState = [...state];
    int index = newState.indexWhere((habit) => habit.id == newHabit.id);
    newState[index] = newHabit;
    state = newState;
}
tommytucker7182
  • 213
  • 1
  • 5
  • 11
Florian Leeser
  • 590
  • 1
  • 6
  • 20
  • 1
    I was really starting to get confused with Riverpod after migration from Provider. Thank you so much. I see that since the state is immutable it has to replace itself with new data instead of just appending to it if it was a List. – Atish Shakya Jan 19 '22 at 14:22
5

I don't know if this is the right way to handle things, but I figured it out. In the HabitListStateNotifier, for addNewHabit and deleteHabit, I added this line of code: to the end: state = state; and it works exactly how I want it to.

wolfeweeks
  • 355
  • 4
  • 12
4

I used to solve this Issue by putting state = state; at the end, but now for some reason maybe flutter or Riverpod update, It doesn't work anymore.

Anyway this is how I managed to solve it now.

  void addNewHabit(String title) {
    List<Habit> _habits = [...state]; 
    final newHabit = Habit(title: title);
    _habits.add(newHabit);
    state = _habits ; 
  }

the explanation as I understand it. when state equals an object, in order to trigger the consumer to rebuild. state must equal a new value of that object, but updating variables of the state object itself will not work.

Hope this helps anyone.