Changing property of children don't trigger UI change on Flutter Getx - flutter

I'm trying to render a reactive widget, based on a object list.
I needed that when the children of this object, has his boolean property changes, the UI should be updated.
In this scenario, my controller has a list os Sections and each section has a list of Lessons.
To better understanding, i rewriter my code making a simple example, based on what i need.
I already try using .map, .forEach, and others approaches, but the children property never trigger the UI update.
GetX<LessonController>(
initState: (state) => controller.getLessonsByCourse(course.id),
builder: (_) {
return Expanded(
child: ListView.separated(
scrollDirection: Axis.vertical,
itemCount: _.sectionList.length,
itemBuilder: (BuildContext context, int index) {
return ExpansionTile(
title: Text(_.sectionList[index].title),
children: <Widget>[
for(var i = 0; i < _.sectionList[index].lessons.length; i++)
Padding(
padding: const EdgeInsets.all(8.0),
child:
Obx(() =>
GestureDetector(
child: Text((_.sectionList[index].lessons[i] as Lesson).isCompleted.toString()),
onTap: () {
(_.sectionList[index].lessons[i] as Lesson).isCompleted = !(_.sectionList[index].lessons[i]as Lesson).isCompleted;
print((_.sectionList[index].lessons[i]as Lesson).isCompleted.toString());
})
),
)
]);
},
separatorBuilder: (context, ind) => Divider(
height: 2,
color: Colors.grey[300],
),
),
);
})
Solution:
The GetX container monitores changes on the list items, so when you change a property from one of this items, the list itself doesn't change. To solved that, i change the item property, and after that i overrited it on the list.
onTap: (value) {
Section tappedSection = _.sectionList[index];
tappedSection.lessons[lessonIndex].isCompleted = value;
// This is the secret
// Making GetX detect a change on the list and rebuild UI
_.sectionList[index] = tappedSection;
}));

As far as I could tell, an observable list in Flutter GetX is not concerned with internal changes to objects in itself.
To force the UI update, I had to return the updated object to the same position in the list, in order to pretend that there was a change in the objects in the main list.
Something like that:
Section tappedSection = _.sectionList[index];
tappedSection.lessons[i].isCompleted = value;
_.notifyLessonProgress(tappedSection.lessons[i].id, value);
_.sectionList[index] = tappedSection;

It looks like the problem is here:
this._sectionList.value = value;
Try this instead:
this._sectionList = value;

Related

How to create and access value of controller for different TextFormFields in ListView Builder inside Future Builder

