skip to Main Content

I’m discovering Flutter and I have a problem using the Provider package. I would like to understand why I’m having this behavior. I’ve prepared a demo app in order to explain this problem:

In my provider class I use a service to communicate with a back-end that I made myself in Symfony:

class AppProvider extends ChangeNotifier {
  String? _token;
  List<ToDoModel> _todos = [];

  String? get token => _token;

  List<ToDoModel> get todos => _todos;

  requestToken(String username, String password) async {
    _token = await ApiService.getToken(username, password);

    notifyListeners();
  }

  Future<void> fetchToDos() async {
    if (_token == null) {
      throw Exception('Token is null');
    }

    try {
      _todos = [];
      var todos = await ApiService.fetchToDos(_token!);
      todos.forEach((todo) {
        _todos.add(ToDoModel.fromJson(todo));
      });
      notifyListeners();
    } catch (e) {
      throw Exception('Failed to fetch todos, $e');
    }
  }
}

Here is my HomeScreen:

class HomeScreen extends StatelessWidget {
  static const routeName = '/home';

  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final appProvider = Provider.of<AppProvider>(context);
    final isLogged = appProvider.token != null;

    return Builder(builder: (context) {
      return Scaffold(
        appBar: AppBar(
          title: const Text('Home'),
        ),
        body: isLogged
            ? const TodosList()
            : Center(
                child: ElevatedButton(
                  onPressed: () {
                    Navigator.of(context).pushNamed(LoginScreen.routeName);
                  },
                  child: const Text('Login'),
                ),
              ),
      );
    });
  }
}

Once the login is done a notifyListener() is called and HomeScreen() is correctly rebuild, in fact if I replace the TodosList widget with a Placeholder() everything works great. My problem happens when in the TodosList widget I call fetchTodos() in AppProvider, once the request is fulfilled and data is set in the _todos variable notifyListeners() is called again. Now for a fraction of second the ListTodos widgets appears but right after a blank page is shown (not even the appBar is show) and in the console I’m not having any error message.
Here my ListTodos widget implementation:

class TodosList extends StatefulWidget {
  const TodosList({Key? key}) : super(key: key);

  @override
  State<TodosList> createState() => _TodosListState();
}

class _TodosListState extends State<TodosList> {
  var _isInit = true;

  @override
  void didChangeDependencies() {
    if (_isInit) {
      Provider.of<AppProvider>(context, listen: false).fetchToDos();
    }
    _isInit = false;
    super.didChangeDependencies();
  }

  @override
  Widget build(BuildContext context) {
    final appProvider = Provider.of<AppProvider>(context, listen: false);

    return ListView.builder(
      itemCount: appProvider.todos.length,
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          title: Text(appProvider.todos[index].title),
          subtitle: Text(appProvider.todos[index].body),
        );
      },
    );
  }
}

If I call fetchTodos() directly in the requestToken() method and I keep ListTodos stateless everything works.

2

Answers


  1. Have you tried wrapping with Consumer?

    Login or Signup to reply.
  2. Change your HomePage to stateful and in initState() call your fetchTodo() with addPostFrameCallback.

    WidgetsBinding.instance.addPostFrameCallback((_) async {
          final toDo = Provider.of<AppProvider>(context, listen: false);
    
          toDo.fetchToDo();
        });
    

    and In body use Consumer:

    body: Consumer<AppProvider>(
      builder: (context, provider, child) {
        if(provider.token != null){
           return ListView.builder(
                    itemCount: provider.todos.length,
                    itemBuilder: (BuildContext context, index) {
                      return ListTile(
                         title: Text(provider.todos[index].title),
                         subtitle: Text(provider.todos[index].body),
                    );
        }else{
           return Center(
                child: ElevatedButton(
                  onPressed: () {
                    Navigator.of(context).pushNamed(LoginScreen.routeName);
                  },
                  child: const Text('Login'),
                ),
              );
        }
      },
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search