Provider state management for PageController in Flutter - flutter

I made a website with PageController to control the scroll of some screens. And I made a menu section with screen names, and when any of them are pressed, the user will navigate to the respective screen.
The app is working fine. But I refactored the menu button so I can control it's style, and to add animation in the future.
But when I refactored the button, I can't pass the PageController index, or the nextPage function.
That's why I thought of using the provider package. However, I didn't the package before, and my setup seems complex. For I should pass the PageController state to the button, and the button should send the nextPage function with a new number.
How can I achieve this?
Here's my code:
The button:
class NavigationButton extends StatefulWidget {
const NavigationButton({
Key key,
this.title,
// this.onPressed
}) : super(key: key);
final String title;
// final VoidCallback onPressed;
#override
_NavigationButtonState createState() => _NavigationButtonState();
}
class _NavigationButtonState extends State<NavigationButton> {
bool isHovering = false;
#override
Widget build(BuildContext context) {
return InkWell(
child: Container(
decoration: BoxDecoration(
border: isHovering == true
? Border(right: BorderSide(color: Colors.blueGrey, width: 10))
: Border(right: BorderSide.none)
),
child: Text(
widget.title,
style: TextStyle(
color: Colors.white,
fontSize: 25
)
)
),
onHover: (value) {
setState(() {
isHovering = value;
});
},
onTap: () {}
);
}
}
The main layout:
class MainLayout extends StatefulWidget {
#override
_MainLayoutState createState() => _MainLayoutState();
}
class _MainLayoutState extends State<MainLayout> {
int _index = 0;
int selectedButton;
bool isHovering = false;
PageController pageController = PageController();
final List<String> _sectionsName = [
"Home",
"About",
"Services",
"Portfolio",
"Testimonials",
"News",
"Contact"
];
#override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: MediaQuery.of(context).size.width > 760
? null
: AppBar(
elevation: 0.0,
backgroundColor: Colors.transparent
),
drawer: MediaQuery.of(context).size.width < 760 ? DrawerWidget() : null,
body: Stack(
children: [
// MediaQuery.of(context).size.width < 760 ? Container() : DrawerWidget(),
Listener(
onPointerSignal: (pointerSignal) {
if (pointerSignal is PointerScrollEvent) {
if (pointerSignal.scrollDelta.dy > 0) {
if(_index < _sectionsName.length) {
// int newIndex = _index + 1;
// pageController.jumpToPage(newIndex);
pageController.nextPage(
curve: Curves.easeIn, duration: Duration(milliseconds: 500)
);
}
}
else
{
if(_index < _sectionsName.length) {
// int newIndex = _index - 1;
// pageController.jumpToPage(newIndex);
pageController.previousPage(
curve: Curves.easeIn, duration: Duration(milliseconds: 500)
);
}
}
}
},
child: PageView(
controller: pageController,
scrollDirection: Axis.vertical,
physics: NeverScrollableScrollPhysics(),
children: [
HeroSection(),
AboutSection(),
ServicesSection(),
PortfolioSection(),
TestimonialsSection(),
NewsSection(),
ContactSection()
],
onPageChanged: (index) {
_index = index;
}
)
),
MediaQuery.of(context).size.width < 760
? Container()
: Align(
alignment: Alignment.centerRight,
child: Container(
height: 205,
width: 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
for (int index = 0; index < _sectionsName.length; index++)
NavigationButton(title: _sectionsName[index])
]
)
)
)
]
)
);
}
}
Thanks in advance...
Edit:
I followed #EdwynZN answer, and it was very helpful. But I had to pass the index from MainLayout to NavigationButton like so:
MainLayout:
NavigationButton(title: _sectionsName[index], index: index)
NavigationButton:
Added this to the constructor: this.index.
And this to the class: final int index;
finally:
onTap: () {
controller.animateToPage(widget.index, curve: Curves.easeIn, duration: Duration(milliseconds: 500));
}
I hope this will help someone some day...

Using Provider you could wrap the widget that will need PageController with a ChangeNotifierProvider.value
ChangeNotifierProvider.value(
value: pageController,
Align(
alignment: Alignment.centerRight,
child: Container(
height: 205,
width: 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
for (int index = 0; index < _sectionsName.length; index++)
NavigationButton(title: _sectionsName[index])
]
)
)
),
),
Then all NavigationButton can access pageController as an inheretedWidget
class _NavigationButtonState extends State<NavigationButton> {
bool isHovering = false;
PageController controller;
int index;
#override
void didChangeDependencies() {
controller = Provider.of<PageController>(context); //you can read the pagecontroller here
index = controller.page.round(); // will update when the page changes so you can do some logic with colors
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
return InkWell(
child: Container(
decoration: BoxDecoration(
border: isHovering == true
? Border(right: BorderSide(color: Colors.blueGrey, width: 10))
: Border(right: BorderSide.none)
),
child: Text(
widget.title,
style: TextStyle(
color: Colors.white,
fontSize: 25
)
)
),
onHover: (value) {
setState(() {
isHovering = value;
});
},
onTap: () {}
);
}
}

Related

Flutter text editing in child widget with validation in parent widget

