Search code examples
flutterdartriverpod

UI not updating in real-time after deleting an item using Riverpod StateProvider


I'm currently facing an issue with my Flutter app, where the UI is not updating in real-time after I delete an item from a list using Riverpod's StateProvider.

I have a Listview.Builder and using a StateProvider to manage it's state. The data is fetched from a SQLFlite database and displayed in the UI. I also have a Slidable for each item in the list, allowing the user to delete the item after swiping and clicking the button.

I have tried a few different approaches, including using StateNotifierProvider and StateProvider, but none of them seem to update the UI automatically after the item is deleted.

The item is deleted in both instances, however, the UI is not updated to reflect the change. Leaving the current screen and coming back shows that the item has been deleted from the db.

Here's the relevant code for the issue I'm currently facing:

final productListProvider = StateProvider<List<Product>>((ref) => []);


class ProductScreen extends ConsumerWidget with FilteredList {
   ProductScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final searchText = ref.watch(searchTextProvider);
    return Scaffold(
      floatingActionButton: createProductButton(context),
      appBar: AppBar(
        title: const Text("Tela de Produtos"),
      ),
        body: Column(
           children: [
                       searchProductField(ref),
                       const SizedBox(height: 16),
                       productList(context, ref, searchText),
      ],
    ));
  }


Widget productList(BuildContext context, WidgetRef ref, String searchText) {
    return Expanded(
      child: FutureBuilder<List<Product>>(
        future: productRepository.getAll(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }
          if (snapshot.hasError) {
            return const Center(
              child: Text("Erro ao buscar produtos"),
            );
          }

          final List<Product> productList = snapshot.data ?? [];
          ref.read(productListProvider).addAll(productList);

          return productListTile(productList ,ref);
        },
      ),
    );
  }
  
  Widget productListTile(List<Product> productList , WidgetRef ref){
    return ListView.builder(
        itemCount: productList.length,
        itemBuilder: (context, index) {
          final product = productList[index];
          return Slidable(
            endActionPane:  ActionPane(
              motion: const ScrollMotion(),
              children: [
                SlidableAction(
                  onPressed:(context) async{
                    final productList = ref.read(productListProvider);
                    productList.remove(product);
                    ref.read(productListProvider.notifier).update((state) => productList);
                    await productRepository.delete(product.id!);
                  },
                  backgroundColor: Colors.red,
                  foregroundColor: Colors.white,
                  icon: Icons.delete,
                  label: "Deletar",)
              ],
            ),
            child: ListTile(
              title: Text(product.name),
              subtitle: Text(product.brand ?? ''),
              trailing: Text(product.price.toStringAsFixed(2)),
            ),
          );
        });
  }
}

Solution

  • You're doing it wrong. First – the StateProvider is too simple for your case (that's why you have a hard time making it work). Second – Riverpod providers are not normally used along with the Flutter's built-in async builders (such as FutureBuilder and StreamBuilder), so don't try to mix them unnecessarily – just do all the operations on your product list within the provider itself.

    In Riverpod, there is a special type of provider designed for async operations on complex values – AsyncNotifierProvider. Moreover, there is already a good example in the docs that you can adapt for your case.

    class AsyncTodosNotifier extends AsyncNotifier<List<Todo>> {
      Future<List<Todo>> _fetchTodo() async {
        final json = await http.get('api/todos');
        final todos = jsonDecode(json) as List<Map<String, dynamic>>;
        return todos.map(Todo.fromJson).toList();
      }
    
      @override
      Future<List<Todo>> build() async {
        // Load initial todo list from the remote repository
        return _fetchTodo();
      }
    
      Future<void> addTodo(Todo todo) async {
        // Set the state to loading
        state = const AsyncValue.loading();
        // Add the new todo and reload the todo list from the remote repository
        state = await AsyncValue.guard(() async {
          await http.post('api/todos', todo.toJson());
          return _fetchTodo();
        });
      }
    
      ...
    
      // Let's mark a todo as completed
      Future<void> toggle(String todoId) async {
        state = const AsyncValue.loading();
        state = await AsyncValue.guard(() async {
          await http.patch(
          'api/todos/$todoId',
          <String, dynamic>{'completed': true},
          );
          return _fetchTodo();
        });
      }
    }
    
    // Finally, we are using NotifierProvider to allow the UI to interact with
    // our TodosNotifier class.
    final asyncTodosProvider =
        AsyncNotifierProvider<AsyncTodosNotifier, List<Todo>>(() {
      return AsyncTodosNotifier();
    });
    

    As you can see, all operations are neatly encapsulated within the provider's public methods. And this is how you will be using it (no FutureBuilder-s!):

    class TodoListView extends ConsumerWidget {
      const TodoListView({super.key});
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        // rebuild the widget when the todo list changes
        final asyncTodos = ref.watch(asyncTodosProvider);
    
        // Let's render the todos in a scrollable list view
        return asyncTodos.when(
          data: (todos) => ListView(
            children: [
              for (final todo in todos)
                CheckboxListTile(
                  value: todo.completed,
                  // When tapping on the todo, change its completed status
                  onChanged: (value) =>
                      ref.read(asyncTodosProvider.notifier).toggle(todo.id),
                  title: Text(todo.description),
                ),
            ],
          ),
          loading: () => const Center(
            child: CircularProgressIndicator(),
          ),
          error: (err, stack) => Text('Error: $err'),
        );
      }
    }
    

    Hope that helps