Flutter Range Slider in showModalBottomSheet not updating UI - flutter

So I have this showModalBottomSheet() and in it I have a couple widgets that require state, so I wrapped with StatefulBuilder(). Even though the other widgets are updating their data and UI as expected but the RangeSlider() does not update the UI(slide), it only updates the data.
I've tried wrapping the RangeSlider() with StatefulBuilder() but still it does not work(slide). I've also tried other solutions like this
How can I solve this issue.
Here is my code snippet
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.0),)),
builder:(_){
return StatefulBuilder(builder: (BuildContext context, StateSetter setter){
return SafeArea(
child: Column(
children:[
...
Wrap(
children: List.generate(1, (index) {
final selectable = facets[index];
final facet = selectable.item;
RangeValues _currentRangeValues = RangeValues(0, double.parse(facets.last.item.value));
return SliderTheme(
data: SliderTheme.of(context).copyWith(
showValueIndicator: ShowValueIndicator.always,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 15),
),
child: RangeSlider(
min: 0,
max: double.parse(facets.last.item.value),
activeColor: AppColors.secondaryThemeColor,
inactiveColor: AppColors.fontHeaderColor,
labels: RangeLabels(
_currentRangeValues.start.round().toString().replaceAllMapped(reg, mathFunc),
_currentRangeValues.end.round().toString().replaceAllMapped(reg, mathFunc),
),
values: _currentRangeValues,
onChanged: (value) {
setState(() {
sVal = value.start.round();
sFal = value.end.round();
_currentRangeValues = value;
facetPriceList.toggle(facet.value);
});
},
),
);
}),
) ...
]
)
);
}
}
)
And here is the screenshots

