I am using the expandable package (https://pub.dev/packages/expandable), and when I call the setState () method when taping on a checkbox the expandable panel closes during the widget tree reconstruction.
When I call setState () I tell the controller to keep the panel expanded expController.expanded = true, but that doesn't work.
I researched and it seems to me that the solution would be to use a key, but my tests did not work.
Can someone help me? I need to change the state of the checkbox, but keep the panel expanded.
Here is an sample from my code:
class ExpandableCard extends StatefulWidget {
ExpandableCard({Key key}) : super(key: key);
#override
_ExpandableCardState createState() => _ExpandableCardState();
}
class _ExpandableCardState extends State<ExpandableCard> {
var _value = false;
#override
Widget build(BuildContext context) {
ExpandableController expController =
new ExpandableController(initialExpanded: false);
return ExpandableNotifier(
controller: expController,
child: Padding(
padding: const EdgeInsets.all(2),
child: Card(
clipBehavior: Clip.antiAlias,
child: Column(
children: <Widget>[
ScrollOnExpand(
scrollOnExpand: true,
scrollOnCollapse: false,
child: ExpandablePanel(
theme: const ExpandableThemeData(
headerAlignment: ExpandablePanelHeaderAlignment.center,
tapBodyToCollapse: false,
tapHeaderToExpand: true,
tapBodyToExpand: true,
hasIcon: true,
),
header: Padding(
padding: EdgeInsets.all(10),
child: Text('HEADER'),
),
collapsed: Padding(
padding: EdgeInsets.only(left: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('collapsed'),
],
),
),
expanded: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
for (var _ in Iterable.generate(3))
Padding(
padding: EdgeInsets.only(left: 2, bottom: 2),
child: Row(
children: [
Checkbox(
value: _value,
onChanged: (bool value) {
setState(() {
this._value = value;
expController.expanded = true;
});
},
),
Text('Checkbox'),
],
),
),
],
),
builder: (_, collapsed, expanded) {
return Padding(
padding:
EdgeInsets.only(left: 10, right: 10, bottom: 10),
child: Expandable(
collapsed: collapsed,
expanded: expanded,
theme: const ExpandableThemeData(crossFadePoint: 0),
),
);
},
),
),
],
),
),
));
}
}
Sorry it is very late but as i was using expandable in my app then i came to know that:
You can do this by first making an ExpandableController, then assiging its initialExpanded a static bool isOpened (It needs to be static because the bool that we are assigning to initialExpanded should be present before the construction of this ExpandableController). Then in initState(), you have to add an addListener to it that will change the value of isOpened. So now whenever you will tap on the expandable, the listener will listen and will change the value of isOpened and now when the tree widget will reconstruct this isOpened variable will have the current state of the Expandable.
static bool isOpened=false;
ExpandableController additionalInfoController=ExpandableController(
initialExpanded: isOpened,
);
// do this in initState
additionalInfoController.addListener(()
{
isOpened=!isOpened;
});
//Then assign this controller to the controller of ExpandablePanel like this
ExpandablePanel(
controller: additionalInfoController,
);
Related
I read a lot of callbacks issues but i can´t find where is my problem. I think is something in the callback function but i don't know. I need a ExpansionTile with a button in the title and different buttons in the children, but all the buttons do the same, sum 1 to a variable.
This is my reusable ExpansionTile.
import 'package:flutter/material.dart';
class ExpTile extends StatelessWidget {
final String name;
final Function(int?) callbackFunction;
final List<Widget> children;
final int? val;
ExpTile(
{required this.name,
this.children = const <Widget>[],
required this.callbackFunction,
required this.val,
key})
: super(key: key);
#override
Widget build(BuildContext context) {
return Card(
child: ExpansionTile(
title: ElevatedButton(
onPressed: callbackFunction(val),
child: Row(
children: [Text(name), Text(val.toString())],
),
),
children: children),
);
}
}
This is the callback and how I call the ExpTile Widget:
int val = 0;
callback(int? value) {
value = 0;
value++;
WidgetsBinding.instance.addPostFrameCallback((_) => setState(() {
val = value!;
}));
}
int? defense;
#override
Widget build(BuildContext context) {
var value = 0.0;
print(val);
return ListView(
children: <Widget>[
const SizedBox(
height: 45,
),
const Center(
child: Text(
"Data",
style: ThemeText.progressFooter,
),
),
const SizedBox(
height: 10,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
decoration:
textInputDecoration.copyWith(hintText: "Name"),
onChanged: (val) => setState(() {})),
),
const SizedBox(
height: 10,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
decoration: textInputDecoration.copyWith(
hintText: "Tank"),
onChanged: (val) => setState(() {})),
),
const SizedBox(
height: 10,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
decoration: textInputDecoration.copyWith(hintText: "Field"),
onChanged: (val) => setState(() {})),
),
const SizedBox(
height: 45,
),
ExpansionTile(
title: const Text("Character"),
children: [
Text(Intensity),
Slider(value: (value), onChanged: ((value) {})),
ExpTile(
name: "Defense",
val: defense,
callbackFunction: callback,
children: []
The button in the title of ExpTile "Defense" it's disabled and when I open the first ExpansionTile "Character" then
val = 1
I don't know why if I don't tap any callback button and the callback to the widget don´t work because
defense = null
Thanks for all.
I believe the mistake is with the way you've added the callback here in your build method. You need to use the invoke the function when tapped rather than creating a reference. Just add () =>
Widget build(BuildContext context) {
return Card(
child: ExpansionTile(
title: ElevatedButton(
onPressed: () => callbackFunction(val),
child: Row(
children: [Text(name), Text(val.toString())],
),
),
children: children),
);
}
EDIT: regarding the button disabled status, please check the callback function that you're passing because a button is set to disabled if it receives null for the callbackFunction. Also it could be the same issue as earlier, where you need to do () => callback(value)
I have a parent widget that draws multiple child widgets using a listview. There is a checkbox within each of these child widgets. I am trying to implement a "select all" button in the parent widget which checks all of the children's checkboxes, but I'm having a hard time figuring out how to accomplish this.
Here is my parent widget:
class OrderDisplay extends StatefulWidget {
static const routeName = '/orderDisplay';
//final Order order;
//const OrderDisplay(this.order);
#override
OrderDisplayState createState() {
return OrderDisplayState();
}
}
class OrderDisplayState extends State<OrderDisplay> {
bool preChecked = false;
double total = 0;
List<OrderedItem> itemsToPayFor = [];
#override
Widget build(BuildContext context) {
final OrderDisplayArguments args =
ModalRoute.of(context).settings.arguments;
return Scaffold(
backgroundColor: MyColors.backgroundColor,
body: SafeArea(
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
physics: ScrollPhysics(),
child: Container(
padding: EdgeInsets.only(top: 10),
child: Column(
children: [
Text(args.order.restaurantName,
style: MyTextStyles.headingStyle),
ListView.separated(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: args.order.orderedItems.length,
itemBuilder: (context, index) {
return FoodOrderNode(
preChecked, args.order.orderedItems[index],
onCheckedChanged: (isChecked) {
isChecked
? setState(() {
itemsToPayFor.add(
args.order.orderedItems[index]);
})
: setState(() {
itemsToPayFor.remove(
args.order.orderedItems[index]);
});
});
},
separatorBuilder: (context, index) =>
MyDividers.MyDivider)
],
)),
),
),
MyDividers.MyDivider,
Container(
height: 140,
color: MyColors.backgroundColor,
child: Row(children: [
Expanded(
flex: 5,
child: Column(
children: [
Expanded(flex: 2, child: SizedBox()),
Expanded(
flex: 6,
child: SelectAllButton(() {
print("SELECT ALL");
setState(() {
preChecked = true;
});
})),
Expanded(flex: 2, child: SizedBox())
],
)),
Expanded(
flex: 5,
child: Column(
children: [
Expanded(flex: 1, child: SizedBox()),
Expanded(
flex: 8,
child: PayNowButton(() {
print("PAY NOW");
},
double.parse(itemsToPayFor
.fold(0, (t, e) => t + e.itemPrice)
.toStringAsFixed(
2)))),
Expanded(flex: 1, child: SizedBox())
],
))
]))
],
)));
}
}
And here is FoodOrderNode:
typedef void SelectedCallback(bool isChecked);
class FoodOrderNode extends StatefulWidget {
final bool preChecked;
final OrderedItem item;
final SelectedCallback onCheckedChanged;
const FoodOrderNode(this.preChecked, this.item,
{#required this.onCheckedChanged});
#override
FoodOrderNodeState createState() {
return FoodOrderNodeState();
}
}
class FoodOrderNodeState extends State<FoodOrderNode> {
bool isChecked = false;
bool isSplitSelected = false;
#override
Widget build(BuildContext context) {
isChecked = widget.preChecked;
return Container(
height: 80,
padding: EdgeInsets.only(left: 15, right: 15),
decoration: BoxDecoration(
color: MyColors.nodeBackgroundColor,
),
child: Row(
children: [
Expanded(
flex: 1,
child: CircularCheckBox(
value: isChecked,
checkColor: Colors.white,
activeColor: Colors.blue,
autofocus: false,
onChanged: (bool value) {
print("Change to val: $value");
widget.onCheckedChanged(value);
setState(() {
isChecked = value;
});
},
)),
Expanded(
flex: 7,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: EdgeInsets.only(bottom: 5, left: 40),
child: Text(
widget.item.itemName,
style: TextStyle(fontSize: 18, color: Colors.black),
textAlign: TextAlign.left,
maxLines: 2,
overflow: TextOverflow.ellipsis,
)),
Container(
padding: EdgeInsets.only(left: 40),
child: Text(
"\$${widget.item.itemPrice}",
style:
TextStyle(fontSize: 16, color: MyColors.labelColor),
))
],
),
),
Expanded(
flex: 2,
child: isSplitSelected
? SplitButtonSelected(() {
setState(() {
isSplitSelected = false;
});
})
: SplitButtonUnselected(() {
setState(() {
isSplitSelected = true;
});
}))
],
),
);
}
}
I have tried creating a "preChecked" argument for FoodOrderNode and then using setState from the parent widget, however, that hasn't worked out. I have also tried using keys, but I couldn't figure out how to get those working for this either. Thank you, and let me know if you'd like any more relevant code.
Just put a global checkbox above the list items and give it isAllChecked (bool) on its value so when it will be checked set the state to isAllChecked => true and then in child checkboxes check for condition if isAllChecked is true then mark as true or checked.
GlobalCheckbox(
onChanged(value){
setState(()
{
isAllChecked==value;
});
}
);
ChildCheckBox(
value: isAllChecked ? true : false
)
this might help you:)
I have the following situation
Column(
children: [
Tabs(),
getPage(),
],
),
the getPage method
Widget getPage() {
if (tab1IsSelected == true) {
return Container(
child: Center(
child: Text('Tab1'),
),
);
}
if (tab1IsSelected == false) {
return Container(
child: Center(
child: Text('Tab2'),
),
);
}
}
and globally I have declared a variable
bool tab1IsSelected = true;
In the Tabs Class (statefull):
class Tabs extends StatefulWidget {
#override
_TabsState createState() => _TabsState();
}
class _TabsState extends State<Tabs> {
#override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
tab1IsSelected = true;
});
},
child: Container(
decoration: BoxDecoration(
color: tab1IsSelected ? primary : second,
),
child: Padding(
padding:
EdgeInsets.only(top: 1.5 * SizeConfig.heightMultiplier),
child: Center(
child: Text(
'New Hunt',
style: Theme.of(context).textTheme.bodyText1,
),
),
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
tab1IsSelected = false;
});
},
child: Container(
decoration: BoxDecoration(
color: tab1IsSelected ? second : primary,
),
child: Padding(
padding:
EdgeInsets.only(top: 1.5 * SizeConfig.heightMultiplier),
child: Center(
child: Text(
'My Hunts',
style: Theme.of(context).textTheme.bodyText2,
),
),
),
),
),
),
],
);
}
}
I change the value of that bool, but only if I hot reload the page the content is changing. Why?
Can you guide me please?
I've tried to use ? : in that Column but the same result and if I declare that variable in the Main Class where the Column is, I can't access it in the Tabs class, so that's why I declared it globally, maybe that's the cause I have to hot reload, but how can I implement that to do what I want. Thank you in advance
setState is inside _TabsState so it will only affect/rebuilt that particular widget, not getPage(), you could try using ValueChanged<bool> to retrieve the new value and then using setState in the widget that wraps the getPage()
class Tabs extends StatefulWidget {
final ValueChanged<bool> onChanged;
Tabs({this.onChanged});
#override
_TabsState createState() => _TabsState();
}
class _TabsState extends State<Tabs> {
#override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => widget.onChanged(true), //pass the value to the onChanged
child: Container(
decoration: BoxDecoration(
color: tab1IsSelected ? primary : second,
),
child: Padding(
padding:
EdgeInsets.only(top: 1.5 * SizeConfig.heightMultiplier),
child: Center(
child: Text(
'New Hunt',
style: Theme.of(context).textTheme.bodyText1,
),
),
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () => widget.onChanged(false), //pass the value to the onChanged
child: Container(
decoration: BoxDecoration(
color: tab1IsSelected ? second : primary,
),
child: Padding(
padding:
EdgeInsets.only(top: 1.5 * SizeConfig.heightMultiplier),
child: Center(
child: Text(
'My Hunts',
style: Theme.of(context).textTheme.bodyText2,
),
),
),
),
),
),
],
);
}
}
Now on the widget with the column (That should be a StatefulWidget for setState to work)
Column(
children: [
Tabs(
onChanged: (bool value) => setState(() => tab1IsSelected = value);
),
getPage(),
],
),
everytime you change the value of tab1IsSelected it will update getPage()
If you want to rebuild a widget when something in its state changes you need to call the setState() of the widget.
The variable is referenced to the State class and when you call setState() Flutter will rebuild the widget itself by calling the build() method of the State class.
If you want to have some variables outside the widgets I suggest you to use a state management approach listed here: https://flutter.dev/docs/development/data-and-backend/state-mgmt/options.
For example you could use Provider to store the active tab and reference the provider variable in both widgets.
You can try to handle the setstate in the parent class holding the Tab Widget then pass a the function to tab class and execute it in the gesture detector.
I have a page that divides the screen into left (CheckOutPage) and right (MyFoodOrder()):
class TakeOrderPage extends StatefulWidget {
#override
_TakeOrderPageState createState() => _TakeOrderPageState();
}
class _TakeOrderPageState extends State<TakeOrderPage> {
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(flex: 4, child: CheckOutPage()),
VerticalDivider(),
Expanded(flex: 6, child: MyFoodOrder()),
],
);
}
}
In MyFoodOrder, I have a widget that builds the food items using FoodCard:
Widget buildFoodList() {
return Expanded(
child: GridView.count(
//itemCount: foods.length,
childAspectRatio: 3.0,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
crossAxisCount: 2,
controller: _controller,
physics: BouncingScrollPhysics(),
//children: foods.map((food) {
// return FoodCard(food);
//}).toList(),
children: [for (var food in Level1) if ((food.foodType == MyFoodTypes[value])) FoodCard(food)].toList(),
),
);
}
Inside FoodCard, I have a widget that has an InkWell that can move to another page when tapped for selecting options. At the moment, the new page ChooseOptions() will occupy the whole screen:
Widget buildPriceInfo() {
ConfirmAction action;
return Padding(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
'\$ ${food.price}',
style: titleStyle,
),
Card(
margin: EdgeInsets.only(right: 0),
shape: roundedRectangle4,
color: mainColor,
child: InkWell(
onTap: IsAvailable() ? () async {
remark = ''; //cancel any selected taste
if (food.options.length != 0) {
if (food.options.containsKey('2')) {
action = await _showTasteDialog(food.index);
}
if (food.options.containsKey('1')) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChooseOptions(food)),
);
}
else
addItemToCard();
}
else
addItemToCard();
} : (){},
splashColor: Colors.white70,
customBorder: roundedRectangle4,
child: Icon(Icons.add, size: 30,),
),
)
],
),
);
}
I want to modify it so that the new page of ChooseOptions only occupies the area of MyFoodOrder() instead of the whole screen. I read that nested navigator is the solution but I couldn't work it out after reading some of the examples online. Grateful if more explicit guidance or help can be provided.
Many thanks!
Wrap your MyFoodOrder with Navigator, set the routes, and assign a Navigation Key to it.
static final navigatorKey = GlobalKey<NavigatorState>();
Then use the Navigation Key to changing the routing.
navigatorKey.currentState.pushNamed("Your route");
I have simple form , inside it have CircularAvatar when this is pressed show ModalBottomSheet to choose between take picture from gallery or camera. To make my widget more compact , i separated it to some file.
FormDosenScreen (It's main screen)
DosenImagePicker (It's only CircularAvatar)
ModalBottomSheetPickImage (It's to show ModalBottomSheet)
The problem is , i don't know how to passing value from ModalBottomSheetPickImage to FormDosenScreen. Because value from ModalBottomSheetPickImage i will use to insert operation.
I only success passing from third Widget to second Widget , but when i passing again from second Widget to first widget the value is null, and i think the problem is passing from Second widget to first widget.
How can i passing from third Widget to first Widget ?
First Widget
class FormDosenScreen extends StatefulWidget {
static const routeNamed = '/formdosen-screen';
#override
_FormDosenScreenState createState() => _FormDosenScreenState();
}
class _FormDosenScreenState extends State<FormDosenScreen> {
String selectedFile;
#override
Widget build(BuildContext context) {
final detectKeyboardOpen = MediaQuery.of(context).viewInsets.bottom;
print('trigger');
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('Tambah Dosen'),
actions: <Widget>[
PopupMenuButton(
itemBuilder: (_) => [
PopupMenuItem(
child: Text('Tambah Pelajaran'),
value: 'add_pelajaran',
),
],
onSelected: (String value) {
switch (value) {
case 'add_pelajaran':
Navigator.of(context).pushNamed(FormPelajaranScreen.routeNamed);
break;
default:
}
},
)
],
),
body: Stack(
fit: StackFit.expand,
children: <Widget>[
SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
SizedBox(height: 20),
DosenImagePicker(onPickedImage: (file) => selectedFile = file),
SizedBox(height: 20),
Card(
margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextFormFieldCustom(
onSaved: (value) {},
labelText: 'Nama Dosen',
),
SizedBox(height: 20),
TextFormFieldCustom(
onSaved: (value) {},
prefixIcon: Icon(Icons.email),
labelText: 'Email Dosen',
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 20),
TextFormFieldCustom(
onSaved: (value) {},
keyboardType: TextInputType.number,
inputFormatter: [
// InputNumberFormat(),
WhitelistingTextInputFormatter.digitsOnly
],
prefixIcon: Icon(Icons.local_phone),
labelText: 'Telepon Dosen',
),
],
),
),
),
SizedBox(height: kToolbarHeight),
],
),
),
Positioned(
child: Visibility(
visible: detectKeyboardOpen > 0 ? false : true,
child: RaisedButton(
onPressed: () {
print(selectedFile);
},
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
color: colorPallete.primaryColor,
child: Text(
'SIMPAN',
style: TextStyle(fontWeight: FontWeight.bold, fontFamily: AppConfig.headerFont),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
textTheme: ButtonTextTheme.primary,
),
),
bottom: kToolbarHeight / 2,
left: sizes.width(context) / 15,
right: sizes.width(context) / 15,
)
],
),
);
}
}
Second Widget
class DosenImagePicker extends StatefulWidget {
final Function(String file) onPickedImage;
DosenImagePicker({#required this.onPickedImage});
#override
DosenImagePickerState createState() => DosenImagePickerState();
}
class DosenImagePickerState extends State<DosenImagePicker> {
String selectedImage;
#override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.center,
child: InkWell(
onTap: () async {
await showModalBottomSheet(
context: context,
builder: (context) => ModalBottomSheetPickImage(
onPickedImage: (file) {
setState(() {
selectedImage = file;
widget.onPickedImage(selectedImage);
print('Hellooo dosen image picker $selectedImage');
});
},
),
);
},
child: CircleAvatar(
foregroundColor: colorPallete.black,
backgroundImage: selectedImage == null ? null : MemoryImage(base64.decode(selectedImage)),
radius: sizes.width(context) / 6,
backgroundColor: colorPallete.accentColor,
child: selectedImage == null ? Text('Pilih Gambar') : SizedBox(),
),
),
);
}
}
Third Widget
class ModalBottomSheetPickImage extends StatelessWidget {
final Function(String file) onPickedImage;
ModalBottomSheetPickImage({#required this.onPickedImage});
#override
Widget build(BuildContext context) {
return SizedBox(
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Wrap(
alignment: WrapAlignment.spaceEvenly,
children: <Widget>[
InkWell(
onTap: () async {
final String resultBase64 =
await commonFunction.pickImage(quality: 80, returnFile: ReturnFile.BASE64);
onPickedImage(resultBase64);
},
child: CircleAvatar(
foregroundColor: colorPallete.white,
backgroundColor: colorPallete.green,
child: Icon(Icons.camera_alt),
),
),
InkWell(
onTap: () async {
final String resultBase64 =
await commonFunction.pickImage(returnFile: ReturnFile.BASE64, isCamera: false);
onPickedImage(resultBase64);
},
child: CircleAvatar(
foregroundColor: colorPallete.white,
backgroundColor: colorPallete.blue,
child: Icon(Icons.photo_library),
),
),
],
),
),
);
}
}
The cleanest and easiest way to do this is through Provider. It is one of the state management solutions you can use to pass values around the app as well as rebuild only the widgets that changed. (Ex: When the value of the Text widget changes). Here is how you can use Provider in your scenario:
This is how your model should look like:
class ImageModel extends ChangeNotifier {
String _base64Image;
get base64Image => _base64Image;
set base64Image(String base64Image) {
_base64Image = base64Image;
notifyListeners();
}
}
Don't forget to add getters and setters so that you can use notifyListeners() if you have any ui that depends on it.
Here is how you can access the values of ImageModel in your UI:
final model=Provider.of<ImageModel>(context,listen:false);
String image=model.base64Image; //get data
model.base64Image=resultBase64; //set your image data after you used ImagePicker
Here is how you can display your data in a Text Widget (Ideally, you should use Selector instead of Consumer so that the widget only rebuilds if the value its listening to changes):
#override
Widget build(BuildContext context) {
//other widgets
Selector<ImageModel, String>(
selector: (_, model) => model.base64Image,
builder: (_, image, __) {
return Text(image);
},
);
}
)
}
You could achieve this easily. If you are using Blocs.