Flutter text editing in child widget with validation in parent widget - flutter

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

Related

How can I solve this problem that two keyboard cursors in textfield?

https://i.stack.imgur.com/YKClg.png
I have multiple textfields and I have written code that expects that clearing index 0, i.e. the first textfield in the list, will set the focus back to the first textfield, but contrary to expectations, the cursor moves to the second field. text field. , and why do I see two cursors when I click on another textfield in this state?
`
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:todolist/dataForm.dart';
class TodoCase extends StatefulWidget {
final String title;
const TodoCase({
Key? key,
required this.title,
}) : super(key: key);
#override
State<TodoCase> createState() => _TodoCaseState();
}
class _TodoCaseState extends State<TodoCase> {
List<ToDoDataForm> toDoLineList = [];
final FocusScopeNode textListScopeNode = FocusScopeNode();
final ScrollController toDoScrollController = ScrollController();
List<TextEditingController> textEditController = [];
List<FocusNode> keyBoardFocusNode = [];
List<FocusNode> textFocusNode = [];
Future<void> scrollToEnd() async {
await Future.delayed(const Duration(milliseconds: 100));
toDoScrollController.animateTo(
toDoScrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 400),
curve: Curves.ease,
);
}
#override
Widget build(BuildContext context) {
void addList() {
setState(() {
toDoLineList.add(ToDoDataForm(todoData: ""));
scrollToEnd();
});
}
Widget renderTextFormField(ToDoDataForm dataForm, int index) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Checkbox(
value: dataForm.isDone,
onChanged: (value) {
setState(() {
dataForm.isDone = value!;
});
}),
Flexible(
child: KeyboardListener(
focusNode: keyBoardFocusNode[index],
onKeyEvent: (value) {
if (value.logicalKey == LogicalKeyboardKey.backspace &&
dataForm.todoData.isEmpty) {
setState(() {
if (index != 0) {
textFocusNode[index - 1].requestFocus();
toDoLineList.removeAt(index);
textFocusNode.removeAt(index);
keyBoardFocusNode.removeAt(index);
textEditController.removeAt(index);
} else if (index == 0 && toDoLineList.length == 1) {
toDoLineList.removeAt(index);
textFocusNode.removeAt(index);
textEditController.removeAt(index);
keyBoardFocusNode.removeAt(index);
} else if (index == 0 && toDoLineList.length != 1) {
FocusScope.of(context)
.requestFocus(textFocusNode[index + 1]);
toDoLineList.removeAt(index);
textFocusNode.removeAt(index);
textEditController.removeAt(index);
keyBoardFocusNode.removeAt(index);
}
});
print(textFocusNode);
}
},
child: TextField(
decoration: const InputDecoration(
border: InputBorder.none,
),
autofocus: false,
controller: textEditController[index],
focusNode: textFocusNode[index],
onChanged: (value) {
//print('edit : $index');
dataForm.todoData = value;
//print(textFocusNode);
},
),
),
)
],
);
}
return SizedBox(
width: 180,
height: 200,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.title,
style: const TextStyle(
color: Colors.black,
fontSize: 24,
),
),
IconButton(
onPressed: addList,
icon: const Icon(Icons.add),
padding: EdgeInsets.zero,
iconSize: 20,
),
],
),
Container(
height: 1,
color: Colors.black,
),
Expanded(
child: ListView.builder(
itemCount: toDoLineList.length,
controller: toDoScrollController,
itemBuilder: ((context, index) {
if (textEditController.length - 1 < index) {
print(index);
textEditController.add(TextEditingController(
text: toDoLineList[index].todoData));
textFocusNode.add(FocusNode());
textFocusNode[index].unfocus();
keyBoardFocusNode.add(FocusNode());
}
print(index);
print(textFocusNode);
return renderTextFormField(toDoLineList[index], index);
}),
),
),
],
),
);
}
}
`
i try to focusNode List move to glover and textfield wrap FocusScopeNode and many try things but i can't solve this problem...

FLUTTER : How to only rebuild the top page in stack flutter