Because you are calling setState and this will update the main class, if you want to update inside StatefulBuilder you need to call StatefulBuilder's setState, first change your StatefulBuilder to this:
StatefulBuilder(builder: (context, innerSetState) {
...
}
then instead of setState call innerSetState in RangeSlider's onChanged:
innerSetState(() {
sVal = value.start.round();
sFal = value.end.round();
_currentRangeValues = value;
facetPriceList.toggle(facet.value);
});
also your main issue is you are define _currentRangeValues inside build method. Every time you update _currentRangeValues its getting reset and define again. because you only want the first item in facets you don't need list. generate remove it, and put the variable before return StatefulBuilder like this:
final selectable = facets[0];
final facet = selectable.item;
RangeValues _currentRangeValues = RangeValues(0, double.parse(facets.last.item.value));
return StatefulBuilder(builder: (context, innerSetState) {
return ....
}
then change your wrap widget to this:
Wrap(
children: [
SliderTheme(
data: SliderTheme.of(context).copyWith(
showValueIndicator: ShowValueIndicator.always,
thumbShape:
const RoundSliderThumbShape(enabledThumbRadius: 15),
),
child: RangeSlider(
min: 0,
max: double.parse(facets.last.item.value),
activeColor: AppColors.secondaryThemeColor,
inactiveColor: AppColors.fontHeaderColor,
labels: RangeLabels(
_currentRangeValues.start
.round()
.toString()
.replaceAllMapped(reg, mathFunc),
_currentRangeValues.end
.round()
.toString()
.replaceAllMapped(reg, mathFunc),
),
values: _currentRangeValues,
onChanged: (value) {
innerSetState(() {
sVal = value.start.round();
sFal = value.end.round();
_currentRangeValues = value;
facetPriceList.toggle(facet.value);
});
},
),
),
],
)

Related

Flutter - How to get the value of a provider call function that requires 'await' within a variable?

I'm trying to make a budget app where each budget has its own spending history. Each of those spending histories would have a variable called 'budgetName' which I can compile and total the amount of spending by using sqflite code as below.
return await db.rawQuery("select sum(budgetSpent) as total from spending where budgetName ='" + budgetTitle + "'");
and this works if I try to use a .then((value) {print(value);}) when calling the sqflite function and see the value of each budget's spendings in the debug console.
But the problem is that I need the 'budgetTitle' when calling the function so it can compare with the spending's 'budgetName' to get the total spending amount.
So what I have right now is I try to get the spending amount like below:
child: BudgetCard(
budgetName: budget.budgetName,
budgetSpent: '${Provider.of<SpendingDatabaseHelper>(context, listen: false).getSpecificSpending(budget.budgetName}',
maxBudget: currency.format(int.parse(budget.maxBudget)),
svgIcon: iconListBudgetCards[budget.iconValue],
color: colorSwatch[budget.colorValue],
percentage: 0.5),
),
But it only returns Instance of 'Future<dynamic>' because it needs the 'await' before getting the value. But I couldn't find another way of doing this because it needs the 'budgetTitle' to be passed on.
Any help, ideas, or suggestions are highly appreciated! thank you in advance.
Here is the database code:
String? budgetSpendingAmount;
getSpecificSpending(budgetTitle) async {
dynamic result =
await SpendingDatabaseHelper.instance.getSpendingAmount(budgetTitle);
String a = result.toString();
debugPrint('A: $a');
if (a == '[{total: null}]') {
a = currency.format(int.parse('000'.trim()));
budgetSpendingAmount = a;
print(budgetSpendingAmount);
} else {
String? b = a.replaceAll(RegExp(r'[{\}\[\]\-]+'), '');
String c = b.substring(b.indexOf(":") + 1);
budgetSpendingAmount = currency.format(int.parse(c.trim()));
}
notifyListeners();
}
Future getSpendingAmount(String budgetTitle) async {
Database db = await instance.database;
return await db.rawQuery("select sum(budgetSpent) as total from spending where ='" + budgetTitle + "'");
}
Here is the full code of where I call the function to get the spending amount data:
Widget build(BuildContext context) {
return FutureBuilder<List<Budget>>(
future: Provider.of<BudgetDatabaseHelper>(context).getBudgets(),
/// Displaying the data from the list
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center();
}
return snapshot.data!.isEmpty
? const Flexible(
child: Center(
child: Padding(
padding: EdgeInsets.only(bottom: 80.0),
child: Text(
'You don\'t have any budget',
style: kCaption,
),
)))
: Flexible(
child: ListView.builder(
physics: const BouncingScrollPhysics(),
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
final budget = snapshot.data![index];
return Dismissible(
key: UniqueKey(),
background: const Align(
alignment: Alignment.centerRight,
child: Padding(
padding: EdgeInsets.only(bottom: 12.0, right: 24),
child: Icon(
IconlyLight.delete,
color: cRed,
size: 24,
),
),
),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
snapshot.data!.removeAt(index);
Provider.of<BudgetDatabaseHelper>(context,
listen: false)
.removeMethod(budget.id!, budget.budgetName);
},
child: GestureDetector(
onTap: () => showModalBottomSheet(
backgroundColor: Colors.transparent,
context: context,
enableDrag: true,
isScrollControlled: true,
builder: (context) {
return DraggableScrollableSheet(
snap: true,
minChildSize: 0.43,
maxChildSize: 0.85,
initialChildSize: 0.43,
snapSizes: const [0.43, 0.85],
builder: (context, scrollController) {
return ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(32),
topRight: Radius.circular(32)),
child: Container(
color: cWhite,
child: SingleChildScrollView(
controller: scrollController,
physics: const BouncingScrollPhysics(),
child: BudgetDetails(
id: budget.id!,
budgetName: budget.budgetName,
budgetSpent: 'budgetSpent',
colorValue:
colorSwatch[budget.colorValue],
maxBudget: currency.format(
int.parse(budget.maxBudget)),
svgIcon: iconListBudgetDetails[
budget.iconValue],
),
),
),
);
},
);
},
),
child: BudgetCard(
budgetName: budget.budgetName,
budgetSpent: '${Provider.of<SpendingDatabaseHelper>(context, listen: false).getSpecificSpending(budget.budgetName}',
maxBudget: currency.format(int.parse(budget.maxBudget)),
svgIcon: iconListBudgetCards[budget.iconValue],
color: colorSwatch[budget.colorValue],
percentage: 0.5),
),
);
},
),
);
},
);
}
Use provider in a widget tree is not a good idea. Make a statefullWidget
Make a getter in your SpendingDatabaseHelper like this
String? _budgetSpendingAmount;
String get budgetSpendingAmount=> _budgetSpendingAmount;
and initialize it like this _budgetSpendingAmount = currency.format(int.parse(c.trim()));
So using this getter you can access this value anywhere in widget tree
Future<void> _getSpecificSpending(String budgetName)async{
try{
await Provider.of<SpendingDatabaseHelper>(context, listen: false).getSpecificSpending(budgetName);
} catch(e){
print('error :$e');
}
}
and in your widget tree write something like this
child: FutureBuilder(
future : _getSpecificSpending(budget.budgetName)
builder: (ctx,snapshot){
var spendDataProv=Provider.of<SpendingDatabaseHelper>(context, listen: false);
return snapshot.connectionState==ConnectionState.waiting ?
CircularProgressIndicator() :
BudgetCard(
budgetName: budget.budgetName,
budgetSpent:spendDataProv.budgetSpendingAmount ,
maxBudget: currency.format(int.parse(budget.maxBudget)),
svgIcon: iconListBudgetCards[budget.iconValue],
color: colorSwatch[budget.colorValue],
percentage: 0.5)
},
)
Some idea's
Use a FutureBuilder inside your BudgetCard widget. You can then show a CircularProgressIndicator where the spent amount is going to be when you are still waiting on the future to finish.
Or
Use a Boolean flag (which you flip at the beginning of the future method and at the end) that indicates whether the future is finished. Flag false: show progressIndicator, flag true show the spent amount.
Or
When calling Provider.of<BudgetDatabaseHelper>(context).getBudgets() you can let the method getBudgets() also fill an array with the information you need later on. So, call Provider.of<SpendingDatabaseHelper>(context, listen: false).getSpecificSpending(budget.budgetName) inside the getBudgets() method for each budgetName you have.

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