I'm creating an Edit Products form in which the data is being fetched from api and populate inside the future builder.
As of saving new data from the TextFormField, I will need different Controllers.
It have different TextFields for Product name Product price and Product Description
So what I'm trying to say is that as if there are multiple products, there will be multiple Product name TextFields and it will have multiple controllers.
So what I want to achieve is that how can I access the values of different Controllers in one List on button click.
Suppose if there are 4 different products like Furniture Window Glass Artificial Grass andModular kitchen and when user click on save button all these values should get in one list and then send to database.
This is what I've tried
var productNameControllers = [];
I've populated this list inside the Future Builder and assigned the controllers to textfields
FutureBuilder(
future: _showData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
productNameControllers.add(TextEditingController(
text: snapshot.data[index].prodSerName.toString()));
return Container(
child: TextFormField(
controller: productNameControllers[index],
decoration: InputDecoration(
labelText: "Product Name",
),
),
);
});
}
});
And this is the code for accessing the controller
_saveEdits() {
List savedProds = [];
for (int i = 0; i < productNameControllers.length; i++) {
savedProds.add(productNameControllers[i].text);
}
print(savedProds);
}
So the problem is that when the _saveEdits method is called It only access the first two controller values instead of all the values and when I'm scrolling the page to the last product and then calling the _saveEdits method then it works correctly but why It is not accessing all the values without scrolling to the last item.
The output I want
To access all the values
[Furniture, Window glass, Artificial Grass, Modular Kitchen]
The output I got
Only first two values
[Furniture, Window glass]
So how can I achieve it.
After Replacing ListView.builder with Listview
FutureBuilder(
future: _showData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return ListView(
children: snapshot.data.map((val) {
final int index = snapshot.data.indexOf(val);
productNameControllers
.add(TextEditingController(text: val.prodSerName));
return Container(
child: TextFormField(
controller: productNameControllers[index],
decoration: InputDecoration(
labelText: "Product Name",
),
),
);
}).toList());
} else if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
} else {
return Center(child: Text(snapshot.error.toString()));
}
},
),
ListView.builder does some optimizations such as only holding the objects that are displayed in the UI at that moment, not all values which is a good thing
I would recommend that do not use the builder but use the default ListView and do something like the following
ListView(
children: snapshot.data.map(
(val) {
final int index = snapshot.data.indexOf(val);
productNameControllers.add(TextEditingController(
text: val.prodSerName.toString()));
return Container(
child: TextFormField(
controller: productNameControllers[index],
decoration: InputDecoration(
labelText: "Product Name",
),
),
);
}
).toList()

Group Checkbox in ModalBottomSheet Flutter

How to apply group checkbox in modalbottomsheet? I have create group checkbox and its work fine. But when I put it in to the modalbottomsheet the state not instantly changes (need to reopen to see value changes). I do have wrap the checkboxListTile with StatefulBuilder and give StateSetter. Any Solution? Thank you
here my codes
Column(
children: [
StatefulBuilder(builder:
(BuildContext context,
StateSetter setState) {
return CheckboxListTile(
controlAffinity:
ListTileControlAffinity.leading,
value: checkAllPermit,
title: Text('Permit', style: TextStyle(fontWeight: FontWeight.bold)),
onChanged: (value) {
setState(() {
checkAllPermit = value;
_permitType.forEach((tipePermit){
tipePermit.value = value;
});
});
},
);
}),
ListView.builder(
shrinkWrap: true,
itemCount: _permitType.length,
itemBuilder: (context, index) {
return StatefulBuilder(builder:
(BuildContext context,
StateSetter state) {
return CheckboxListTile(
controlAffinity:
ListTileControlAffinity.leading,
value: _permitType[index].value,
title: Text(_permitType[index].title),
onChanged: (value) {
state(() {
_permitType[index].value = value;
});
},
);
});
},
),
],
),
List.forEach is useful in cases where you want to perform an operation using each element of the List.
But it is isn't a good approach when you want to update the List in your state.
When you do
_permitType.forEach((tipePermit){
tipePermit.value = value;
});
You are just updating the references of the individual elements inside your _permitType and not actually updating the reference of your _permitType, which is causing your UI to not update.
Instead do this,
_permitType = _permitType.map((_) => value);
With this, you are using map to not just update each element to value but are also changing the reference of _permitType to this new List that you are getting by calling .map.
Secondly,
When you call the StateSetter that you obtain from the StatefulBuilder, only the widget inside the builder is re-built and widgets outside the builder will not rebuild.
In your use case, there doesn't seems to be any need for StatefulBuilder since you are not using the context that it is giving you.
So, just remove the StatefulBuilder and use it normally.

Flutter Hooks Riverpod not updating widget despite provider being refreshed

I've been studying flutter for a couple of months and I am now experimenting with Hooks and Riverpod which would be very important so some results can be cached by the provider and reused and only really re-fetched when there's an update.
But I hit a point here with an issue where I can't wrap my head around the provider update to reflect in the Widget. Full example can be checked out from here -> https://github.com/codespair/riverpod_update_issue I've added some debug printing and I can see the provider is properly refreshed but the changes don't reflect on the widget.
The example has a working sample provider:
// create simple FutureProvider with respective future call next
final futureListProvider =
FutureProvider.family<List<String>, int>((ref, value) => _getList(value));
// in a real case there would be an await call inside this function to network or local db or file system, etc...
Future<List<String>> _getList(int value) async {
List<String> result = [...validValues];
if (value == -1) {
// do nothing just return original result...
} else {
result = []..add(result[value]);
}
debugPrint('Provider refreshed, result => $result');
return result;
}
a drop down list when changed refreshes the provider:
Container(
alignment: Alignment.center,
padding: EdgeInsets.fromLTRB(5, 2, 5, 1),
child: DropdownButton<String>(
key: UniqueKey(),
value: dropDownValue.value.toString(),
icon: Icon(Icons.arrow_drop_down),
iconSize: 24,
elevation: 16,
underline: Container(
height: 1,
color: Theme.of(context).primaryColor,
),
onChanged: (String? newValue) {
dropDownValue.value = newValue!;
context
.refresh(futureListProvider(intFromString(newValue)));
},
items: validValues
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(
value,
style: Theme.of(context).primaryTextTheme.subtitle1,
),
);
}).toList(),
),
),
And a simple list which uses the provider elements to render which despite the provider being properly refreshed as you can see in the debugPrinting it never updates:
Container(
key: UniqueKey(),
height: 200,
child: stringListProvider.when(
data: (stringList) {
debugPrint('List from Provider.when $stringList');
return MyListWidget(stringList);
// return _buildList(stringList);
},
loading: () => CircularProgressIndicator(),
error: (_, __) => Text('OOOPsss error'),
),
),
]),
class MyListWidget extends HookWidget {
final GlobalKey<ScaffoldState> _widgetKey = GlobalKey<ScaffoldState>();
final List<String> stringList;
MyListWidget(this.stringList);
#override
Widget build(BuildContext context) {
debugPrint('stringList in MyListWidget.build $stringList');
return ListView.builder(
key: _widgetKey,
itemCount: stringList.length,
itemBuilder: (BuildContext context, int index) {
return Card(
key: UniqueKey(),
child: Padding(
padding: EdgeInsets.all(10), child: Text(stringList[index])),
);
},
);
}
As I am evaluating approaches to develop some applications I am getting inclined to adopt a more straightforward approach to handle such cases so I am also open to evaluate simpler, more straightforward approaches but I really like some of the features like the useMemoized, useState from hooks_riverpod.
One thing I wanted to note before we get started is you can still use useMemoized, useState, etc. without hooks_riverpod, with flutter_hooks.
As far as your problem, you are misusing family. When you pass a new value into family, you are actually creating another provider. That's why your list prints correctly, because it is, but the correct result is stuck in a ProviderFamily you aren't reading.
The simpler approach is to create a StateProvider that you use to store the currently selected value and watch that provider from your FutureProvider. It will update the list automatically without having to refresh.
final selectedItemProvider = StateProvider<int>((_) => -1);
final futureListProvider = FutureProvider<List<String>>((ref) async {
final selected = ref.watch(selectedItemProvider).state;
return _getList(selected);
});
DropdownButton<String>(
...
onChanged: (String? newValue) {
dropDownValue.value = newValue!;
context.read(selectedItemProvider).state = intFromString(newValue);
},
}

checkbox lost checked value in flutter

I show my list of answers via ListView.builder and check value on checkbox work ok, but when I scroll down and turn back checked value is lost. Other way when lost focus in checked answer automatic checkbox lost checked value.
Below is my code. I would be grateful if someone could help me.
class AnswerItem extends StatefulWidget {
#override
_AnswerItemState createState() => _AnswerItemState();
}
class _AnswerItemState extends State<AnswerItem> {
List<bool> _data = [false, false, false, false];
void _onChange(bool value, int index) {
setState(() {
_data[index] = value;
});
}
Widget build(BuildContext context) {
final questionItems = Provider.of<Item>(context);
List<Answer> listOfAnswers = questionItems.answers.toList();
return SingleChildScrollView(
child:
ListView.builder(
shrinkWrap: true,
itemCount: listOfAnswers.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: Card(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: CheckboxListTile(
value: _data[index],
title: Text(listOfAnswers[index].title),
onChanged: (val) {
_onChange(val, index);
},
),
),
),
);
},
),
);
}
}
Somewhere, you're confusing "model" with "view", and storing state in the view. This necessarily means when the view is refreshed or updated, you will lose state.
Specifically, your model here appears to be listOfAnswers, which being a local variable inside a build() method, will possibly be rebuilt on every refresh (possibly 120 fps!). You need to put your model outside any build method.

Flutter Hive: deleteAt is making the value null instead of deleting it

So let's say I have a list of books.
Deleting book at nth index using deleteAt is not actually deleting it and shifting (n+1)th element to it's place, rather it is making it null. i.e. the element at nth is now null.
How to perform deleteAt perfectly?
BTW I have used delete outside of the ValueListenableBuilder.
ValueListenableBuilder(
valueListenable: Hive.box('books').listenable(),
builder: (context, box, _) {
if (box.values.length == 0)
return Center(
child: Text("No books"),
);
return ListView.builder(
primary: true,
padding: EdgeInsets.only(bottom: 95),
itemCount: box.values.length,
itemBuilder: (context, int index) {
Book book = box.get(index);
return Padding(
padding:
const EdgeInsets.only(bottom: kMasterPadding),
child: BookItem(
title: book.title,
author: book.authorName,
),
);
},
);
},
),
deleting code
() async {
await Hive.box("books").deleteAt(Hive.box("books").length - 2);
//deleted at last 2nd coz deleting at the end was working perfectly
},
You can try Book book = box.getAt(index); instead of Book book = box.get(index);
https://github.com/hivedb/hive/issues/376
You can remove an element from a List<E> by running .removeAt(index) on your list. This also shifts your last elements one index to the left.