I have an issue with rebuilding pages in the stack with Flutter
This is all my users and have added the search to the top appBar.
But it works with conditions to see if it is widgets there then get height of the widgets that is being fixed underneath the appBar...But that happens asynchronously.
So when firstLoad it works but when I call setState it then rebuilds all the pages in the stack and with that it looks like this
This is how it looks after a set state. The problem i saw is that the previous pages have an influence. I couldn't find a good viable solution to this. Will explain my architecture.
I have a page Layout that is a container wrapper for all my pages that has it's appBar styles and just sends through the children. But that is the page Layout wrapper that is being rebuild every time the a set States happen
HOW I GET MY WIDGET SIZE
HOW I IMPLEMENTED IT
It goes in the else with the other pages in the stack.. I tried putting it in the initState but it never goes inside because it is used in the other pages in the stack
I only need an implementation to rebuild the TOP page in the stack.
PAGE LAYOUT
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutterweb/constants.dart';
import 'package:flutterweb/controllers/channel_controller.dart';
import 'package:flutterweb/controllers/user_controller.dart';
import 'package:flutterweb/main.dart';
import 'package:flutterweb/models/channels_model.dart';
import 'package:flutterweb/utils/functions.dart';
import 'package:flutterweb/views/channels/func.dart';
import 'package:flutterweb/views/home/home.dart';
import 'package:flutterweb/views/menu/permissions/choose_assign_group.dart';
import 'package:flutterweb/widgets/builders/KNetworkFadeImage.dart';
import 'package:flutterweb/widgets/builders/kPopups.dart';
import 'package:flutterweb/widgets/drawerDara.dart';
import 'package:get/get.dart';
class CustomAppBar extends StatefulWidget {
final String title;
final List<Map<String, dynamic>>? topTabs;
final TabController? topTabController;
final List<Widget>? children;
final List<Widget>? childrenFixed;
final Function? leftActionFunction;
final Icon? leftActionIcon;
final Drawer? drawer;
final Function? logOutPressed;
final bool showOptionsMenu;
final Widget? optionMenu;
final Widget? bottomNavigationBar;
final String? backGroundImage;
final ScrollController? scrollController;
CustomAppBar({
required this.title,
this.topTabs,
this.topTabController,
this.leftActionFunction,
this.leftActionIcon,
this.children,
this.childrenFixed,
this.drawer,
this.logOutPressed,
this.showOptionsMenu = false,
this.optionMenu,
this.bottomNavigationBar,
this.backGroundImage,
this.scrollController,
});
#override
_CustomAppBarState createState() => _CustomAppBarState();
}
double app_content_height = 0;
double fixedWidgetSize = 0;
String prevTitle = "";
class _CustomAppBarState extends State<CustomAppBar>
with SingleTickerProviderStateMixin {
final GlobalKey<ScaffoldState> scaffoldkey = GlobalKey<ScaffoldState>();
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
_toggleAnimation() {
scaffoldkey.currentState!.openDrawer();
}
double _getAppBarSize() {
double fixedHeightInclude = fixedWidgetSize;
if (widget.topTabs != null) {
fixedHeightInclude += 100;
} else if (widget.title == "") {
fixedHeightInclude += 0;
} else {
fixedHeightInclude += 60;
}
return fixedHeightInclude;
}
#override
Widget build(BuildContext context) {
// if (widget.title != global_title) return SizedBox();
List<Widget> arr = [];
Widget arrView = SizedBox();
double statusBar = 0;
double _width = MediaQuery.of(context).size.width;
Widget? fixedChild = SizedBox();
if ((widget.childrenFixed?.length ?? 0) > 1) {
fixedChild = WidgetSize(
child: Column(children: widget.childrenFixed!),
onChange: (Size size) {
fixedWidgetSize = 0;
setState(() {
fixedWidgetSize = size.height;
});
kPrint("fixedWidgetSize ${size.height}");
},
);
} else {
fixedWidgetSize = 0;
}
// Widget? fixedChild = (widget.childrenFixed?.length ?? 0) > 1
// ? WidgetSize(
// child: Column(children: widget.childrenFixed!),
// onChange: (Size size) {
// fixedWidgetSize = 0;
// setState(() {
// fixedWidgetSize = size.height;
// });
// kPrint("fixedWidgetSize ${size.height}");
// },
// )
// : SizedBox();
app_content_height =
MediaQuery.of(context).size.height - _getAppBarSize() - statusBar;
if (widget.title != "") {
arr.add(
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 2,
child: widget.showOptionsMenu == true
? IconButton(
icon: const Icon(Icons.menu, color: Colors.white),
onPressed: () => _toggleAnimation(),
)
: IconButton(
icon: widget.leftActionIcon ??
const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {
if (widget.leftActionFunction != null) {
widget.leftActionFunction!();
} else {
if (Navigator.canPop(context)) {
Get.back();
}
}
},
),
),
Expanded(flex: 2, child: SizedBox()),
Expanded(
flex: 10,
child: Center(
child: Text(
widget.title,
style: const TextStyle(color: Colors.white, fontSize: 24.0),
),
),
),
Expanded(
flex: 4,
child: widget.logOutPressed != null
? IconButton(
icon: const Icon(Icons.power_settings_new_outlined,
color: Colors.white),
onPressed: () {
widget.logOutPressed!();
},
)
: widget.optionMenu ?? Container(),
),
],
),
);
}
if (widget.topTabs != null) {
List<Widget> tempTopBar = [];
List<Widget> tempTopView = [];
for (var i = 0; i < widget.topTabs!.length; i++) {
String key = widget.topTabs![i].keys
.toString()
.replaceAll("(", "")
.replaceAll(")", "");
Widget value = widget.topTabs![i][key];
tempTopBar.add(Tab(text: key));
tempTopView.add(SingleChildScrollView(child: value));
}
arr.add(
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Center(
child: Container(
height: 30,
child: DefaultTabController(
length: widget.topTabs!.length,
child: TabBar(
labelPadding: widget.topTabs!.length == 2
? const EdgeInsets.symmetric(horizontal: 40.0)
: const EdgeInsets.symmetric(horizontal: 16.0),
controller: widget.topTabController,
indicatorSize: TabBarIndicatorSize.tab,
indicator: CircleTabIndicator(color: Colors.white, radius: 4),
isScrollable: true,
labelColor: Colors.white,
tabs: tempTopBar,
),
),
),
),
),
);
// arr.add(child);
arrView = Container(
width: _width,
height: app_content_height,
child: TabBarView(
controller: widget.topTabController,
children: tempTopView,
),
);
}
if (widget.children != null) {
arrView = Container(
width: _width,
height: app_content_height,
child: ListView(
// controller: widget.scrollController ?? ScrollController(),
children: widget.children!,
),
);
}
_getStatus() {
if (statusBar > 0) {
Color color = AppColors.kBlue;
return Container(
height: Get.height * 0.03,
width: Get.width,
color: color,
child: const Center(
child: Text(
"",
style: const TextStyle(color: Colors.black),
),
),
);
} else {
return const SizedBox();
}
}
return SafeArea(
child: Material(
child: Stack(
children: [
Scaffold(
resizeToAvoidBottomInset:
true, //That the keyboard shows correctly
extendBodyBehindAppBar: true,
key: scaffoldkey,
appBar: PreferredSize(
preferredSize: Size.fromHeight(
_getAppBarSize()), // here the desired height
child: Container(
decoration: kAppBarBoxDecorations,
child: Column(
children: [
Column(
children: arr,
),
fixedChild,
],
),
),
),
drawer: Drawer(
child: ListView(
children: <Widget>[
UserAccountsDrawerHeader(
accountName: InkWell(
onTap: () {
Channels element =
ChannelController.to.gSelectedChannel.value;
getChannelRoles(element);
},
child: Text(
"${ChannelController.to.gSelectedChannel.value.rolDesc} >",
),
),
accountEmail: Text(
UserController.to.gUserModel.value.email.toString()),
currentAccountPicture: GestureDetector(
child: const CircleAvatar(
backgroundImage: NetworkImage(
"https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500"),
),
onTap: () => print("Current User")),
decoration: const BoxDecoration(
image: DecorationImage(
fit: BoxFit.fill,
image: NetworkImage(
"${URLS.keyBaseUrl}/assets/images/background/background7.jpg")),
),
),
ListTile(
title: const Text("Home"),
trailing: const Icon(Icons.home),
onTap: () =>
Get.toNamed(Home.router, preventDuplicates: false),
),
ListTile(
title: const Text("Menu Permissions"),
trailing: const Icon(Icons.home),
onTap: () => Get.toNamed(ChooseAssignGroup.router,
preventDuplicates: false),
),
const Divider(
thickness: 1.0,
),
drawerData(),
const Divider(
thickness: 1.0,
),
ListTile(
title: const Text("Close"),
trailing: const Icon(Icons.cancel),
onTap: () => Navigator.of(context).pop(),
),
ListTile(
title: const Text("Log Out"),
trailing: const Icon(Icons.logout),
onTap: () => UserController.to.logOutUser(),
),
],
),
),
body: Container(
decoration: widget.backGroundImage != null
? BoxDecoration(
color: Colors.black.withOpacity(0.9),
image: DecorationImage(
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
Colors.black.withOpacity(0.2), BlendMode.dstATop),
image: NetworkImage(widget.backGroundImage!),
),
)
: BoxDecoration(color: Colors.grey.shade400),
child: Center(
child: Container(
constraints: BoxConstraints(maxWidth: 800),
padding: EdgeInsets.only(
left: 15.0, right: 15.0, top: _getAppBarSize()),
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: arrView,
),
),
),
),
bottomNavigationBar: widget.bottomNavigationBar,
),
],
),
),
);
}
}
class CircleTabIndicator extends Decoration {
final BoxPainter _painter;
CircleTabIndicator({required Color color, required double radius})
: _painter = _CirclePainter(color, radius);
#override
BoxPainter createBoxPainter([onChanged()?]) => _painter;
}
class _CirclePainter extends BoxPainter {
final Paint _paint;
final double radius;
_CirclePainter(Color color, this.radius)
: _paint = Paint()
..color = color
..isAntiAlias = true;
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration cfg) {
final Offset circleOffset =
offset + Offset(cfg.size!.width / 2, cfg.size!.height - radius);
canvas.drawCircle(circleOffset, radius, _paint);
}
}
class WidgetSize extends StatefulWidget {
final Widget child;
final Function onChange;
const WidgetSize({
required this.onChange,
required this.child,
});
#override
_WidgetSizeState createState() => _WidgetSizeState();
}
class _WidgetSizeState extends State<WidgetSize> {
#override
Widget build(BuildContext context) {
SchedulerBinding.instance.addPostFrameCallback(postFrameCallback);
return Container(
key: widgetKey,
child: widget.child,
);
}
var widgetKey = GlobalKey();
var oldSize;
void postFrameCallback(_) {
var context = widgetKey.currentContext;
if (context == null) return;
var newSize = context.size;
if (oldSize == newSize) return;
oldSize = newSize;
widget.onChange(newSize);
}
}
ALL VERIFIED USERS
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutterweb/controllers/channel_controller.dart';
import 'package:flutterweb/controllers/user_controller.dart';
import 'package:flutterweb/main.dart';
import 'package:flutterweb/models/user_model.dart';
import 'package:flutterweb/thirdParty/googleSignin.dart';
import 'package:flutterweb/utils/functions.dart';
import 'package:flutterweb/views/menu/permissions/menu_assign.dart';
import 'package:flutterweb/widgets/builders/kPopups.dart';
import 'package:flutterweb/widgets/buttons/KIconOnlyButton.dart';
import 'package:flutterweb/widgets/builders/KNetworkFadeImage.dart';
import 'package:flutterweb/widgets/builders/customAppBar.dart';
import 'package:flutterweb/widgets/buttons/KButton.dart';
import 'package:flutterweb/constants.dart';
import 'package:flutterweb/widgets/cards/KStudentInfoCard.dart';
import 'package:flutterweb/widgets/cards/kStudentCard.dart';
import 'package:flutterweb/widgets/input/KInputBar.dart';
import 'package:flutterweb/widgets/input/KTextField.dart';
import 'package:flutterweb/widgets/static/kLabel.dart';
import 'package:flutterweb/widgets/text/kInfo.dart';
import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
class AllVerifiedUsers extends StatefulWidget {
static const String router = "/allVerifiedUsers";
AllVerifiedUsers({Key? key}) : super(key: key);
#override
_MyPageState createState() => _MyPageState();
}
// The controller for the ListView
late ScrollController _controller;
class _MyPageState extends State<AllVerifiedUsers> {
// The controller for the ListView
late ScrollController _controllerTest;
int _page = 1;
final int _limit = 20;
bool _hasNextPage = true;
bool _isFirstLoadRunning = false;
bool _isLoadMoreRunning = false;
List<UserModel> _posts = [];
String searchVal = "";
void _firstLoad() async {
setState(() {
_isFirstLoadRunning = true;
});
try {
List<UserModel> lUserMode = await ChannelController.to
.fetchUsersChannels(_limit, _page, searchVal);
setState(() {
_posts = lUserMode;
});
} catch (err) {
kPrint('Something went wrong');
}
setState(() {
_isFirstLoadRunning = false;
});
}
void _loadMore() async {
if (_isFirstLoadRunning == false &&
_isLoadMoreRunning == false &&
_controller.position.extentAfter < 300) {
setState(() {
_isLoadMoreRunning = true; // Display a progress indicator at the bottom
});
_page += 1; // Increase _page by 1
try {
List<UserModel> lUserMode = await ChannelController.to
.fetchUsersChannels(_limit, _page, searchVal);
if (lUserMode.isNotEmpty) {
setState(() {
_hasNextPage = true;
_posts.addAll(lUserMode);
});
} else {
// This means there is no more data
// and therefore, we will not send another GET request
setState(() {
_hasNextPage = false;
});
}
} catch (err) {
print('Something went wrong!');
}
setState(() {
_isLoadMoreRunning = false;
});
}
}
#override
void initState() {
super.initState();
_firstLoad();
_controller = ScrollController()..addListener(_loadMore);
_controllerTest = ScrollController()
..addListener(() => {kPrint("CustomView Scroll")});
}
#override
void dispose() {
super.dispose();
_controller.removeListener(_loadMore);
}
#override
Widget build(BuildContext context) {
global_title = "All Verified Users";
return CustomAppBar(
title: "All Verified Users",
scrollController: _controllerTest,
childrenFixed: [
kAddSpace(2),
CustomInputBar(
inverse: true,
title: "Search",
onChanged: (String value) {
if (value == "") {
_firstLoad();
return;
}
setState(() {
searchVal = value;
_posts = [];
});
_loadMore();
},
),
],
children: [
kAddSpace(2),
KLabel(
label: "Choose Verified User",
),
kAddSpace(2),
_isFirstLoadRunning
? const Center(
child: CircularProgressIndicator(),
)
: Column(
children: [
SizedBox(
height: app_content_height,
// width: Get.width,
child: ListView.builder(
shrinkWrap: true,
controller: _controller,
itemCount: _posts.length,
itemBuilder: (_, index) {
UserModel item = _posts[index];
return KStudentCard(
imgUrl: "",
onPressed: () {
ChannelController.to.gSelectedMenuUserModel.value =
item;
Get.toNamed(MenuAssign.router);
},
name: "${item.name} ${item.surname}",
);
},
),
),
// when the _loadMore function is running
if (_isLoadMoreRunning == true)
const Padding(
padding: EdgeInsets.only(top: 10, bottom: 40),
child: Center(
child: CircularProgressIndicator(),
),
),
// When nothing else to load
if (_hasNextPage == false)
Container(
padding: const EdgeInsets.only(top: 30, bottom: 40),
color: Colors.amber,
child: const Center(
child: Text('You have fetched all of the content'),
),
),
],
),
kAddSpace(2),
],
);
}
}
Thank you
As I understand it, the idea is a fixed TextArea and a scrollable List beneath. A solution that works without computing any height would be:
final items = List<String>.generate(1000, (i) => 'Item $i');
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: [
//
// TEXTBOX
//
Container(
color: Colors.red,
child: Padding(
padding: const EdgeInsets.all(16),
child: TextFormField(
// controller: controller,
))),
const SizedBox(height: 16),
//
// LIST
//
Expanded(
child: ListView.builder(
shrinkWrap: true, // IMPORTANT
itemCount: items.length,
itemBuilder: (context, index) {
return Text(items[index]);
})),
]),
);
}

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

Provider state management for PageController in 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: () {}
);
}
}

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