Search code examples
flutterdartflutter-provider

Error: Could not find the correct Provider<TodoModel> above this Builder Widget


I am developing a tiny to-do list app and I am new to the Provider package. I get this error:

Error: Could not find the correct Provider above this Builder Widget. This happens because you used a 'BuildContext' that does not include the provider of your choice.

When the exception was thrown, this was the stack: 
#0      Provider._inheritedElementOf (package:provider/src/provider.dart:332:7)
#1      Provider.of (package:provider/src/provider.dart:284:30)
#2      HomePage.build.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:mytodolist/main.dart:57:36)
#3      _InkResponseState._handleTap (package:flutter/src/material/ink_well.dart:994:20)
#4      GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:182:24)
...
Handler: "onTap"
Recognizer: TapGestureRecognizer#9540a
  debugOwner: GestureDetector
  state: possible
  won arena
  finalPosition: Offset(308.6, 425.5)
  finalLocalPosition: Offset(34.8, 20.3)
  button: 1
  sent tap down

Here's my main.dart:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:mytodolist/TodoModel.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          brightness: Brightness.light,
          primaryColor: Colors.blue,
          accentColor: Colors.orange,
        ),
        home: ChangeNotifierProvider(
          create: (context) => TodoModel(),
          child: HomePage(),
        )
    );
  }
}


class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("mytodos"),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          showDialog(
              context: context,
              builder: (BuildContext context) {
                return AlertDialog(
                  shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(8)),
                  title: Text("Add Todolist"),
                  content: TextField(
                    onChanged: (String value) {
                      Provider.of<TodoModel>(context, listen: false)
                          .addTitle(value);
                    },
                  ),
                  actions: <Widget>[
                    TextButton(
                        onPressed: () {
                          Provider.of<TodoModel>(context, listen: false)
                              .createTodos();
                          Navigator.of(context).pop();
                        },
                        child: Text("Add"))
                  ],
                );
              });
        },
        child: Icon(
          Icons.add,
          color: Colors.white,
        ),
      ),
      body: StreamBuilder(
          stream: FirebaseFirestore.instance.collection("MyTodos").snapshots(),
          builder: (context, snapshots) {
            if (snapshots.data == null) return CircularProgressIndicator();
            return Consumer<TodoModel>(builder: (context, todo, child) {
              return ListView.builder(
                  shrinkWrap: true,
                  itemCount: snapshots.data.docs.length,
                  itemBuilder: (context, index) {
                    DocumentSnapshot documentSnapshot =
                        snapshots.data.docs[index];
                    return Dismissible(
                        onDismissed: (direction) {
                          Provider.of<TodoModel>(context, listen: false)
                              .deleteTodos(documentSnapshot["todoTitle"]);
                        },
                        key: Key(documentSnapshot["todoTitle"]),
                        child: Card(
                          elevation: 4,
                          margin: EdgeInsets.all(8),
                          shape: RoundedRectangleBorder(
                              borderRadius: BorderRadius.circular(8)),
                          child: ListTile(
                            title: Text(documentSnapshot["todoTitle"]),
                            trailing: IconButton(
                              icon: Icon(Icons.delete),
                              color: Colors.red,
                              onPressed: () {
                                /*setState(() {
                                todos.removeAt(index);
                              });*/
                                Provider.of<TodoModel>(context, listen: false)
                                    .deleteTodos(documentSnapshot["todoTitle"]);
                              },
                            ),
                          ),
                        ));
                  });
            });
          }),
    );
  }
}

And here's my TodoModel.dart:

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/cupertino.dart';

class TodoModel extends ChangeNotifier {
  // List todos = [];
  String todoTitle = "";

  createTodos() {
    DocumentReference documentReference =
    FirebaseFirestore.instance.collection("MyTodos").doc(todoTitle);
    // Map
    Map<String, String> todos = {"todoTitle": todoTitle};
    documentReference.set(todos).whenComplete(() =>
        print("$todoTitle created"));
  }

  deleteTodos(item) {
    DocumentReference documentReference =
    FirebaseFirestore.instance.collection("MyTodos").doc(item);
    documentReference.delete().whenComplete(() => print("$item deleted"));
  }

  addTitle(String title) {
    todoTitle = title;
  }
}

In main.dart, I believe I have lifted up the ChangeNotifierProvider high enough to make its child, HomePage class, use Provider.of<TodoModel>(content).xxx. Why does it still popping up the error of not finding the correct Provider above the Builder Widget?


Solution

  • When you use showDialog() method, the inner BuildContext and outer BuildContext can get tangled up which implies potential misbehaviors for your UI. To clear things out, you should declare your common class at the beginning of build(), then reuse it within the AlertDialog (which is also a best practice for reusing code):

    class HomePage extends StatelessWidget {
    
      @override
      Widget build(BuildContext context) {
        final todoModel = Provider.of<TodoModel>(context, listen: false);
        return Scaffold(
          appBar: AppBar(
            title: Text("mytodos"),
          ),
          ...
    
    // Then use it in the dialog
    
    showDialog(
        context: context,
    
        ...
    
        content: TextField(
        onChanged: (String value) {
              todoModel.addTitle(value); // Use it here
          },
        ),
    

    There are other ways you can resolve the issue as well. You can create a separate widget that house the dialog's content, especially when the UI doesn't resemblance AlertDialog, which force us to create a custom UI widget:

    // This is useful when you cannot use AlertDialog's UI, but I'll put it here for the example
    class CustomDialog extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return AlertDialog(
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
          title: Text("Add Todolist"),
          content: TextField(
            onChanged: (String value) {
              Provider.of<TodoModel>(context, listen: false).addTitle(value);
            },
          ),
          actions: <Widget>[
            TextButton(
                onPressed: () {
                  Provider.of<TodoModel>(context, listen: false).createTodos();
                  Navigator.of(context).pop();
                },
                child: Text("Add"))
          ],
        );
      }
    }
    

    Finally, when providing common classes down to the widget tree, sometimes you can run into problems of navigating to other screens and receive the Provider not found error.

    Providers are scoped so only its descendants can access it if it's located inside the widget tree. You should move it to above MaterialApp:

    @override
      Widget build(BuildContext context) {
          return ChangeNotifierProvider(
               create: (context) => TodoModel(),
               child: MaterialApp(
                   debugShowCheckedModeBanner: false,
                   theme: ThemeData(
                   ...