Observable Lists in flutter with mobx - flutter

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;

Related

Change a dropdown's items when another dropdown's value is chosen in flutter (UI wont update)

I have two drop downs, and I want to do things when they get values selected. One of those is to change the second buttondrop items based on what's selected in the first dropdown.
For example:
Dropdown1 is a list of car manufactuers
Dropdown2 is a list of their models
Dropdown1 selects mercedes
Dropdown2 gets "E Class, S Class" etc
Dropdown1 selects lexus
Dropdown2 gets "ES, LS", etc
(Eventually the second drop down will update a listview as well, but haven't gotten to that yet.)
Data wise, it works, I update the list. The problem is the UI won't update unless I do a hot reload
Currently I am just having the dropdowns fetch their data and using Future builders
Future? data1;
Future? data2;
void initState(){
super.initState();
data1 = _data1AsyncMethod();
data2 = _data2AsyncMethod();
}
_data2AsyncMethod([int? item1_id]) async{
if(item1_id == null){
item2Classes = await DefaultItems().getAllItem2Classes();
listOfItem2ClassNames = DefaultItems().returnListOfItemClassNames(item2Classes);
}
else{
// The methods below calls the DefaultItems methods which have Futures all in them.
// The getAllItems calls a network file with GET methods of future type to get data and decodes them, etc.
// They build a list of the object type, ex List<Item2>
item2Classes = await DefaultItems().getAllItem2Classes(item1_id);
listOfItem2ClassNames = DefaultItems().returnListOfItemClassNames(item2Classes);
}
}
I have this Future Builder nested in some containers and paddings
FutureBuilder{
future: data2,
builder: (context, snapshot){
if(snapshot.connectionState != done...)
// return a circle progress indictator here
else{
return CustomDropDown{
hintText: 'example hint'
dropDownType: 'name'
dropDownList: listOfItem2ClassNames
dropDownCallback: whichDropDown,
}
The onChanged in CustomDropDown passes the dropDownType and the dropDownValue
The callback
whichDropDown(String dropDownType, String dropDownValue){
if(dropDownType == 'item1'){
//so if the first dropdown was used
// some code to get item_1's id and I call the data2 method
_data2AsyncMethod(item1_id);
}
Again the data updates (listOfItem2ClassNames) BUT the UI won't update unless I hot reload. I've even called just setState without any inputs to refresh but doesn't work
So how do I get the UI to update with the data, and is my solution too convoluted in the first place? How should I solve? StreamBuilders? I was having trouble using them.
Thanks
If you do a setState in the whichDropDown function, it will rebuild the UI. Although I'm not exactly sure what you want, your question is really ambiguous.
whichDropDown(String dropDownType, String dropDownValue){
if(dropDownType == 'item1'){
//so if the first dropdown was used
// some code to get item_1's id and I call the data2 method
_data2AsyncMethod(item1_id).then((_) {
setState(() {});
});
}
}
I notice a couple things:
nothing is causing the state to update, which is what causes a rebuild. Usually this is done explicitly with a call to setState()
in whichDropdown(), you call _data2AsyncMethod(item1_id), but that is returning a new Future, not updating data2, which means your FutureBuilder has no reason to update. Future's only go from un-completed to completed once, so once the Future in the FutureBuilder has been completed, there's no reason the widget will update again.
You may want to think about redesigning this widget a bit, perhaps rather than relying on FutureBuilder, instead call setState to react to the completion of the Futures (which can be done repeatedly, as opposed to how FutureBuilder works)

How can I get a transformed Stream<List<Object>> to display data with asynchronous code?

I am having trouble getting a stream with a list of objects to populate in a ViewModel.
Load an asynchronous Stream<List<Habit>> from a Firestore Service
in a DailyViewModel.
Call a transform method to turn that stream into a Stream<List<HabitCompletionViewModel>> that looks at a specific
instance of each of my habits for today, and creates that instance if one doesn't exist. There are a few component pieces to this:
For each habit in the initial stream, run a private method that checks if there is an instance of the habit for today, and initializes one if not. This is an asynchronous method because it calls back to the database to update the habit with the new instance.
Then find the instance for today, and return a HabitCompletionViewModel with the habit and today's instance.
Map these to a list.
That new stream is set in a property in the DailyViewModel as todaysHabits.
todaysHabits is called as the stream in a StreamBuilder in the DailyView widget.
The issue I am running into is that I know a completion for today is being found.
I am very fuzzy on what can/should be called as asynchronous code, and whether I'm using correct async/async* return/yield statements in my code, especially since I am trying to kick this process off as part of my constructor function for the DailyViewModel. I've used a bunch of print comments and it seems like everything is running, but the todaysHabits in my ViewModel is always set to null, and the StreamBuilder doesn't return any items.
Is there anything off in my code that could be causing this?
The DailyViewModel has this todaysHabits property, which is loaded near the bottom of the constructor function:
late Stream<List<HabitCompletionViewModel>> todaysHabits;
DailyViewModel({required WeekDates week}) {
_log.v('initializing the daily viewmodel');
_week = week;
_habitService
.loadActiveHabitsByUser(_loginAndUserService.loggedInUser!.id!)
.then(
(activeUserHabits) {
todaysHabits = _getTodaysHabits(activeUserHabits);
_log.v('todaysHabits has a length of ${todaysHabits.length}');
},
);
setBusy(false);
}
That constructor calls this _getTodaysHabits function which is supposed to convert that Stream<List<Habit>> into a Stream<List<HabitCompletionViewModel>>:
Stream<List<HabitCompletionViewModel>> _getTodaysHabits(
Stream<List<Habit>> habitsStream) {
return habitsStream.asyncMap((habitsList) {
return Stream.fromIterable(habitsList).asyncMap(
(habit) async {
await _updateHabitWithNewCompletions(habit);
HabitCompletion completion = habit.completions!.firstWhere(
(completion) => completion.date
.dayEqualityCheck(DateTime.now().startOfDate()));
return HabitCompletionViewModel(completion: completion, habit: habit);
},
).toList();
});
}
And my view (which is used the Stacked package to display the contents of the ViewModel and update accordingly) contains this StreamBuilder that should be returning a list of tiles for each HabitCompletionViewModel:
StreamBuilder<List<HabitCompletionViewModel>>(
stream: vm.todaysHabits,
builder: ((context, snapshot) {
if (snapshot.hasData == false) {
return Center(child: Text('No Habits Found'));
} else {
return Column(children: [
ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, i) => HabitCompletionTile(
key: ValueKey(snapshot.data![i].habit.id),
vm: snapshot.data![i],
),
),
]);
}
})),
Based on pskink's comment, I made the following updates that seem to work. (There is a slight lag when I switch to that view), but it is now showing the correct data.
Basically seems like the issue was that my previous code was returning a list of futures, instead of just a list of HabitCompletionViewModels, and using Future.wait waits for all those to complete.
Pulled out the mapping from List to List into a separate sub-method (here is the main method):
Stream<List<HabitCompletionViewModel>> _getTodaysHabits(
Stream<List<Habit>> habitsStream) {
return habitsStream.asyncMap(
(habitsList) async => await _mapHabitsToViewModel(habitsList));
}
Updated that sub-method so it first returns a List<Future>, and then uses Future.wait to wait for those to complete as HabitCompletionViewModels before returning that new list:
Future<List<HabitCompletionViewModel>> _mapHabitsToViewModel(
List<Habit> habitsList) async {
List<Future<HabitCompletionViewModel>> newList =
habitsList.map((habit) async {
HabitCompletion completion = habit.completions!.firstWhere((completion) =>
completion.date.dayEqualityCheck(DateTime.now().startOfDate()));
return HabitCompletionViewModel(completion: completion, habit: habit);
}).toList();
List<HabitCompletionViewModel> transformed = await Future.wait(newList);
return transformed;
}

Best practice: Update Model from View flutter

My question is small and is basically a request for advice on how to follow best practice in flutter. I have years of programming experience, but only a few weeks with flutter or dart.
Scenario:
To keep things simple, I have the following:
Model ClassA which has a List of ClassB. ClassB contains some String properties.
A StatefulWidget view that has a ClassA instance with a PageView builder based on the List of ClassB
Every page inside the PageView shows some String properties of ClassB in a TextField
Problem description:
Now, I want to update the properties in ClassB in realtime as the user types (or if the focus drops). There's an event for that and I created a method which accepts the ClassB instance. If I change the attribute in that instance, I see no changes to the original list. If I understand it correctly, dart passed the object by value to the event handler and not a reference to the original object. The original object remains unmodified.
In code this would be roughly the following:
Widget createTextField(ClassB classB) {
return EditableText(
onSubmitted: (value) {
setState(() {
classB.myStringValue = value;
});
}
);
}
PageView.builder(
itemBuilder: (BuildContext context, int index) {
var classB = widget.classA.myList[index];
return createTextField(classB);
},
}
)
My approach:
Instead of passing the instance of ClassB to the event handler, I just pass the index coming from the builder to the event handler instead. The handler will then directly modify the list inside ClassA using the index.
Now, having a strong Java background this somehow feels weird. My first instinct was to pass the instance of the ClassB to the event handler and modify the instance there. My assumption was that this would be a reference to the original Class B instance.
How would some of you approach this problem? It sounds easy enough, but I just want to follow the best practice here.
You should wrap the code that changes the value of a property inside
setState((){
// Put your codes here.
});

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

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

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