I've been using a Firebase database in my project. I've been following a tutorial, and when returning widgets to the future builder it says to use:
if(snapshot.hasError) {
// Cannot connect to database
}
else {
// Return widgets as normal
}
I have checked the Flutter documentation and they say a snapshot is:
Immutable representation of the most recent interaction with an asynchronous computation.
But what does this mean, and why does the above code make sense?
Snapshot is the result of the Future or Stream you are listening to in your FutureBuilder.
Before interacting with the data being returned and using it in your builder, you have to access it first.
To access this data, which is technically being delivered indirectly to you, by your FutureBuilder, you need to ask FutureBuilder for it.
You do this by first, saying snapshot, because that is the nickname so to speak you told Flutter that you will be using, because your Future builder looks something like this:
FutureBuilder(
future: someFutureFunction(),
builder: (context, snapshot) { // Here you told Flutter to use the word "snapshot".
if (snapshot.connectionState == ConnectionState.waiting)
return Center(child: CircularProgressIndicator());
else
return Text(counter.toString());
}),
If you refereed to it as "finno", you can later on access this information by typing finno.data.
snapshot has many properties you can make use of, like hasData and connectionStatus.
If your future was expected to return an object you created, for example
Student(String name, int age)
You can print the name by saying print(snapshot.data.name).
Caution: there are two common meanings for snapshot. One is the one you use with StreamBuilder or FutureBuilder in the build method. The other is the kind of data you get back from Firebase. Unfortunately, you often use a Firebase snapshot in a FutureBuilder or StreamBuilder, which also uses the term snapshot, and the snapshot from Firebase ends up in the snapshot.data value in the builder at the appropriate time. Ugh!
Snapshot is just the response you may have from Firebase. So here they are just trying to check if the response is empty.
To access data from the received response, you can just do:
final responseData = snapshot.data
In fact you can also change the snapshot to any desired name you want.
Related
I am working on a flutter app which is meant for music students. The students can set the difficulty of the tasks they are shown. These settings are stored in an object of the Settings class and this object is stored in SharedPreferences. However, we frequently change the settings class in order to provide new options for the students. This causes problems when the app is loaded for the first time after the update.
Here is the part where the settings are loaded. I do not want the QuizView to load before the settings are available. Therefore, I use a FutureBuilder widget:
MaterialPageRoute(builder: (context) =>
FutureBuilder<Settings>(
// Fetching the settings from SharedPreferences. This is the asynchronous operation.
future: loadSettings("Uebungsmodus"),
builder: (BuildContext context,
AsyncSnapshot<Settings> snapshot) {
Widget child;
if (snapshot.hasData) {
// Extract the data from the snapshot.
// There might be a better solution, but at least this works for now.
Settings settings = Settings();
settings.augmentedFrequency =
snapshot.data!.augmentedFrequency;
settings.diminishedFrequency =
snapshot.data!.diminishedFrequency;
settings.majorFrequency =
snapshot.data!.majorFrequency;
settings.minorFrequency =
snapshot.data!.minorFrequency;
settings.maxKeySignatureAccidentals =
snapshot.data!
.maxKeySignatureAccidentals;
settings.snowmenOnly =
snapshot.data!.snowmenOnly;
settings.standardClefArrangement =
snapshot
.data!.standardClefArrangement;
settings.clefPreferences =
snapshot
.data!.clefPreferences;
QuizPageViewModel qpvm =
QuizPageViewModel(
settings,
TriadProvider(),
ParamGenerator());
child = QuizView(vm: qpvm);
} else if (snapshot.hasError) {
// Provide default settings in case of an error.
QuizPageViewModel qpvm =
QuizPageViewModel(
Settings(),
TriadProvider(),
ParamGenerator());
child = QuizView(vm: qpvm);
} else {
// Show a circular progress indicator as long as the settings are loading.
child = Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [SizedBox(
width: 60,
height: 60,
child: CircularProgressIndicator())
],
);
}
return child;
},
)));
The Map clefPreferences was not there before. Instead, there were two boolean variables. The problem now is that snapshot.data!.clefPreferences is null, since this part was never stored in SharedPreferences before. Since the data object is not null itself, flutter won't notice. If I try
settings.clefPreferences =
snapshot
.data!.clefPreferences ?? [some code initialising default preferences];
flutter tells me that "the left operand can't be null, so the right operand is never executed". It is null though! In this scenario, the app crashes.
I am unsure how to deal with this. On loading the App, there will be an update screen informing users about the new version, storing new settings at the same time. However, I am worried that I might miss something and users might somehow bypass this update-screen. This will completely break the app on a user's device unless I do another check somewhere. How can I still make sure no null entries are loaded? Any advice is appreciated!
Update: Everything seems to work now and this problem does not occur any more. If any of the variables from the Settings class cannot be retrieved, if (snapshot.data) simply evaluates to false immediately and the default settings are loaded. I am still wondering why the problem described above occurred in the first place. Apparently, I was able to load snapshot data which were null without Flutter admitting it. I could see in the debugger that they were null. I am unable to reproduce this scenario whatever I try. Has anyone come across this problem before?
There is another thing which I am wondering about: When I manually write corrupt data into the file FlutterSharedPreferences.xml, it seems to fix itself, meaning the changes are reverted when I trigger loading the data. I noticed that SharedPreferences uses the two variables _store and _preferenceCache. Are these responsible for this? Are there any backup copies or cached data which are used to overwrite the changes I manually write into that file. I am asking this because it might be easier to reproduce the problem described above if I could manually change FlutterSharedPreferences.xml.
So for my project, I am using Riverpod, and I am creating the home page of my app.
The tree looks like this ->
CommunityView(Stateful Widget)
WillPopScope
ProviderScope
Consumer
Scaffold
...and so on
Now inside the build method of the CommunityView Widget,
final params = _VSControllerParams(
selectedFamily: widget.selectedFamily,
);
print('familiesStatus: rebuild main');
return WillPopScope(
onWillPop: onWillPop,
child: ProviderScope(
overrides: [_paramsProvider.overrideWithValue(params)],
child: Consumer(
builder: (context, watch, child) {
print('familiesStatus: rebuild consumer');
final state = watch(_vsProvider(params));
final stateController = watch(_vsProvider(params).notifier);
The rebuild main print happens only once, while the rebuild consumer print happens twice. Previously home page fetched data and then shows data fetched or error depending on network response. Now I use an enum to get loading state, i.e. ApiStatus.loading is the initial state, then ApiStatus.success or failure depending on response. Now what I have changed that is causing this issue is -> I have added a function call in initstate that fetches local saved cache and loads data from that. So what happens is ApiStatus.loading->ApiStatus.success(Cache fetched)->ApiStatus.loading(somehow whole widget rebuild)->ApiStatus.success(cache fetched again)->ApiStatus.success(Data fetched from internet). So I am not sure why it is rebuilding first time when cache is fetched.
Now to fix this, I first tried to find any reason in the cache method call that is causing this rebuild, but was not able to find any even with heavy debugging. Then I thought to create a global parameter which is something like this
bool fetchDataFromCache = true;
and then inside initState, I call fetch Cache method something like this
if(fetchDataFromCache){
fetchCache();
fetchDataFromCache = false;
}
But this caused the ApiStatus loading parameter to change like this ->ApiStatus.loading(initial build)->ApiStatus.success(Cache fetched)->ApiStatus.loading(somehow whole widget rebuild)->ApiStatus.success(Data fetched from internet). So widget is still rebuilding because I have set ApiStatus.loading only in initState, and nowhere else. So it is still glitching out.
Demo video - https://youtu.be/1EzYfCRiwk0
How to create stream and use it with StreamBuilder().
I need to stream External Storage Directory in real time like as Firebase Cloud Firestore and get info if storage have current file which I search. For example, I have videos or images which I downloaded from my application, and want to listen external storage directory to show files in real time. And when I delete any file it removes from screen.
StreamBuilder widget is really useful for such cases, you can check the documentation here.
First of all, what is a stream? Imagine it like a tube, where things are passed in one side of the tube, asynchronously (meaning not all items are passed at the same time); and, at some point of the future, those items arrive at the other end of the tube, again, asynchronously.
Now, knowing what a Stream is, we have to realise that we do not know when our request will end, it may never end! Therefore, Flutter provides us with the StreamBuilder widget. Let's say that your Firebase instance returns a Stream<List<Item>> named getListOfItems. Using the StreamBuilder, you could do:
//... As a child of any of your widgets
StreamBuilder(
stream: instance.getListOfItems(), // <-- We simply pass the function that returns the stream, Flutter will manage the rest!
builder: (ctx, snapshot) {
// The snapshot contains the data that is being recieved if any
// you can check the docs:
// https://api.flutter.dev/flutter/widgets/AsyncSnapshot-class.html
// And for the ConnectionState enum
// https://api.flutter.dev/flutter/widgets/ConnectionState-class.html
if (snapshot.connectionState == ConnectionState.waiting) {
// What should be rendered when loading or waiting for a response?
return Loading();
}
if (snapshot.hasData) {
// What should be rendered when there's data?
// Remember, data may keep arriving
return MyList(snapshot.data);
}
if (snapshot.hasError) {
// Wow, the stream failed, what should be rendered when there's an error?
return Error();
}
}
)
That should do for any basic implementation of the StreamBuilder. From here you can build dynamic lists, pagination and more!
I am writing a very basic flutter app for reading public domain books. I included a .txt file containing a book in my app's assets. since a book is quite long and would take time to load I was trying to use a FutureBuilder that will display a circular progress indicator while the book is loading, however, when I click on the button to open the book the app freezes until the book is loaded instead of transitioning to the book page and showing a progress indicator while the book is loading as I would like.
I checked this for a smaller file and it didn't freeze. I tried to just tell the FutureBuilder to show the progress indicator and again, it didn't freeze.
FutureBuilder(
future: text, //Future<String>
builder: (context,snapshot) {
if (snapshot.connectionState==ConnectionState.done) {
return Text(
snapshot.data,
style: TextStyle(fontSize: 20),);
}
else {
return CircularProgressIndicator();
}
},
)
It looks like the FutureBuilder is just trying to build with the text instead of building without it and adding it later like it is supposed to do. How do I tell it to do that?
Dart is mostly single-threaded. So when you're reading the text, it's doing it in the same thread as the UI and that's why it's slowing it down. Using a future means that the call may be delegated to a later time (depending on how you schedule it), but it is still running in the same thread.
What you want to do is use an Isolate, and do the file reading within that. Once you have the file (and do any processing you need to do), you should be able to pass it back to the Text class and it should be faster - although if you're dealing with a very large amount of text it may still stutter a bit as the Text class still has to process all the text. If it does stutter, I'd recommend breaking the text into parts (chapter/paragraph?) and using multiple text and/or richtext objects to display it in a list.
The easiest way to use an Isolate in flutter is the `compute' function.
Then your future will have something like this in it:
await compute(readFile, <path to file>);
Note that the input and output of compute have some limitations since it uses an Isolate's SendPort under the hood:
The content of message can be: primitive values (null, num, bool, double, String), instances of SendPort, and lists and maps whose elements are any of these. List and maps are also allowed to be cyclic.
I think the problem is here:
if (snapshot.connectionState == ConnectionState.done) {
return Text(snapshot.data, style: TextStyle(fontSize: 20));
}
The ConnectionState.done is set just after the stream is connected (I assume you're using a stream) so that condition is always true.
I think is better if you use the snapshot.hasData as validation like so:
FutureBuilder<String>(
future: futureStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data);
} else {
return CircularProgressIndicator();
}
},
);
I have final _fetcher = PublishSubject<MyModel>() ; in my bloc Component. Here is structure of MyModel:
MyModel { List<MyObjects> _objects = [];
List<MyObjects> get allObjects => _objects; }
also there is
Observable<MyModel> get myObjects => _fetcher.stream;
in bloc.
I have two pages, first displays list of MyObjects inside Listview.builder, and second displays selected MyObject data.
I'm trying to get data from myObjects using StreamBuilder.
In the first page all objects displays perfectly. But when I open a page with selected object, my AsyncSnapshot inside StreamBuilder always has connections.state waiting, although I have data in stream.
What am I doing wrong?
Having data doesn't mean you always have access to it.
By default streams (and subjects) don't store the data they received earlier. So if you're coming late to the party, then sorry for you but no data.
To solve this problem rxdart introduces a ReplaySubject and BehaviorSubject. Both are used so that late listeners can still grab the last few events. ReplaySubject will keep track of the N latest, while BehaviorSubject will keep only the last one.
Using a BehaviorSubject instead of PublishSubject should do the trick