StreamProvider in flutter: do not always change the UI but set when - flutter

I have a StreamProvider as follows
Stream<List<Match>> streamMatches() =>
FirebaseFirestore.instance.collection('matches').snapshots()
.map((list) => list.docs.map((doc) => Match.fromJson(doc.data(), doc.id)).toList());
StreamProvider<List<Match>>(create: (context) => streamMatches(), initialData: [])
which I use it in a ListView inside a StatelessWidget
ListView(
shrinkWrap: true,
children: _getMatchesWidget(context.watch<List<Match>>())
This is nice because any update to the DB refreshes the stream. However I would like to not have the UI showing the list view change constantly in real-time for new updates (since I believe it might be a bad UX).
I would like to use a pull-on refresh (RefreshIndicator) and update the list view only onRefresh
Of course I also would like to update the list of matches in background when the user is not visualizing the list (e.g. he paused the app).
How can I tell the StreamProvider to update only in certain cases or what other Provider should I use for it?

You can change this code below:
context.watch<List<Match>>()
to this:
context.read<List<Match>>()
.read returns the List<Match> object without listening to it while .watch makes the widget listen to changes on List<Match>
Edit:
When you want to update the List<Match> object, you can call setState which will rebuild the UI and read the latest value of the stream.
You mentioned that you want to refresh the list using the RefreshIndicator.
You can just add the setState to your onRefresh method like below:
RefreshIndicator(
onRefresh: () {
setState(() {
//This refreshes the list
});
}
)

Related

Flutter BlocProvider consumption

I'm implementing a BLoC pattern for state management in my Fluter application. As I'm new in Flutter and BLoC particularly I'm evolving its usage gradually.
For new I use BLoC to communicate between two pages. One page sends an asset to the BLoC and navigates to details page. The details page uses StreamBuilder to read from the BLoC and build page with according data:
AppWidget:
Widget build(BuildContext context) => MultiProvider(
providers: [
BlocProvider(create: (context) => AssetBloc())
...
Requesting page
_onAssetMenuAction(BuildContext context, AssetMenu value, Asset asset) {
switch (value) {
case AssetMenu.validate:
var bloc = BlocProvider.of<AssetBloc>(context);
bloc.validate(asset);
Navigator.push(context,
MaterialPageRoute(builder: (context) => ValidateAssetPage()));
break;
}
Validation page
Widget build(BuildContext context) {
var bloc = BlocProvider.of<AssetBloc>(context);
Logger.root.info("Building validation page");
return StreamBuilder<AssetValidation>(
stream: bloc.outValidation,
builder: (context, snapshot) => snapshot.hasData
? QrImage.withQr(qr: snapshot.data!.qr)
: Text("No QR"));
}
BLoC
class AssetBloc extends BlocBase {
//
// Stream to handle the validation request outcome
//
StreamController<AssetValidation> _validationController =
StreamController<AssetValidation>.broadcast();
StreamSink<AssetValidation> get _inValidation => _validationController.sink;
Stream<AssetValidation> get outValidation => _validationController.stream;
//
// Stream to handle the validation request
//
StreamController<Asset> _validateController = StreamController<Asset>();
void Function(Asset) get validate => _validateController.sink.add;
//
// Constructor
//
AssetBloc([state]) : super(state) {
_validateController.stream.listen(_handleLogic);
}
void _handleLogic(Asset asset) {
_inValidation.add(AssetValidation.create(asset));
Logger.root.finest("AssetValidation instance is sent to stream");
}
void dispose() {
_validateController.close();
_validationController.close();
}
}
The problem I have is I'm getting "No QR". According to logs I see following sequence of actions:
new AssetValidation.create(): Validating asset Instance of 'Asset'
AssetBloc._handleLogic(): AssetValidation instance is sent to stream
ValidateAssetPage.build(): Building validation page
So at the moment of validation page building the validation result data should be in the stream but it seems they are not.
Unit tests of AssetBloc work as expected. So I suspect it should be related to StreamBuilder in validation page.
The StreamBuilder just shows you the last value of the stream whether the StreamBuilder was present on the current deployed widget when the stream was updated. So, if you add a new value to the stream, but the StreamBuilder is not on the current deployed widget, and, after that, you deploy the widget with the StreamBuilder, it's very likely that it won't show the updated data (in fact it shows empty data). I know, it's weird, i have the same problem when i like to use streams in that way. So, instead, i recommend you to use ValueListenable on the bloc and ValueListenableBuilder on the widget. It's very useful for that cases.
Another thing to point out is that if you're going to use just streams for the state management, it's better to use another state manager type such as provider or singleton. The reason is that, the right way to use bloc (the way you take advantage of the power of bloc) is using just the method add() for the events and logic, and using the established bloc State classes to show and update the data with the BlocBuilder on the widget.

Flutter/Dart - Edit Page in PageView, then Refresh and Scroll back to Same Place?

Currently, I can submit edits to a single page in a PageView and then either Navigator.push to a newly created single edited page or Navigator.pop back to the original Pageview containing the unedited page.
But I'd prefer to pop back to the the same place in an updated/refreshed Pageview. I was thinking I could do this on the original PageView page:
Navigator.pushReplacement(context,new MaterialPageRoute(
builder: (BuildContext context) => EditPage()),);
But after editing, how can I pop back to a refreshed PageView which is scrolled to the now updated original page? Or is there a better way? Someone mentioned keys, but I've not yet learned to use them.
The question deals with the concept of Reactive App-State. The correct way to handle this is through having an app state management solution like Bloc or Redux.
Explanation: The app state takes care of the data which you are editing. the EditPage just tells the store(App-State container) to edit that data and the framework takes care of the data that should be updated in the PageView.
as a temporary solution you can use an async call to Navigation.push() and refresh the PageView State once the EditPage comes back. you can also use an overloaded version of pop() to return a success condition which aids for a conditional setState().
Do you know that Navigator.pushReplacement(...) returns a Future<T> which completes when you finally return to original context ?
So how are you going to utilize this fact ?
Lets say you want to update a String of the original page :
String itWillBeUpdated="old value";
#override
Widget build(BuildContext ctx)
{
.
.
.
onPressesed:() async {
itWillBeUpdated= await Navigator.pushReplacement(context,new MaterialPageRoute(
builder: (BuildContext context) => EditPage()),);
setState((){});
},
}
On your editing page , you can define Navigator.pop(...) like this :
Navigator.pop<String>(context, "new string");
by doing this , you can provide any data back to the original page and by calling setState((){}) , your page will reflect the changes
This isn't ideal, but works somewhat. First I created a provider class and added the following;
class AudioWidgetProvider with ChangeNotifier {
int refreshIndex;
setRefreshIndex (ri) {
refreshIndex = ri;
return refreshIndex;
}
}
Then in my PageView Builder on the first page, I did this;
Widget build(context) {
var audioWidgetProvider = Provider.of<AudioWidgetProvider>(context);
return
PreloadPageView.builder(
controller: PreloadPageController(initialPage: audioWidgetProvider.refreshIndex?? 0),
Then to get to the EditPage (2nd screen) I did this;
onPressed: () async {
audioWidgetProvider.setRefreshIndex(currentIndex);
Navigator.pushReplacement(context,new MaterialPageRoute(builder: (BuildContext context) => EditPage()),); }
And finally I did this to return to a reloaded PageView scrolled to the edited page;
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) =>HomePage()));
The only problem now is that the PageView list comes from a PHP/Mysql query and I'm not sure what to do if new items are added to the list from the Mysql database. This means the currentIndex will be wrong. But I guess that's the topic of another question.

How to rebuild the state after pushing a new screen

When i push a new screen onTap with Navigator and pass a new class constructor, how can I have that new screen updates every time _playerTimer updates without having to click again
Since the state of my new class only updates onTap, please help!
The build method of FullScreenDialog is called once since its only being built when onTap is pressed
InkWell(
onTap: () {
return Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => FullScreenDialog(
_playerTimer,
));
},
child: VideoPlayer(
_controller,)
);
you have to use setState to rebuild the UI.
ex:
setState((){
_playerTimer = _playerTimer + 1;
});
that's about all the help I can give without seeing the rest of your code
When you instantiate a new class (in your code would be the FullScreenDialog) by passing an attribute, you're only saying to your code that a new class will be initialized by using the argument you provided.
If you want your FullScreenDialog class to always be updated when _playerTimer changes, you must observe this attribute inside this class by using setState(), which is a build-in function for StatefulWidgets that makes apps' UI update every time the observable attribute changes.
Example:
setState( () {
_playerTimer = getUpdatedTimer();
});
Supposing that inside getUpdatedTimer() method you will manage the logic for updating this variable, calling a service or so.
If you want this variable to be updated without interacting with interface, you probably will need a timer, too. Check this question to more details about it.
If you're starting with Flutter development, I suggest you to read this article (Adding interactivity to your Flutter app) about state management
and
setState method documentation.
Hope that helps.

Observable Lists in flutter with mobx

I am trying to make an observable list (i.e. when an element of the list is changed, removed, or added, I would like the UI to updated). I know that mobx has something called an "observableList", and so this seems like it should be possible. However, I am having issues implementing it. I currently have the following code in my mobx store file:
var metrics = Observable([.5,.5,.5]);
Then later, I try to change one of the elements like so:
metrics[index] = data;
I get the error:
The method '[]=' isn't defined for the class 'Observable>'.
Is there a way to create an observable list (or better yet, an observable dictionary) in flutter, or is that not implemented yet?
Thanks!
With MobX you need to create methods annotated with #action to be notified about changes over an Observable.
In your Store you must have something like
#observable
var metrics = ObservableList<int>([.5,.5,.5]);
// This is action method. You need to use this method to react
// your UI properly when something changes in your observable list.
#action
void addItem(float data) => metrics.add(data);
// the same for this method but with a different operation.
#action
void removeItem(float data) => metrics.remove(data);
#In your UI layer
Observer(
builder: (context) =>
ListView.builder(
itemCount: yourStore.metrics.length,
builder: (_, index) => Text('${yourStore.metrics[index]}'),
),
);
In this case after some change in yourStore.metrics observable list the Observer builder callback will be fired and the list view will be updated.
You can use my code. You need to add ".of" to initialize the list
ObservableList<int> values = ObservableList<int>.of([1,2,3]);
Later you can use it like this.
values[index] = data;

How to delete data from StreamBuilder after reading?

I want to delete data from my stream after I read it.
Basically I want the same system than channel in Go.
So, if I add 5, 3 and 2, my stream contains 5, 3 and 2.
When I start reading, I get 5, and my stream now contains 3 and 2 etc...
Is it possible?
EDIT: Here my problem with some code.
I use a StreamBuilder to receive data. When I change the state, it trigger again my function like if I'd just receive data.
child: StreamBuilder<Tag>(
stream: widget.tagStream,
initialData: Tag(),
builder: (BuildContext context, AsyncSnapshot<Tag> snapshot) {
/// This should be trigger only when I receive data
if (mapController.ready && snapshot.hasData) {
tag = snapshot.data;
mapController.move(
LatLng(tag.position.latitude, tag.position.longitude),
mapController.zoom);
}
return RubberBottomSheet(...);
),
Here some context:
I have a map with icons representing objects. When I click on an icon or if I search the item related on my search bar, a RubberBottomSheet appears to show informations about the object. To do that, I use a StreamBuilder, so I just need to put the object clicked or searched in it to make my rubber appears and fill in. I also need to centrer on my icon to let the user know where is the object. My problem is that when I open or close my keyboard or when I use a setState (for changing the appearance of my search bar for example), it automatically trigger the StreamBuilder like if it receive new data.
Sorry, I should have started here...
The answer of Amine seems to be the best, but I'd like to share my solution too, maybe it'll help some persons.
After I've executed my code, I pass an empty object to my Stream. I just have to verify that my object is not empty before executing my code and everything works like a charm.
builder: (BuildContext context, AsyncSnapshot<Tag> snapshot) {
if (mapController.ready &&
snapshot.hasData &&
snapshot.data.mobile.nid != 0) {
tag = snapshot.data;
... /// My code
widget.tagStream.sink.add(Tag());
}
I've had similar behavior with a StreamBuilder and I couldn't find a solution for days. What I did instead is use a ListView builder that takes data from an InheritedWidget instead.
So basically instead of having to put data into the stream sink, I simply wrap my data setter in the InheritedWidget in a setState() and the ListView rebuilds every time I change the data.
N.B: My StreamBuilder also involved a map, I've always thought it was the one interfering with it but I never got to solve the problem. As in your case, every time I change the state, the stream rebuilds with the same data it had before.
Have similar problem in my app.
Workaroung I found is quite simple - global variable, which by default will do nothing ("false" in this example).
But in method which need to call setState() change this variable value to block next snapshot.data when rebuilding widget (in this example value "true" will block).
Remember to change variable to default value or you won't get new stream updates.
// global variable - doing nothing by default
bool _clear = false;
(...)
/// Add additional condition
if (mapController.ready && snapshot.hasData && !_clear) {
tag = snapshot.data;
mapController.move(
LatLng(tag.position.latitude, tag.position.longitude),
mapController.zoom);
}
/// change variable to default value
_clear = false;
return RubberBottomSheet(...);
),
(...)
// some method calling setState
void _clearMethod() {
_clear = true;
setState(() {});
}