I'm trying to setup a Flutter parent/child widget configuration to allow for multiple textControllers on the child widget and the parent widget to control the saving of the form answer and some validation.
I need to be able to have multiple answer areas. Most questions will have 1 answer area but some will have multiple.
So far, I have a function being passed into the Child Widget called nextPageStatus. This is used in the textController Listener to send a true/false back to the Parent Widget and set the active status of the "Next" question button.
What I can't figure out is how to get the values from the textController in the Parent Widget. I need those values on the Parent Widget because I call the database to save the answers on the Parent Widget.
Parent Wiget:
class ParentWidget extends StatefulWidget {
ParentWidget({super.key});
#override
State<StatefulWidget> createState() => ParentWidgetState();
}
class ParentWidgetState extends State<ParentWidget> {
late int _currentIndex = 0;
bool nextPageIsActive = false;
bool prevPageIsActive = false;
late SwiperController _swiperController;
List<QuestionModel> questionList = [];
List<AnswerModel> answerList = [];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children:[
Row(
children: [
Column(
children: [
IconButton(
onPressed: prevPageIsActive ? _previousCard : () {},
icon: Icon(
Icons.arrow_back,
color: prevPageIsActive ? null : Colors.grey,
)
),
const Text("Back")
]),
Column(
children: [
IconButton(
onPressed: nextPageIsActive ? _nextPageAction : () { },
icon: Icon(
Icons.arrow_forward,
color: nextPageIsActive ? null : Colors.grey
)
),
Text("Next")
]
),
],
),
]
)
);
}
void setNextPageStatus(bool status) {
setState(() {
nextPageIsActive = status;
});
}
void _previousCard() {
_swiperController.previous(animation: true);
setState(() {
prevPageIsActive = false;
})
}
void _nextCard() {
_swiperController.next(animation: true);
setState(() {
prevPageIsActive = true;
});
}
void _saveAnswerToDatabase() {
final firestoreDatabase =
Provider.of<FirestoreDatabase>(context, listen: false);
List<String> answers = [];
// TODO: Get all answers from Child Widget here
// for (var ans in answerList) {
// answers.add(ans);
// }
firestoreDatabase.setUserAnswers(answers, userId);
}
void _nextPageAction() {
// this function should only be available
// once requirements are met on child
// (must have at least 7 letters typed for question)
_saveAnswerToDatabase();
if (questionList.length == (_currentIndex + 1)) {
// last prompt -- finish
Navigator.pushNamed(
context,
AppRoutes.home,
);
} else {
_nextCard();
}
}
Widget buildSwiper() {
return Expanded(
child: Swiper(
controller: _swiperController,
itemCount: questionList.length,
onIndexChanged: (index) {
setState(() {
_currentIndex = index;
});
},
itemBuilder: Card(
child: SingleChildScrollView(
child: Column(
children: [
// !! this is where I iterate over the all the possible questions
for (int i = 0; i < currentPrompt.questionPrompts.length; i++)
AnswerArea(
key: widget.promptScreenKey,
nextPageStatus: setNextPageStatus,
questionPrompt: questionList[index].questionPrompts[i],)
],
),
),
),
),
);
}
}
Child Widget (Answer Area):
class AnswerArea extends StatefulWidget {
AnswerArea({Key? key, required this.textPrompt, required this.nextPageStatus}) : super(key: key);
late String textPrompt;
late Function(bool) nextPageStatus;
#override
State<AnswerArea> createState() => _AnswerAreaState();
}
class _AnswerAreaState extends State<AnswerArea> {
late List<TextEditingController> answerAreaTextControllers;
final answerAreaTextController = TextEditingController();
_textAnswerListener() {
if (answerAreaTextController.text.isNotEmpty && answerAreaTextController.text.length >= 7) {
widget.nextPageStatus(true);
} else {
widget.nextPageStatus(false);
}
}
#override
Widget build(BuildContext context) {
answerAreaTextController.addListener(_textAnswerListener);
const maxLines = 5;
const numberOfLines = 5;
const cursorHeight = 22.0;
return Container(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
child: Column(
children: [
Stack(
children: [
Container(
child: SizedBox(
height: numberOfLines * (cursorHeight + 8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: TextField(
controller: answerAreaTextController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: widget.textPrompt,
hintStyle: TextStyle(color: Colors.white),
),
cursorHeight: cursorHeight,
keyboardType: TextInputType.multiline,
maxLines: maxLines,
),
),
),
),
for (int i = 0; i < numberOfLines; i++)
answerAreaTextController.text.isEmpty ? Container(
width: double.infinity,
margin: EdgeInsets.only(
top: 4 + (i + 1) * cursorHeight,
left: 15,
right: 15,
),
height: 1,
color: Colors.white,
) : Container(),
],
),
],
),
);
}
}

Animate every item in a ListView.builder