Flutter did future widget didnt update screen ? i need to update data when its updated

I have an array which i set as a class like this
class FilterArray {
static var FilterArrayData = [];
}
I am simply adding the values in an array. Issue is i am calling this array in a page when array is null. Then on next Page i am adding values in array. Now issue is when i come back in previous page the array is still null. I need to refresh page for this. Which i dont want thats why i use FutureWidget i though from Future widget when array update it will also update in my screen but thats not working. Need to know what can i do for this here i need to update data when array is update so it can show in a Future Widget.
This is my total code
class _SearchPgState extends State<SearchPg> {
Future getData() async {
var result = FilterArray.FilterArrayData;
if (result.length != 0) {
return result;
} else {
return null;
}
}
#override
Widget build(BuildContext context) {
print(FilterArray.FilterArrayData);
return Scaffold(
appBar: AppBar(
title: Container(
height: 50.0,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 3.0),
child: Center(
child: TextField(
onTap: () => Get.to(SearchPgExtra()),
readOnly: true,
decoration: InputDecoration(
hintText: tr('search.search'),
alignLabelWithHint: true,
hintStyle: Theme.of(context).textTheme.subtitle2,
prefixIcon: Icon(Icons.search),
),
),
),
),
),
actions: [
IconButton(
icon: Icon(
FlutterIcons.sort_descending_mco,
color: Theme.of(context).accentColor,
),
onPressed: navigateToSortPage,
),
IconButton(
icon: Icon(
FlutterIcons.filter_fea,
color: Theme.of(context).primaryColor,
),
onPressed: navigateToFilterPage,
),
],
),
body: FutureBuilder(
future: getData(), // async work
builder: (context, projectSnap) {
print(projectSnap.data);
if (projectSnap.hasData) {
return StaggeredGridView.countBuilder(
itemCount: projectSnap.data.length,
crossAxisCount: 4,
staggeredTileBuilder: (int index) => StaggeredTile.fit(2),
mainAxisSpacing: 15.0,
crossAxisSpacing: 15.0,
scrollDirection: Axis.vertical,
shrinkWrap: true,
physics: ScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: 18.0),
itemBuilder: (context, index) {
var product = projectSnap.data[0][index];
return FadeInAnimation(
index,
child: ProductCard2(
product: product,
isHorizontalList: false,
),
);
},
);
} else {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/images/search.png',
width: MediaQuery.of(context).size.width / 2,
),
SizedBox(height: 15.0),
Text(
'search.title',
style: Theme.of(context).textTheme.headline1,
).tr(),
SizedBox(height: 15.0),
Text(
'search.subtitle',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.subtitle1,
).tr(),
SizedBox(
height: MediaQuery.of(context).size.height / 5,
),
],
),
);
}
},
),
);
}
}
In start array is null then ill add values in array then comeback nothing change then i reload the screen then its working fine.
This is the how i am adding array
RangeSlider(
values: _currentRangeValues,
min: 0,
max: 10000,
divisions: 10,
labels: RangeLabels(
_currentRangeValues.start.round().toString(),
_currentRangeValues.end.round().toString(),
),
onChanged: (RangeValues values) {
setState(() {
_currentRangeValues = values;
//print(_currentRangeValues);
});
var data = searchArray.searchArrayData;
for (int i = 0; i < data.length; i++) {
var current = data[i];
if(current['Price'] >= _currentRangeValues.start && current['Price'] <= _currentRangeValues.end){
print(data);
FilterArray.FilterArrayData.add(data);
}
}
},
),
when data add to FilterArrayData ill go back on Page array on that page not updating so then i change the page and comeback again in SearchPg then i can see data
Don't do your validation with the length of your array. It is like trying to do a validation with something that doesn't existe yet. Instead of that, try using
if(snapshot.hasData)
{ return ... ; }
then, after that, now you can do another validation, for instance, sometimes what you receive is data, but an empty array. There is where I would place the other two options. Remember, inside of the first if.
if(array.isNotEmpty)
{ return ... ; }
and
else
{ return ... ; }
After the first if, then you can now also validate, what will happen if you didn't receive data at all. Simply with an else.
else
{ return ... ; }
In summary: use one first validation with hasData and then, inside of that, decide what to do with the received information. Outside all that, decide what to do if you didn't receive any information at all.
Such cases are faced by new developers often. The best way to deal with it is state management packages like Provider, Bloc, etc. Visit the link and you will find all the relevant packages. Personally, I have used Provider a lot. Bloc is also a good option. A lot of people use it. But I haven't had the chance to use it. Riverpod is an up and coming package. But it still requires a lot of fixing.

