Flutter Streams usage - flutter

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!

Related

Flutter: How to load user settings from SharedPreferences when a new version of the app introduces structural changes to the Settings class?

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.

Navigating to another screen gets slower and slower each time i repeat click, go back, click, go back

I have a Navigator.push and MaterialPageRoute() to navigate to another screen. But navigation to other screens gets slower and slower becuase in my initState() i have a method which initializes the json data which i show each time i navigate to another screen. The json data is big and i only use one file with big json data which has objects and each object is shown i different screens. In my usecase i have to use one file with big json data.
In my initState() i initialize the data i grabbed with the method setTestData() and inside this method i set the data inside an object:
late Map<String, dynamic> grabbedData = {};
setTestData() async {
await TestData()
.getTestData()
.then((result) => setState(() => grabbedData = result));
}
#override
initState() {
setTestData();
super.initState();
}
In my view i can for example navigate to another screen and then show different objects inside the same object json i grabbed in setTestData(). I only use one view called AppView() to show different screen so when i navigate for example from screen A to B, both A and B screen are shown with AppView() widget. This is necessary for my use case which is irrelevant for this question.
This is the navigation which i use to load another screen and which technacly runs initState() from AppView() again because the previous route is also AppView():
Navigator.push(
context,
MaterialPageRoute(
maintainState: false,
builder: (_) => AppView(
selectedMenuItems:
grabbedData['data']
['views'][
widget.selectedMenuItems[
index]
[
'content']],
)));
But the problem is that each time i navigate to AppView() and click back on my phone to show previous data from AppView() and navigate again, it re-initializes the state and so the proces is slowed after i repeat it a couple of times. How do i solve this problem?
Better to use Provider package for this task. Provider does not rebuild the widget rather it only updates the state of the widget so it is faster. Then, you can get your data or data stream only once at the beginning of your app or a particular screen. No need to generate data each time. Also, provider automatically disposes data when the screen is closed. It is recommended by flutter team as well and an awesome package. For more example, check some YouTube videos. For your particular problem, I think it is better to use provider.value. Check the references and hopefully later on you will understand what is a provider.value object.
If you are generating new data every time then you need to set your provider.value each time where you are using Navigator.push, otherwise if you do not use your provider at the beginning of your app at MaterialApp section then after the push the provider won't be available.
As an example, to add provider inside a push please follow the following code snippet:
Navigator.push(context,
MaterialPageRoute(builder: ((context) {
return StreamProvider<User>.value(
initialData: initialUserData,
value: userDataStream,
child: const UpdateUser(),
);
})));
Next, access the value in the UpdateUser page like this:
final user = Provider.of<User>(context);
If you are not using any data stream then just try the normal ChangeNotifierProvider and get the value from the Consumer. Try some youtube tutorials and you will love it.

Stateful widget consumer widget builds twice and hence calls initState twice. How to stop this from happening?

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

What is a snapshot in Flutter?

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.

FutureBuilder makes my app freeze because it waits for a file to load before building

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();
}
},
);