I would like to, on long press on a item in the list, animate in a checkbox on the leading end of the list item for every list item like in this video.
I have tried with implicit animations, here under is my list item code.
class InspectionItemWidget extends StatefulWidget {
final Inspection inspection;
final Function(Inspection, bool) selected;
const InspectionItemWidget(
{Key? key, required this.inspection, required this.selected})
: super(key: key);
#override
State<InspectionItemWidget> createState() => _InspectionItemWidgetState();
}
class _InspectionItemWidgetState extends State<InspectionItemWidget> {
File? _imageFile;
final _duration = const Duration(milliseconds: 400);
late double _selectWidth;
late double _opacity;
late bool _selected;
#override
void initState() {
super.initState();
_selectWidth = 0.0;
_opacity = 0.0;
_selected = false;
di<EventBus>().on<InspectionSelectEvent>().listen((event) {
if (mounted) {
setState(() {
if (event.selectMode) {
_selectWidth = 60.0;
_opacity = 1.0;
} else {
_selectWidth = 0.0;
_opacity = 0.0;
}
});
}
});
}
#override
Widget build(BuildContext context) {
AppLocalizations loc = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.fromLTRB(16, 2, 16, 8),
child: SizedBox(
height: 80,
child: PhysicalModel(
color: Colors.white,
elevation: 8,
child: Row(
children: [
_buildSelectBox(),
_buildImage(),
Padding(
padding: const EdgeInsets.only(left: 8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(getStatusName(widget.inspection.status, loc),
style: TextStyle(color: colorMedium)),
Text(getTypeName(widget.inspection.type, loc),
style: const TextStyle(fontWeight: FontWeight.bold)),
Text(formatDateTime(widget.inspection.created)),
Text(widget.inspection.description)
],
),
)
],
),
),
),
);
}
Widget _buildSelectBox() {
return AnimatedContainer(
duration: _duration,
width: _selectWidth,
child: AnimatedOpacity(
opacity: _opacity,
duration: _duration,
child: Checkbox(
value: _selected,
onChanged: (_) {
setState(() {
_selected = !_selected;
});
widget.selected(widget.inspection, _selected);
}),
),
);
}
Widget _buildImage() {
if (widget.inspection.imageName.isNotEmpty) {
if (_imageFile != null) {
return SizedBox(
width: 80,
height: 80,
child: Padding(
padding: const EdgeInsets.all(4),
child: Image.file(_imageFile!,
width: 72, height: 72, fit: BoxFit.fill)),
);
} else {
return FutureBuilder<void>(
future: _loadImage(widget.inspection.imageName),
builder: (context, snapshot) {
return SizedBox(
width: 80,
height: 80,
child: Padding(
padding: const EdgeInsets.all(4),
child: snapshot.connectionState == ConnectionState.done
? Image.file(_imageFile!,
width: 72, height: 72, fit: BoxFit.fill)
: Container(),
),
);
});
}
}
return const SizedBox(
width: 80,
height: 80,
child: Padding(
padding: EdgeInsets.all(4),
child: Icon(Icons.no_photography, size: 72),
));
}
Future<void> _loadImage(String name) async {
Directory appDocumentsDirectory = await getApplicationDocumentsDirectory();
String appDocumentsPath = appDocumentsDirectory.path;
String filePath = '$appDocumentsPath/images/$name';
_imageFile = File(filePath);
}
}
But the animation is not happening?
Also, I am using a event bus to start the animation for every item, is there a better way for this? A way to signal every item to start the animation?
You can achieve this in multiple ways. Here's one simple solution using AnimatedCrossFade.
For time being, I did it for just one item (other items won't show an unchecked checkbox, but you could do it easily with any state management solution of your choice).
Basic Example
class AnimationDemo extends StatefulWidget {
#override
AnimationDemoState createState() => AnimationDemoState();
}
class AnimationDemoState extends State<AnimationDemo> {
bool isAllSelected = false;
#override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) {
return Item(
(index + 1).toString(),
isAllSelected: isAllSelected,
onLongPress: () {
setState(() {
isAllSelected = !isAllSelected;
});
},
);
},
itemCount: 20,
);
}
}
class Item extends StatefulWidget {
final String title;
final bool isAllSelected;
final VoidCallback onLongPress;
const Item(this.title,
{required this.isAllSelected, required this.onLongPress});
#override
ItemState createState() => ItemState();
}
class ItemState extends State<Item> with TickerProviderStateMixin {
bool isSelected = false;
//todo: instead of this, use a global variable with any state management solution to show unchecked boxes for others
#override
Widget build(BuildContext context) {
return ListTile(
onLongPress: () {
setState(() {
isSelected = !isSelected;
});
},
title: Text(widget.title),
minLeadingWidth: 0,
leading: AnimatedCrossFade(
alignment: Alignment.bottomLeft,
reverseDuration: const Duration(milliseconds: 200),
secondCurve: Curves.linearToEaseOut,
duration: const Duration(milliseconds: 200),
firstChild: Checkbox(value: true, onChanged: (_) {}),
secondChild: const SizedBox(),
crossFadeState:
isSelected ? CrossFadeState.showFirst : CrossFadeState.showSecond,
),
);
}
}
Output

Why do two UniqueKeys still trigger a "Multiple widgets use the same GlobalKey" assertion?

