How to combine features of FutureProvider/FutureBuilder (waiting for async data) and ChangeNotifierProvider (change data and provide on request)
B

1

3

What I wanted to achieve, was to retrieve a document from firestore, and provide it down the widget tree, and be able to make changes on the data of the document itself.

  1. show something like "No data available/selected" when there's nothing to display,
    show loading-screen/widget if the document is loading,
    show data in the UI when document is ready
  2. be able to make changes to the data of the document itself (in firestore) AND reflect those changes in the UI, WITHOUT reading the document again from firestore
  3. be able to reload the document/load another document from firestore, show a loading screen while waiting for document, then show data again

The whole purpose of this, is to avoid too many firestore read operations. I update the document data on the server (firestore), then make the same updates in the frontend (an ugly alternative would be to retrieve the document from firestore each time I make a change).
If the changes are too complex though, or if it is an operation that is rarely executed, it might just be a better idea to read the whole document again.

"Answer your own question – share your knowledge, Q&A-style" -> see my solution to this problem in my answer below

Bezonian answered 7/6, 2022 at 14:50 Comment(0)
B
2

The above described functionalities are NOT POSSIBLE with:

  • FutureProvider: you can use this to retrieve a document from firestore and show a loading-screen while waiting, but can't make changes to it afterwards, that will also show in the UI
  • FutureBuilder: same as above, even worse, this can't provide data down the widget-tree

It was however possible with ChangeNotifierProvider (& ChangeNotifier), and this is how I did it:

enum DocumentDataStatus { noData, loadingData, dataAvailable }

...

class QuestionModel extends ChangeNotifier {
  DocumentSnapshot _questionDoc; //the document object, as retrieved from firestore
  dynamic _data; //the data of the document, think of it as Map<String, dynamic>

  DocumentDataStatus _documentDataStatus;
  DocumentDataStatus get status => _documentDataStatus;

  //constructors

  QuestionModel.example1NoInitialData() {
    _documentDataStatus = DocumentDataStatus.noData; //no question selected at first
  }

  QuestionModel.example2WithInitialData() {
    _documentDataStatus = DocumentDataStatus.loadingData; //waiting for default document or something...

    //can't use async/await syntax in a dart constructor
    FirebaseFirestore.instance.collection('quiz').doc('defaultQuestion').get().then((doc) {
      _questionDoc = doc;
      _data = _questionDoc.data();
      _documentDataStatus = DocumentDataStatus.dataAvailable;
      notifyListeners(); //now UI will show document data
    });
  }

  //all kinds of getters for specific data of the firestore document
  dynamic get questionText => _data['question'];
  dynamic get answerText => _data['answer'];
  // dynamic get ...

  ///if operation too complex to update in frontend (or if lazy dev), just reload question-document
  Future<void> loadQuestionFromFirestore(String questionID) async {
    _data = null;
    _documentDataStatus = DocumentDataStatus.loadingData;
    notifyListeners(); //now UI will show loading-screen while waiting for document

    _questionDoc = await FirebaseFirestore.instance.collection('quiz').doc(questionID).get();

    _data = _questionDoc.data();
    _documentDataStatus = DocumentDataStatus.dataAvailable;
    notifyListeners(); //now UI will show document data
  }

  ///instantly update data in the UI
  void updateQuestionTextInUI(String newText) {
    _data['question'] = newText; //to show new data in UI (this line does nothing on the backend)
    notifyListeners(); //UI will instantly update with new data
  }
}

...

Widget build(BuildContext context) {
    QuestionModel questionModel = context.watch<QuestionModel>();

    return [
      Text('No question to show'),
      Loading(),
      Text(questionModel.questionText),
    ][questionModel.status.index]; //display ONE widget of the list depending on the value of the enum
}  

The code in my example is simplified for explanation. I have implemented it in a larger scale, and everything works just as expected.

Bezonian answered 7/6, 2022 at 14:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.