add separate values to DropDownButton

I have a widget with a item builder. In the item builder I want to create Drop down for each item.
For the value property of this DropdownMenuItem i use
SaleRef _selectedRef;
The problem is when I declare this SaleRef _selectedRef; inside the itemBuilder the value does not change after selecting a item.
When I declare it outside this widget but in the class it changes the value of every dropdown
What can I do to select separate values on every drop down ?
This is the code for creating the items
ListView.separated(
itemBuilder: (context, index) {
return Row(
children: <Widget>[
Container(
height: 40,
width: MediaQuery.of(context).size.width * 1 / 5,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: new BorderRadius.circular(25.0),
border: Border.all(
color: Colors.grey,
style: BorderStyle.solid,
width: 0.80),
),
child: DropdownButton<SaleRef>(
hint: Text(" Select Ref"),
value: _selectedRef,
onChanged: (SaleRef value) {
setState(() {
_selectedRef = value;
print(_selectedRef.refID);
});
},
items: _filteredSaleRef.map((SaleRef ref) {
return DropdownMenuItem<SaleRef>(
value: ref,
child: Row(
children: <Widget>[
Text(
" " + ref.refName,
style: TextStyle(color: Colors.black),
),
],
),
);
}).toList(),
),
),
],
);
},
separatorBuilder: (context, index) {
return Divider(height: 16);
},
itemCount: filteredShopItem.length,
)
When I declare it like this
class _AssignRefPageState extends State<AssignRefPage>
with {
SaleRef _selectedRef;
This happens after selecting a value
When I declare it like this inside the builder like this
itemBuilder: (context, index) {
SaleRef _selectedRef;
return Row(
This is what I get it's always the hint even after I select a one
The problem is you are using the same variable for every dropdownbutton and when you try to declare it in the ListView itemBuilder and you call setState the whole Widget is built setting it to null again
What you can do is create a List of values(the size should be number of dropdownbuttons you have)
Like this
class _AssignRefPageState extends State<AssignRefPage>
with {
List<SaleRef>_selectedRef = List.generate(numberOfDropDowns, (index) => SaleRef());
Then use the array in your setState
DropdownButton<SaleRef>(
hint: Text(" Select Ref"),
value: _selectedRef,
onChanged: (SaleRef value) {
setState(() {
_selectedRef[index] = value;
print(_selectedRef[index].refID); //<- The index is from itemBuilder
});
}
As #Josteve-Adekanbi said the problem was using the same variable
Creating an array with a size outside the class helped me solve my problem
List<SaleRef> _selectedRef = new List(filteredShopItem.length);
Then I used it like this
DropdownButton<SaleRef>(
hint: Text(" Select Ref"),
value: _selectedRef[index],
onChanged: (SaleRef value) {
setState(() {
_selectedRef[index] = value;
print(_selectedRef[index].refID);
});
}

How to get Radio to work on flutter when using dynamic object?

I'm new to flutter and I have issues with Radio.
I got it to work perfectly when using the "regular" method:
int _groupValue=-1;
...
...
child: Align(
alignment: Alignment.center,
child: Column(
children: <Widget>[
RadioListTile(activeColor: Colors.black,
groupValue: _groupValue,
value: 1,
onChanged: (value) { setState(() {
_groupValue=value;
}); },
title: Text("a"),
),
RadioListTile(activeColor: Colors.black,
groupValue: _groupValue,
value: 2,
onChanged: (value) { setState(() {
_groupValue=value;
}); },
title: Text("b"),
),
],
),
),
Since I'm using data from API to create the radio buttons I've changed this code to this:
child: Align(
alignment: Alignment.center,
child: children: radioListView
),
),
and on click of a button i call async method to download the data from the API and like this :
void getApiData() async {
...
...
...
setState() {
var radioListView = List<RadioListTile>();
for (Map<String, dynamic> c in apidata)) {
radioListView.add(new RadioListTile(
groupValue: _groupValue,
value: c['option_value'],
title: c['title'],
onChanged: (value) { setState(() {
_groupValue=value;
});
),
));
}
}
}
using the first code it works but using the second code I just get to see the items but nothing happens when I click on the radio buttons (although the onchanged does trigger because I tried to print the value and it's fine)
what am I doing wrong?
I found the solution here:
https://www.didierboelens.com/2018/05/hint-5-how-to-refresh-the-content-of-a-dialog-via-setstate/
The problem was that I had to seperate the widgets and create another stateful widget for the radio