I'm trying to make a reorderable list of custom widgets, so I use a UniqueKey() in their constructors (seeing as there's not really anything else to differentiate them by). However when I go to create another element, I get the Multiple widgets use the same GlobalKey assertion. I took a look at the Widget Inspector and both of the widgets have the UniqueKey but somehow also a key of null? I suspect this is the origin of the issue but I can't figure out how to solve it.
I have a few other properties in the constructor but all of them are late and can't be used in the constructor of a ValueKey.
My code:
section.dart:
class Section extends StatefulWidget {
final String title;
late int index;
bool selected = false;
Section ({required this.title, required this.index});
Key key = UniqueKey();
#override
SectionState createState() => SectionState();
void deselect() {
this.selected = false;
}
}
main.dart:
class WritingPageState extends State<WritingPage> {
List<Section> sections = [Section(index: 0, title: "First Section")];
ValueNotifier<int> selectedIndex = ValueNotifier(-1);
int newKeyConcatter = 1;
Widget build(BuildContext context) {
Widget addSectionButton = Padding(
key: Key("__ADD_SECTION_BUTTON_KEY__"),
padding: const EdgeInsets.fromLTRB(0, 20, 0, 0),
child: InkWell(
onTap: () => setState(() {
sections.add(Section(index: sections.length, title: "New Section $newKeyConcatter",));
newKeyConcatter++;
}),
child: Container(
height: 90,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Theme.of(context).primaryColor, width: 2),
//color: Theme.of(context).primaryColorLight
),
child: Center(
child: Icon(Icons.add, color: Theme.of(context).primaryColor, size: 40,),
),
),
),
);
List<Widget> sectionsToBeDisplayed = [];
for (Widget actualSection in sections) {
sectionsToBeDisplayed.add(actualSection);
sectionsToBeDisplayed.add(addSectionButton);
}
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(32.0),
child: Row(
children: [
Flexible(
flex: 1,
child: NotificationListener<SectionSelectedNotification> (
onNotification: (notif) {
setState(() {
for (Section s in sections) {
if (s.index != notif.index) {s.deselect();}
}
});
return true;
},
child: ReorderableListView(
buildDefaultDragHandles: false,
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final Section item = sections.removeAt(oldIndex);
sections.insert(newIndex, item);
for (int i = 0; i < sections.length; i++) {
sections[i].index = i;
}
});
},
children: sectionsToBeDisplayed,
),
),
),
VerticalDivider(
indent: 20,
endIndent: 20,
color: Colors.grey,
width: 20,
),
Flexible(
flex: 2,
child: Card(
color: Theme.of(context).primaryColorLight,
child: Center(
child: ValueListenableBuilder(
valueListenable: selectedIndex,
builder: (BuildContext context, int newSelected, Widget? child) {
if ((newSelected < 0 && newSelected != -1) || newSelected > sections.length) {
return Text("""An internal error has occured. Namely, newSelected is $newSelected,
which is something that normally shouldn't happen.""");
} else if (newSelected == -1) {
return Text("Select a section to begin writing.");
}
return TextField(
decoration: InputDecoration(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
hintText: "Once upon a time...",
),
);
},
)
)
)
)
],
)
),
);
}
}
You have several issues with your approach.
First - as I mentioned in my comment - you are not assigning the key properly. Instead of passing the constructor argument 'key', you actually did override the key property with your own.
Additionally, in this code you are adding your addSectionButton multiple times - and each time it has the same Key - which will cause a problem:
List<Widget> sectionsToBeDisplayed = [];
for (Widget actualSection in sections) {
sectionsToBeDisplayed.add(actualSection);
sectionsToBeDisplayed.add(addSectionButton);
}
But the main problem is - your approach is wrong in several ways.
You are trying to maintain list of Widgets to be reordered. You should be reordering the data. Let the widgets rebuild on their own.
Your StatefullWidget is tracking if it is selected or not, and you try to sync all the widgets once the selection changes. Instead, you should - again - be focusing on your data. Let the widgets rebuild on their own.
Here's the solution that works - you can try it in DartPad.
You will see key changes:
-I introduced a List to track your ListTile titles and the text you edit
-New variable to track the selected list item
-onReorder is almost exactly the same as Flutter doc - I only added few lines to track the selected item
-everything is in a single widget. You could still extract your Section widget (and pass a callback function for it to post changes back) - but your Section widget should be stateless.
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
final _items=<String> ["First Section"];
final _itemsText=<String> ["First Section Text"];
int _selectedIndex=-1;
void _setSelectedIndex(int? value) {
setState(() {
_selectedIndex=value ?? -1;
});
}
#override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final Color oddItemColor = colorScheme.primary.withOpacity(0.05);
final Color evenItemColor = colorScheme.primary.withOpacity(0.15);
Widget addSectionButton = Padding(
key: const Key("__ADD_SECTION_BUTTON_KEY__"),
padding: const EdgeInsets.fromLTRB(0, 20, 0, 0),
child: InkWell(
onTap: () => setState(() {
_items.add("New Section ${_items.length+1}");
_itemsText.add("");
}),
child: Container(
height: 90,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Theme.of(context).primaryColor, width: 2),
),
child: Center(
child: Icon(Icons.add, color: Theme.of(context).primaryColor, size: 40,),
),
),
),
);
Widget editor;
if ((_selectedIndex < 0 && _selectedIndex != -1) || _selectedIndex > _items.length) {
editor=Text("""An internal error has occured. Namely, newSelected is $_selectedIndex,
which is something that normally shouldn't happen.""");
} else if (_selectedIndex == -1) {
editor=const Text("Select a section to begin writing.");
} else {
TextEditingController _controller=TextEditingController(text: _itemsText[_selectedIndex]);
editor=TextField(
decoration: InputDecoration(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
hintText: "Once upon a time...",
),
controller: _controller,
onSubmitted: (String value) {
_itemsText[_selectedIndex]=value;
}
);
}
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(32.0),
child: Row(
children: [
Flexible(
flex: 1,
child:
ReorderableListView(
buildDefaultDragHandles: true,
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final String item = _items.removeAt(oldIndex);
_items.insert(newIndex, item);
final String itemText = _itemsText.removeAt(oldIndex);
_itemsText.insert(newIndex, itemText);
if (_selectedIndex==oldIndex) {
_selectedIndex=newIndex;
}
});
},
children: <Widget>[
for (int index = 0; index < _items.length; index++)
RadioListTile(
key: Key('$index'),
value: index,
groupValue: _selectedIndex,
tileColor: index.isOdd ? oddItemColor : evenItemColor,
title: Text(_items[index]),
onChanged: _setSelectedIndex
),
addSectionButton
],
),
//),
),
const VerticalDivider(
indent: 20,
endIndent: 20,
color: Colors.grey,
width: 20,
),
Flexible(
flex: 2,
child: Card(
color: Theme.of(context).primaryColorLight,
child: Center(
child: editor
)
)
)
],
)
),
);
}
}

