skip to Main Content

I want to start a function that returns a future when MyChangeNotifier is changed.

Using a future builder widget and calling the function directly can work but this will also call the function when the widget is rebuilt, resulting in unnecessary calls to my function. The function should be called outside the build method like the init function but be called only when MyChangeNotifier is updated.

I did not find a solution to this problem.
In the init function, there is no build context to access MyChangeNotifier and add a listener. it seems that to access this build context you need to be in the build method that then forces you to be called each time the widget rebuilds.

What do I miss? Do you have an elegant solution to this problem?

I hope this speedo code helps you better understand my issue.

MyChangeNotifier extends ChangeNotifier {
//contains a list of directories that can be updated from anywhere in the app
}

MyWidget extend StatelessWidget { // probably some states are needed
    Widget build(BuildContext context) {
        var myChangeNotifier = Provider.of<MyChangeNotifier>(context);
        return FutureBuilder(future: _computemd5(myChangeNotifier), builder: return Text(futur_result);); // the future is computed every time the build method is called not only when MyChangeNotifier changed.
    }

   future<String> _computemd5(MyChangeNotifier myChangeNotifier) {
    // compute
    }
}

2

Answers


  1. The most elegant solution would involve using an external package with a relatively high learning curve- Riverpod.

    Riverpod is a more robust version of Provider that allows you to have persistant asynchronous functions across your entire app scope.

    From their website, the motivation for Riverpod is partially to answer your exact question:

    Asynchronous requests need to be cached locally, as it would be unreasonable to re-execute them whenever the UI updates.

    To actually solve your problem, use an AsyncNotifierProvider as follows:

    note: this uses the Riverpod code generator so you must

    • Fully install riverpod
    • part the file where generated code will reside (part ‘example.g.dart’)
    • execute the code generator (flutter pub run build_runner watch -d)
    @riverpod
    class AsyncTodos extends _$AsyncTodos {
      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
      FutureOr<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 allow removing todos
      Future<void> removeTodo(String todoId) async {
        state = const AsyncValue.loading();
        state = await AsyncValue.guard(() async {
          await http.delete('api/todos/$todoId');
          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();
        });
      }
    }
    

    The build method (the function you want to call) will execute whenever the result changes.

    When you need the result of the function in your code, simply watch the provider and handle loading and error states:

    final AsyncValue<List<Todo>> todos = ref.watch(asyncTodosProvider);
    
            return Center(
              /// Since network-requests are asynchronous and can fail, we need to
              /// handle both error and loading states. We can use pattern matching for this.
              /// We could alternatively use `if (activity.isLoading) { ... } else if (...)`
              child: switch (todos) {
                AsyncData(:final value) => Text('First: ${value.todo.first()}'),
                AsyncError() => const Text('Oops, something unexpected happened'),
                _ => const CircularProgressIndicator(),
              },
            );
    
    Login or Signup to reply.
  2. You can use a combination of StatefulWidget and ListenableBuilder. Ensure your widget is a StatefulWidget since you need to manage state and lifecycle methods. Then, use a ListenableBuilder to listen to changes in MyChangeNotifier. This widget will rebuild only when the notifier notifies its listeners, which is exactly what you need.

    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    
    class MyWidget extends StatefulWidget {
     @override
     _MyWidgetState createState() => _MyWidgetState();
    }
    
    class _MyWidgetState extends State<MyWidget> {
     Future<String>? _futureMd5;
    
     @override
     void initState() {
        super.initState();
        // Initialize _futureMd5 with a default value or null if not needed
        _futureMd5 = _computemd5(Provider.of<MyChangeNotifier>(context, listen: false));
     }
    
     @override
     Widget build(BuildContext context) {
        return ListenableBuilder(
          listenable: Provider.of<MyChangeNotifier>(context),
          builder: (context, child) {
            // Trigger _computemd5 only when MyChangeNotifier is updated
            _futureMd5 = _computemd5(Provider.of<MyChangeNotifier>(context, listen: false));
            return FutureBuilder<String>(
              future: _futureMd5,
              builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {
                  return CircularProgressIndicator(); // Show loading indicator while waiting
                } else if (snapshot.hasError) {
                  return Text('Error: ${snapshot.error}');
                } else {
                  return Text(snapshot.data!); // Display the result
                }
              },
            );
          },
        );
     }
    
     Future<String> _computemd5(MyChangeNotifier myChangeNotifier) async {
        // Your computation logic here
        // For demonstration, let's just return a dummy string after a delay
        await Future.delayed(Duration(seconds: 2));
        return 'MD5 Result';
     }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search