wolfeweeks
wolfeweeks

Reputation: 445

Flutter/Riverpod: UI not updating on state change within a StateNotifierProvider

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?

Upvotes: 9

Views: 14412

Answers (3)

Florian Leeser
Florian Leeser

Reputation: 790

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;
}

Upvotes: 17

Mo&#39;men Amin
Mo&#39;men Amin

Reputation: 73

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.

Upvotes: 4

wolfeweeks
wolfeweeks

Reputation: 445

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.

Upvotes: 5

Related Questions