How can i get value from child to parent on button click when button is in parent?

I have been using Stepper view by flutter.And I am having an issue on getting value from child to parent on button click as the button is in parent widget.
Here is my Parent class and my child class.
Parent Class
This is my parent class which has a Stepper view with next and back button.I want to get value from my child class to parent class when next button is clicked.
class DeliveryTimeline extends StatefulWidget {
DeliveryTimeline({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<DeliveryTimeline> {
int _currentStep = 0;
String shippingtype;
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
centerTitle: true,
iconTheme: new IconThemeData(color: Colors.black),
elevation: 0,
title: Text(
"Checkout",
style: TextStyle(color: Colors.black),
),
),
body: Stepper(
type: StepperType.horizontal,
steps: _mySteps(),
currentStep: this._currentStep,
onStepTapped: (step) {
setState(() {
this._currentStep = step;
});
},
onStepContinue: () {
setState(() {
if (this._currentStep == 0) {
this._currentStep = this._currentStep + 1;
**//need to get value here on first next click**
} else if (this._currentStep == 1) {
this._currentStep = this._currentStep + 1;
} else {
print('Completed, check fields.');
}
});
},
onStepCancel: () {
setState(() {
if (this._currentStep > 0) {
this._currentStep = this._currentStep - 1;
} else {
this._currentStep = 0;
}
});
},
controlsBuilder: (BuildContext context,
{VoidCallback onStepContinue,
VoidCallback onStepCancel,
Function onShippingNextClick}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
OutlineButton(
child: new Text("Back"),
onPressed: onStepCancel,
shape: new RoundedRectangleBorder(
borderRadius: new BorderRadius.circular(4.0))),
MaterialButton(
child: Text("Next"),
color: AppColors.primarycolor,
textColor: Colors.white,
onPressed: onStepContinue,
),
],
);
}));
}
List<Step> _mySteps() {
List<Step> _steps = [
Step(
title: Text('Delivery'),
content: Center(
child: Container(
height: MediaQuery.of(context).size.height / 1.5,
child: Delivery(onShipingTypeClicked: (shippingtype){
shippingtype = shippingtype;
print("myvalue${shippingtype}");
},),
),
),
isActive: _currentStep >= 0,
),
Step(
title: Text('Address'),
content: Address(),
isActive: _currentStep >= 1,
),
Step(
title: Text('Payment'),
content: Payment(),
isActive: _currentStep >= 2,
)
];
return _steps;
}
}
Child Class
This is my child class i have a Listview which act like a radio button.I want the selected item and its value to the parent class when button is clicked..
class Delivery extends StatefulWidget {
final ValueChanged<String> onShipingTypeClicked;
Delivery({this.onShipingTypeClicked});
#override
_DeliveryState createState() => _DeliveryState();
}
class _DeliveryState extends State<Delivery> {
List<RadioModel> sampleData = new List<RadioModel>();
#override
void initState() {
// TODO: implement initState
super.initState();
sampleData.add(new RadioModel(false, 'A', 0xffe6194B, "Standard Delivery",
"Order will be delivered between 3 - 5 business days", 1));
sampleData.add(new RadioModel(
true,
'A',
0xffe6194B,
"Next Day Delivery",
"Place your order before 6pm and your items will be delivered the next day",
2));
sampleData.add(new RadioModel(
false,
'A',
0xffe6194B,
"Nominated Delivery",
"Pick a particular date from the calendar and order will be delivered on selected date",
3));
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new ListView.builder(
itemCount: sampleData.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return new InkWell(
onTap: () {
setState(() {
sampleData.forEach((element) => element.isSelected = false);
sampleData[index].isSelected = true;
widget.onShipingTypeClicked(sampleData[index].buttonText);
});
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextSmallTitleSize(
title: sampleData[index].title,
),
Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Flexible(
child: TextSmallDimText(sampleData[index].label),
),
RadioItem(sampleData[index]),
],
),
)
],
));
},
),
);
}
}
class RadioItem extends StatelessWidget {
final RadioModel _item;
RadioItem(this._item);
#override
Widget build(BuildContext context) {
return new Container(
margin: new EdgeInsets.all(15.0),
child: new Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
new Container(
height: 25.0,
width: 25.0,
alignment: Alignment.center,
child: Container(
height: 15.0,
width: 15.0,
decoration: new BoxDecoration(
color: AppColors.primarycolor,
borderRadius:
const BorderRadius.all(const Radius.circular(15)),
)),
decoration: new BoxDecoration(
color: Colors.transparent,
border: new Border.all(
width: 3.0,
color: _item.isSelected
? AppColors.primarycolor
: Colors.transparent),
borderRadius: const BorderRadius.all(const Radius.circular(25)),
),
),
new Container(margin: new EdgeInsets.only(left: 10.0))
],
),
);
}
}
class RadioModel {
bool isSelected;
final String buttonText;
final int colorCode;
final String title, label;
final int buttonid;
RadioModel(this.isSelected, this.buttonText, this.colorCode, this.title,
this.label, this.buttonid);
}
You can copy paste run full code below
You can use GlobalKey to get deliveryState of Delivery, and use deliveryState to get attribute of Delivery, attribute here is RadioModel selected
Step 1: Use GlobalKey _key = GlobalKey();
class _MyHomePageState extends State<DeliveryTimeline> {
...
GlobalKey _key = GlobalKey();
Step 2: Use deliveryState to get seleted item
onStepContinue: () {
setState(() {
if (this._currentStep == 0) {
this._currentStep = this._currentStep + 1;
final _DeliveryState deliveryState =
_key.currentState;
print("hi ${deliveryState.selected.title} ${deliveryState.selected.label} ");
Step 3: Delivery need key
child: Delivery(
key: _key,
onShipingTypeClicked: (shippingtype) {
...
Delivery({Key key, this.onShipingTypeClicked}) : super(key:key);
Step 4: Set variable selected
RadioModel selected = null;
...
return InkWell(
onTap: () {
setState(() {
...
selected = sampleData[index];
working demo
output of working demo
I/flutter ( 6246): hi Standard Delivery Order will be delivered between 3 - 5 business days
full code
import 'package:flutter/material.dart';
class DeliveryTimeline extends StatefulWidget {
DeliveryTimeline({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<DeliveryTimeline> {
int _currentStep = 0;
String shippingtype;
GlobalKey _key = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
centerTitle: true,
iconTheme: IconThemeData(color: Colors.black),
elevation: 0,
title: Text(
"Checkout",
style: TextStyle(color: Colors.black),
),
),
body: Stepper(
type: StepperType.horizontal,
steps: _mySteps(),
currentStep: this._currentStep,
onStepTapped: (step) {
setState(() {
this._currentStep = step;
});
},
onStepContinue: () {
setState(() {
if (this._currentStep == 0) {
this._currentStep = this._currentStep + 1;
final _DeliveryState deliveryState =
_key.currentState;
print("hi ${deliveryState.selected.title} ${deliveryState.selected.label} ");
} else if (this._currentStep == 1) {
this._currentStep = this._currentStep + 1;
} else {
print('Completed, check fields.');
}
});
},
onStepCancel: () {
setState(() {
if (this._currentStep > 0) {
this._currentStep = this._currentStep - 1;
} else {
this._currentStep = 0;
}
});
},
controlsBuilder: (BuildContext context,
{VoidCallback onStepContinue,
VoidCallback onStepCancel,
Function onShippingNextClick}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
OutlineButton(
child: Text("Back"),
onPressed: onStepCancel,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4.0))),
MaterialButton(
child: Text("Next"),
color: Colors.blue,
textColor: Colors.white,
onPressed: onStepContinue,
),
],
);
}));
}
List<Step> _mySteps() {
List<Step> _steps = [
Step(
title: Text('Delivery'),
content: Center(
child: Container(
height: MediaQuery.of(context).size.height / 1.5,
child: Delivery(
key: _key,
onShipingTypeClicked: (shippingtype) {
shippingtype = shippingtype;
print("myvalue${shippingtype}");
},
),
),
),
isActive: _currentStep >= 0,
),
Step(
title: Text('Address'),
content: Text("Address()"),
isActive: _currentStep >= 1,
),
Step(
title: Text('Payment'),
content: Text("Payment()"),
isActive: _currentStep >= 2,
)
];
return _steps;
}
}
class Delivery extends StatefulWidget {
final ValueChanged<String> onShipingTypeClicked;
Delivery({Key key, this.onShipingTypeClicked}) : super(key:key);
#override
_DeliveryState createState() => _DeliveryState();
}
class _DeliveryState extends State<Delivery> {
List<RadioModel> sampleData = List<RadioModel>();
#override
void initState() {
// TODO: implement initState
super.initState();
sampleData.add(RadioModel(false, 'A', 0xffe6194B, "Standard Delivery",
"Order will be delivered between 3 - 5 business days", 1));
sampleData.add(RadioModel(
true,
'A',
0xffe6194B,
"Next Day Delivery",
"Place your order before 6pm and your items will be delivered the next day",
2));
sampleData.add(RadioModel(
false,
'A',
0xffe6194B,
"Nominated Delivery",
"Pick a particular date from the calendar and order will be delivered on selected date",
3));
}
RadioModel selected = null;
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: sampleData.length,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return InkWell(
onTap: () {
setState(() {
sampleData.forEach((element) => element.isSelected = false);
sampleData[index].isSelected = true;
selected = sampleData[index];
widget.onShipingTypeClicked(sampleData[index].buttonText);
});
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ListTile(
title: Text(sampleData[index].title),
),
Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Flexible(
child: Text(sampleData[index].label),
),
RadioItem(sampleData[index]),
],
),
)
],
));
},
),
);
}
}
class RadioItem extends StatelessWidget {
final RadioModel _item;
RadioItem(this._item);
#override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(15.0),
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
height: 25.0,
width: 25.0,
alignment: Alignment.center,
child: Container(
height: 15.0,
width: 15.0,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius:
const BorderRadius.all(const Radius.circular(15)),
)),
decoration: BoxDecoration(
color: Colors.transparent,
border: Border.all(
width: 3.0,
color: _item.isSelected ? Colors.blue : Colors.transparent),
borderRadius: const BorderRadius.all(const Radius.circular(25)),
),
),
Container(margin: EdgeInsets.only(left: 10.0))
],
),
);
}
}
class RadioModel {
bool isSelected;
final String buttonText;
final int colorCode;
final String title, label;
final int buttonid;
RadioModel(this.isSelected, this.buttonText, this.colorCode, this.title,
this.label, this.buttonid);
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: DeliveryTimeline(),
);
}
}

Is there a number input field in flutter with increment/decrement buttons attached to the field?

I am trying to create a number input field with and up and down arrow button to increment and decrement its value. I am wondering if there is any inbuilt widget which already provides this functionality. I have to create couple of such fields in my UI and creating so many stateful widgets makes me wonder if there is any simpler approach.
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final title = 'Increment Decrement Demo';
return MaterialApp(
title: title,
home: NumberInputWithIncrementDecrement(),
);
}
}
class NumberInputWithIncrementDecrement extends StatefulWidget {
#override
_NumberInputWithIncrementDecrementState createState() =>
_NumberInputWithIncrementDecrementState();
}
class _NumberInputWithIncrementDecrementState
extends State<NumberInputWithIncrementDecrement> {
TextEditingController _controller = TextEditingController();
#override
void initState() {
super.initState();
_controller.text = "0"; // Setting the initial value for the field.
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Number Field increment decrement'),
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Row(
children: <Widget>[
Expanded(
flex: 1,
child: TextFormField(
controller: _controller,
keyboardType: TextInputType.numberWithOptions(
decimal: false, signed: false),
inputFormatters: <TextInputFormatter>[
WhitelistingTextInputFormatter.digitsOnly
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
MaterialButton(
minWidth: 5.0,
child: Icon(Icons.arrow_drop_up),
onPressed: () {
int currentValue = int.parse(_controller.text);
setState(() {
currentValue++;
_controller.text =
(currentValue).toString(); // incrementing value
});
},
),
MaterialButton(
minWidth: 5.0,
child: Icon(Icons.arrow_drop_down),
onPressed: () {
int currentValue = int.parse(_controller.text);
setState(() {
print("Setting state");
currentValue--;
_controller.text =
(currentValue).toString(); // decrementing value
});
},
),
],
),
Spacer(
flex: 2,
)
],
),
),
);
}
}
current output looks some thing like this.
I am looking for something like the following in a simpler manner like in HTML number input field.
I have laid out my Number input widget as shown below. I think I will go ahead with this approach until someone has any different idea for the same.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final title = 'Increment Decrement Demo';
return MaterialApp(
title: title,
home: NumberInputWithIncrementDecrement(),
);
}
}
class NumberInputWithIncrementDecrement extends StatefulWidget {
#override
_NumberInputWithIncrementDecrementState createState() =>
_NumberInputWithIncrementDecrementState();
}
class _NumberInputWithIncrementDecrementState
extends State<NumberInputWithIncrementDecrement> {
TextEditingController _controller = TextEditingController();
#override
void initState() {
super.initState();
_controller.text = "0"; // Setting the initial value for the field.
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Number Field increment decrement'),
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Center(
child: Container(
width: 60.0,
foregroundDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
border: Border.all(
color: Colors.blueGrey,
width: 2.0,
),
),
child: Row(
children: <Widget>[
Expanded(
flex: 1,
child: TextFormField(
textAlign: TextAlign.center,
decoration: InputDecoration(
contentPadding: EdgeInsets.all(8.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
),
),
controller: _controller,
keyboardType: TextInputType.numberWithOptions(
decimal: false,
signed: true,
),
inputFormatters: <TextInputFormatter>[
WhitelistingTextInputFormatter.digitsOnly
],
),
),
Container(
height: 38.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 0.5,
),
),
),
child: InkWell(
child: Icon(
Icons.arrow_drop_up,
size: 18.0,
),
onTap: () {
int currentValue = int.parse(_controller.text);
setState(() {
currentValue++;
_controller.text = (currentValue)
.toString(); // incrementing value
});
},
),
),
InkWell(
child: Icon(
Icons.arrow_drop_down,
size: 18.0,
),
onTap: () {
int currentValue = int.parse(_controller.text);
setState(() {
print("Setting state");
currentValue--;
_controller.text =
(currentValue > 0 ? currentValue : 0)
.toString(); // decrementing value
});
},
),
],
),
),
],
),
),
),
),
);
}
}
Update:
As I see many of us like this approach I created a package for the same. Maybe its helpful for some of us.
number_inc_dec
I was looking for a simple -/+ step counter, so I made one... don't pretend too much, I'm using flutter since a couple of days :-)
It has a maximum and minimum value, by default the minimum is set to zero and maximum to 10, but if you need negative values, just set it to -N.
Preview
Widget source
import 'package:flutter/material.dart';
class NumericStepButton extends StatefulWidget {
final int minValue;
final int maxValue;
final ValueChanged<int> onChanged;
NumericStepButton(
{Key key, this.minValue = 0, this.maxValue = 10, this.onChanged})
: super(key: key);
#override
State<NumericStepButton> createState() {
return _NumericStepButtonState();
}
}
class _NumericStepButtonState extends State<NumericStepButton> {
int counter= 0;
#override
Widget build(BuildContext context) {
return Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
IconButton(
icon: Icon(
Icons.remove,
color: Theme.of(context).accentColor,
),
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 18.0),
iconSize: 32.0,
color: Theme.of(context).primaryColor,
onPressed: () {
setState(() {
if (counter > widget.minValue) {
counter--;
}
widget.onChanged(counter);
});
},
),
Text(
'$counter',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black87,
fontSize: 18.0,
fontWeight: FontWeight.w500,
),
),
IconButton(
icon: Icon(
Icons.add,
color: Theme.of(context).accentColor,
),
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 18.0),
iconSize: 32.0,
color: Theme.of(context).primaryColor,
onPressed: () {
setState(() {
if (counter < widget.maxValue) {
counter++;
}
widget.onChanged(counter);
});
},
),
],
),
);
}
}
Read the counter value
...
int yourLocalVariable = 0;
...
return Container(
child: NumericStepButton(
maxValue: 20,
onChanged: (value) {
yourLocalVariable = value;
},
),
)
],
...
Happy coding!
This is the most complete solution you can find
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
class NumberTextField extends StatefulWidget {
final TextEditingController? controller;
final FocusNode? focusNode;
final int min;
final int max;
final int step;
final double arrowsWidth;
final double arrowsHeight;
final EdgeInsets contentPadding;
final double borderWidth;
final ValueChanged<int?>? onChanged;
const NumberTextField({
Key? key,
this.controller,
this.focusNode,
this.min = 0,
this.max = 999,
this.step = 1,
this.arrowsWidth = 24,
this.arrowsHeight = kMinInteractiveDimension,
this.contentPadding = const EdgeInsets.symmetric(horizontal: 8),
this.borderWidth = 2,
this.onChanged,
}) : super(key: key);
#override
State<StatefulWidget> createState() => _NumberTextFieldState();
}
class _NumberTextFieldState extends State<NumberTextField> {
late TextEditingController _controller;
late FocusNode _focusNode;
bool _canGoUp = false;
bool _canGoDown = false;
#override
void initState() {
super.initState();
_controller = widget.controller ?? TextEditingController();
_focusNode = widget.focusNode ?? FocusNode();
_updateArrows(int.tryParse(_controller.text));
}
#override
void didUpdateWidget(covariant NumberTextField oldWidget) {
super.didUpdateWidget(oldWidget);
_controller = widget.controller ?? _controller;
_focusNode = widget.focusNode ?? _focusNode;
_updateArrows(int.tryParse(_controller.text));
}
#override
Widget build(BuildContext context) => TextField(
controller: _controller,
focusNode: _focusNode,
textInputAction: TextInputAction.done,
keyboardType: TextInputType.number,
maxLength: widget.max.toString().length + (widget.min.isNegative ? 1 : 0),
decoration: InputDecoration(
counterText: '',
isDense: true,
filled: true,
fillColor: Theme.of(context).colorScheme.surface,
contentPadding: widget.contentPadding.copyWith(right: 0),
suffixIconConstraints: BoxConstraints(
maxHeight: widget.arrowsHeight, maxWidth: widget.arrowsWidth + widget.contentPadding.right),
suffixIcon: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topRight: Radius.circular(widget.borderWidth), bottomRight: Radius.circular(widget.borderWidth))),
clipBehavior: Clip.antiAlias,
alignment: Alignment.centerRight,
margin: EdgeInsets.only(
top: widget.borderWidth,
right: widget.borderWidth,
bottom: widget.borderWidth,
left: widget.contentPadding.right),
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
Expanded(
child: Material(
type: MaterialType.transparency,
child: InkWell(
child: Opacity(opacity: _canGoUp ? 1 : .5, child: const Icon(Icons.arrow_drop_up)),
onTap: _canGoUp ? () => _update(true) : null))),
Expanded(
child: Material(
type: MaterialType.transparency,
child: InkWell(
child: Opacity(opacity: _canGoDown ? 1 : .5, child: const Icon(Icons.arrow_drop_down)),
onTap: _canGoDown ? () => _update(false) : null))),
]))),
maxLines: 1,
onChanged: (value) {
final intValue = int.tryParse(value);
widget.onChanged?.call(intValue);
_updateArrows(intValue);
},
inputFormatters: [_NumberTextInputFormatter(widget.min, widget.max)]);
void _update(bool up) {
var intValue = int.tryParse(_controller.text);
intValue == null ? intValue = 0 : intValue += up ? widget.step : -widget.step;
_controller.text = intValue.toString();
_updateArrows(intValue);
_focusNode.requestFocus();
}
void _updateArrows(int? value) {
final canGoUp = value == null || value < widget.max;
final canGoDown = value == null || value > widget.min;
if (_canGoUp != canGoUp || _canGoDown != canGoDown)
setState(() {
_canGoUp = canGoUp;
_canGoDown = canGoDown;
});
}
}
class _NumberTextInputFormatter extends TextInputFormatter {
final int min;
final int max;
_NumberTextInputFormatter(this.min, this.max);
#override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
if (const ['-', ''].contains(newValue.text)) return newValue;
final intValue = int.tryParse(newValue.text);
if (intValue == null) return oldValue;
if (intValue < min) return newValue.copyWith(text: min.toString());
if (intValue > max) return newValue.copyWith(text: max.toString());
return newValue.copyWith(text: intValue.toString());
}
}
Simple, BLoC-friendly approach:
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _controller = TextEditingController();
final _streamController = StreamController<int>();
Stream<int> get _stream => _streamController.stream;
Sink<int> get _sink => _streamController.sink;
int initValue = 1;
#override
void initState() {
_sink.add(initValue);
_stream.listen((event) => _controller.text = event.toString());
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
children: [
TextButton(
onPressed: () {
_sink.add(--initValue);
},
child: Icon(Icons.remove)),
Container(
width: 50,
child: TextField(
controller: _controller,
),
),
TextButton(
onPressed: () {
_sink.add(++initValue);
},
child: Icon(Icons.add)),
],
)
],
),
),
);
}
#override
void dispose() {
_streamController.close();
_controller.dispose();
super.dispose();
}
}